toga-ai 1.0.47 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -36,11 +36,12 @@ Dispatched via action `Elite/Webhook` on the worker queue.
|
|
|
36
36
|
|
|
37
37
|
## Constants
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
Defined in `worker2/Worker/Elite.php` — **values not reproduced here (credentials);
|
|
40
|
+
read them from the source/config:**
|
|
41
|
+
|
|
42
|
+
- `CLIENT_UUID_ELITE` — Elite's TOGA 2 client UUID
|
|
43
|
+
- `API_UUID_ELITE` — API identity UUID
|
|
44
|
+
- `API_SECRET_ELITE` — API secret (sensitive)
|
|
44
45
|
|
|
45
46
|
## Freshservice API — gotchas
|
|
46
47
|
|
|
@@ -3,10 +3,10 @@ title: Elite
|
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
project: Worker
|
|
5
5
|
client: elite
|
|
6
|
-
type:
|
|
6
|
+
type: profile
|
|
7
7
|
status: active
|
|
8
8
|
updated: 2026-06-10
|
|
9
|
-
owners: []
|
|
9
|
+
owners: [snaredla]
|
|
10
10
|
files:
|
|
11
11
|
- worker2/Worker/Elite.php
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -23,13 +23,12 @@ keeps Freshservice and TOGA 2 in sync via webhook-driven worker jobs.
|
|
|
23
23
|
|
|
24
24
|
## TOGA 2 API credentials
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Three constants live in `worker2/Worker/Elite.php` — **values intentionally not reproduced
|
|
27
|
+
here. Read them from the source/config; never paste secret values into knowledge docs.**
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const API_SECRET_ELITE = 'c2701d21-1908-4895-aaed-2e6456710e8d';
|
|
32
|
-
```
|
|
29
|
+
- `CLIENT_UUID_ELITE` — Elite's TOGA 2 client UUID
|
|
30
|
+
- `API_UUID_ELITE` — API identity UUID
|
|
31
|
+
- `API_SECRET_ELITE` — API secret (sensitive; treat as a credential)
|
|
33
32
|
|
|
34
33
|
The library version of the sync methods (`library/app/api/toga2.php`) passes these in as
|
|
35
34
|
parameters — they are not constants there.
|
package/knowledge.js
CHANGED
|
@@ -183,6 +183,19 @@ function cmdManifest() {
|
|
|
183
183
|
/* front-end|hybrid> --q=<keywords to match feature docs> */
|
|
184
184
|
/* ------------------------------------------------------------------ */
|
|
185
185
|
|
|
186
|
+
function extractSection(body, heading) {
|
|
187
|
+
const lines = body.split(/\r?\n/);
|
|
188
|
+
const want = ('## ' + heading).toLowerCase();
|
|
189
|
+
const start = lines.findIndex(l => l.trim().toLowerCase() === want);
|
|
190
|
+
if (start === -1) return '';
|
|
191
|
+
const out = [];
|
|
192
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
193
|
+
if (/^##\s/.test(lines[i])) break;
|
|
194
|
+
out.push(lines[i]);
|
|
195
|
+
}
|
|
196
|
+
return out.join('\n').trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
186
199
|
function cmdPreflight(args) {
|
|
187
200
|
const registry = loadRegistry();
|
|
188
201
|
const chosen = String(args.repos || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -217,21 +230,27 @@ function cmdPreflight(args) {
|
|
|
217
230
|
const fileInfo = (rel) => ({ path: rel, exists: fs.existsSync(path.join(ROOT, rel.split('/').join(path.sep))) });
|
|
218
231
|
const matchQ = (d) => !q || (((d.data.title || '') + ' ' + (d.data.files || []).join(' ') + ' ' + d.body).toLowerCase().includes(q));
|
|
219
232
|
|
|
220
|
-
// per-repo inventory
|
|
233
|
+
// per-repo inventory. Chosen repos load FULL architecture + matched features.
|
|
234
|
+
// Non-chosen repos (deps, incl. framework core) get their `## Summary` inlined
|
|
235
|
+
// here so kickoff reads zero extra files for them — full doc lazy on demand.
|
|
221
236
|
const repoOut = loadSet.map(repo => {
|
|
222
237
|
const e = registry.find(x => x.repo === repo);
|
|
223
238
|
const fw = e ? e.framework : '';
|
|
224
239
|
const repoDir = path.join(ROOT, fw, 'apps', repo);
|
|
225
240
|
const isChosen = chosenSet.has(repo);
|
|
241
|
+
const archRel = `${fw}/apps/${repo}/architecture.md`;
|
|
242
|
+
const archDoc = docs.find(d => d.rel === archRel);
|
|
226
243
|
let features = [];
|
|
227
244
|
if (isChosen) {
|
|
228
245
|
features = docs
|
|
229
246
|
.filter(d => d.file.startsWith(repoDir + path.sep) && d.data.type !== 'architecture' && matchQ(d))
|
|
230
247
|
.map(d => ({ path: d.rel, title: d.data.title || '', files: (d.data.files || []).join(', ') }));
|
|
231
248
|
}
|
|
249
|
+
const summary = (!isChosen && archDoc) ? (extractSection(archDoc.body, 'Summary') || firstSentence(archDoc.body)) : '';
|
|
232
250
|
return {
|
|
233
251
|
repo, framework: fw, project: e ? e.project : '', role: e ? e.role : '', chosen: isChosen,
|
|
234
|
-
architecture: fileInfo(
|
|
252
|
+
architecture: fileInfo(archRel),
|
|
253
|
+
summary,
|
|
235
254
|
features,
|
|
236
255
|
};
|
|
237
256
|
});
|
|
@@ -265,16 +284,22 @@ function cmdPreflight(args) {
|
|
|
265
284
|
};
|
|
266
285
|
}
|
|
267
286
|
|
|
268
|
-
// ordered read list — mirrors kickoff Step 4
|
|
287
|
+
// ordered read list — mirrors kickoff Step 4 precedence (core first, then chosen
|
|
288
|
+
// repos + features, then standards, then client). Chosen repos → a FULL architecture
|
|
289
|
+
// read; non-chosen → an inline-`summary` entry the skill should NOT open unless it
|
|
290
|
+
// needs deeper detail (lazy:true). Core ordering preserved so foundational context
|
|
291
|
+
// (even when summarized) comes before app context.
|
|
269
292
|
const reads = [];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
293
|
+
const pushRepoRead = (r) => {
|
|
294
|
+
if (r.chosen) {
|
|
295
|
+
reads.push({ label: 'repo-architecture', repo: r.repo, path: r.architecture.path, exists: r.architecture.exists });
|
|
296
|
+
for (const f of r.features) reads.push({ label: 'feature', repo: r.repo, path: f.path, exists: true, title: f.title });
|
|
297
|
+
} else {
|
|
298
|
+
reads.push({ label: 'architecture-summary', repo: r.repo, role: r.role, path: r.architecture.path, exists: r.architecture.exists, lazy: true, summary: r.summary });
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
for (const r of repoOut.filter(r => r.role === 'core')) pushRepoRead(r);
|
|
302
|
+
for (const r of repoOut.filter(r => r.role !== 'core')) pushRepoRead(r);
|
|
278
303
|
for (const s of standards) reads.push({ label: 'standard', path: s.path, exists: s.exists });
|
|
279
304
|
if (client) {
|
|
280
305
|
reads.push({ label: 'client-profile', path: client.profile.path, exists: client.profile.exists });
|
|
@@ -516,6 +541,37 @@ function relRow(d, baseDir) {
|
|
|
516
541
|
return `| [${d.data.title || path.basename(d.file, '.md')}](${relLink(d, baseDir)}) | ${firstSentence(d.body)} | ${(d.data.files || []).join(', ')} |`;
|
|
517
542
|
}
|
|
518
543
|
|
|
544
|
+
/* ------------------------------------------------------------------ */
|
|
545
|
+
/* secret scanner — knowledge docs must never contain credential */
|
|
546
|
+
/* values. High-confidence patterns only (named secret assignments, */
|
|
547
|
+
/* AWS keys, PEM blocks) so legitimate UUIDs/IDs don't false-positive. */
|
|
548
|
+
/* Wired into validate → blocks publish on a hit. */
|
|
549
|
+
/* ------------------------------------------------------------------ */
|
|
550
|
+
|
|
551
|
+
const SECRET_NAME = /(secret|password|passwd|api[_-]?key|apikey|access[_-]?key|client[_-]?secret|private[_-]?key|auth[_-]?token|bearer)/i;
|
|
552
|
+
const SECRET_PLACEHOLDER = /(<[^>]+>|redact|example|placeholder|your[_-]|xxxx|\.\.\.|\*\*\*|changeme|dummy|sensitive|not reproduced)/i;
|
|
553
|
+
|
|
554
|
+
function scanSecrets(content) {
|
|
555
|
+
const hits = [];
|
|
556
|
+
const lines = content.split(/\r?\n/);
|
|
557
|
+
for (let i = 0; i < lines.length; i++) {
|
|
558
|
+
const ln = lines[i];
|
|
559
|
+
if (/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/.test(ln)) { hits.push({ line: i + 1, reason: 'private key block' }); continue; }
|
|
560
|
+
if (/\bAKIA[0-9A-Z]{16}\b/.test(ln)) { hits.push({ line: i + 1, reason: 'AWS access key id' }); continue; }
|
|
561
|
+
// NAME = "value" or NAME: value where NAME looks like a credential
|
|
562
|
+
const m = ln.match(/([A-Za-z0-9_]*(?:secret|password|passwd|api[_-]?key|apikey|access[_-]?key|client[_-]?secret|private[_-]?key|auth[_-]?token)[A-Za-z0-9_]*)\s*[:=]\s*['"]?([^'"\s]{8,})['"]?/i);
|
|
563
|
+
if (m && SECRET_NAME.test(m[1]) && !SECRET_PLACEHOLDER.test(ln)) {
|
|
564
|
+
// Only flag a literal-looking value (hex/token/base64). Reject code
|
|
565
|
+
// references like `_Config::database(...)`, `getenv(...)`, `$var` — those
|
|
566
|
+
// are how secrets SHOULD be loaded, not leaked values.
|
|
567
|
+
const val = m[2];
|
|
568
|
+
const looksLiteral = /^[A-Za-z0-9_\-\/+=.]{8,}$/.test(val) && !/^[A-Z][A-Za-z0-9_]*$/.test(val);
|
|
569
|
+
if (looksLiteral) hits.push({ line: i + 1, reason: `literal value assigned to "${m[1]}"` });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return hits;
|
|
573
|
+
}
|
|
574
|
+
|
|
519
575
|
/* ------------------------------------------------------------------ */
|
|
520
576
|
/* command: validate */
|
|
521
577
|
/* ------------------------------------------------------------------ */
|
|
@@ -572,6 +628,10 @@ function cmdValidate() {
|
|
|
572
628
|
}
|
|
573
629
|
}
|
|
574
630
|
if (ELEVATED_TYPES.includes(d.data.type)) elevated.push(d.rel);
|
|
631
|
+
for (const s of scanSecrets(fs.readFileSync(d.file, 'utf8'))) {
|
|
632
|
+
fail(`${d.rel}:${s.line}: possible secret committed — ${s.reason}; redact it (reference the config/constant location, never paste credential values into knowledge docs)`);
|
|
633
|
+
ok = false;
|
|
634
|
+
}
|
|
575
635
|
}
|
|
576
636
|
|
|
577
637
|
if (elevated.length) {
|
package/package.json
CHANGED
package/skills/capture/SKILL.md
CHANGED
|
@@ -143,6 +143,12 @@ For a client `profile.md`, the `title:` **is** the client's formal name (e.g. "C
|
|
|
143
143
|
there is no separate name field; `validate` requires `title`. See New-client onboarding.
|
|
144
144
|
Add `related:` cross-links. Append new repos to `<TEAM_REPO>/knowledge/registry.json`.
|
|
145
145
|
|
|
146
|
+
**Never paste secret VALUES into a doc** — no API secrets, passwords, private keys, tokens,
|
|
147
|
+
or live credential strings. Document *where* a credential lives (the constant name, the
|
|
148
|
+
config key, the file) but not its value. `validate` (run by Step 6's publish and the
|
|
149
|
+
pre-commit hook) scans for credential literals and **fails the publish** if it finds one, so
|
|
150
|
+
a leaked secret blocks the whole capture until redacted. Reference, never reproduce.
|
|
151
|
+
|
|
146
152
|
## Step 6 — Publish (validate → index → mirror → rebase-push, one command)
|
|
147
153
|
|
|
148
154
|
Finish every capture with the deterministic publisher — **do not** run validate/index/git
|
package/skills/kickoff/SKILL.md
CHANGED
|
@@ -144,10 +144,13 @@ node "<TEAM_REPO>/knowledge.js" kickoff-preflight \
|
|
|
144
144
|
|
|
145
145
|
It prints minified JSON:
|
|
146
146
|
- `loadSet` — framework core(s) + chosen repos + transitive `dependsOn`, in load order.
|
|
147
|
-
- `repos[]` — each with `architecture` `{path,exists}`, `role`, `chosen`,
|
|
148
|
-
`features[]` (features
|
|
147
|
+
- `repos[]` — each with `architecture` `{path,exists}`, `role`, `chosen`, an inline
|
|
148
|
+
`summary` (for non-chosen repos), and matched `features[]` (features returned only for the
|
|
149
|
+
repos the developer chose, not deps).
|
|
149
150
|
- `standards[]` and `client` `{title, profile, docs[]}` — both `{path,exists}`-flagged.
|
|
150
|
-
- `reads[]` — **the ordered list
|
|
151
|
+
- `reads[]` — **the ordered work list** (see Step 4). Full-read entries carry `{label, path,
|
|
152
|
+
exists}`; non-chosen repos instead carry `{label:'architecture-summary', summary, lazy:true}`
|
|
153
|
+
with the Summary text inlined so no file read is needed.
|
|
151
154
|
- `unknown[]` — any `--repos` value not in the registry (run New-repo onboarding for each).
|
|
152
155
|
|
|
153
156
|
**Do not run `deps` or `search` separately, and do not probe the filesystem for these docs.**
|
|
@@ -163,14 +166,25 @@ Never ask for a repo not in `loadSet`.
|
|
|
163
166
|
|
|
164
167
|
## Step 4 — Load the knowledge
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
Walk the preflight `reads[]` array **in order**. Each entry is one of:
|
|
170
|
+
|
|
171
|
+
- **`lazy:true` (label `architecture-summary`)** — a repo the developer did NOT choose as
|
|
172
|
+
focus (a dependency or framework core). The entry already carries an inline `summary`
|
|
173
|
+
string — **use that summary as your context for this repo; do NOT open the file.** Only
|
|
174
|
+
open its `path` later, on demand, if a task actually requires that repo's deep internals.
|
|
175
|
+
This is the token-saving default: foundational orientation without loading the full doc.
|
|
176
|
+
- **Any other entry (no `lazy` flag)** — the chosen repos' architecture + matched feature
|
|
177
|
+
docs, standards, and client docs. **Read these in full**, resolving `path` against
|
|
178
|
+
`<TEAM_REPO>/knowledge/`.
|
|
179
|
+
- **`exists:false`** — skip; that doc isn't written yet. Note it in the Step 5 summary as
|
|
180
|
+
"no knowledge captured yet."
|
|
181
|
+
|
|
182
|
+
The `reads[]` order already encodes the correct precedence (core first, then chosen-repo
|
|
183
|
+
architecture + matched features, then standards, then client profile + client docs) and
|
|
184
|
+
already unions across `1.0/` and `2.0/`. Do not re-derive this order or run further `search`.
|
|
185
|
+
|
|
186
|
+
If, while working, you find you need the full architecture of a summarized (`lazy`) repo,
|
|
187
|
+
just read its `path` then — that's the intended lazy-load, not a kickoff failure.
|
|
174
188
|
|
|
175
189
|
**Do NOT create or modify any `CLAUDE.md` stub** — kickoff only reads. A blank session is a
|
|
176
190
|
valid choice; this skill is opt-in.
|