@mmerterden/multi-agent-pipeline 16.25.0 → 16.27.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +1 -1
  3. package/README.tr.md +1 -1
  4. package/install/templates/claude-hooks.json +32 -1
  5. package/package.json +1 -1
  6. package/pipeline/commands/multi-agent/help/SKILL.md +2 -0
  7. package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -1
  8. package/pipeline/commands/multi-agent/search/SKILL.md +28 -0
  9. package/pipeline/commands/multi-agent/setup/SKILL.md +18 -44
  10. package/pipeline/commands/multi-agent/status/SKILL.md +9 -0
  11. package/pipeline/lib/credential-inventory.sh +142 -18
  12. package/pipeline/lib/fetch-crashlytics.sh +123 -28
  13. package/pipeline/multi-agent-refs/features/url-enrichment.md +1 -1
  14. package/pipeline/multi-agent-refs/features/visual-evidence.md +5 -0
  15. package/pipeline/multi-agent-refs/keychain.md +65 -20
  16. package/pipeline/multi-agent-refs/knowledge.md +27 -0
  17. package/pipeline/multi-agent-refs/phases/operations.md +7 -1
  18. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +3 -1
  19. package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -8
  20. package/pipeline/multi-agent-refs/phases/phase-7-report.md +11 -21
  21. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  22. package/pipeline/multi-agent-refs/refactor/observations.md +81 -0
  23. package/pipeline/multi-agent-refs/setup/firebase.md +151 -0
  24. package/pipeline/schemas/learnings-ledger.schema.json +5 -0
  25. package/pipeline/schemas/prefs.schema.json +31 -3
  26. package/pipeline/schemas/skill-observation.schema.json +73 -0
  27. package/pipeline/scripts/capture-flush.sh +158 -0
  28. package/pipeline/scripts/capture-resume.sh +87 -0
  29. package/pipeline/scripts/crush-json.mjs +283 -0
  30. package/pipeline/scripts/firebase-app-discovery.sh +114 -0
  31. package/pipeline/scripts/keychain-save.sh +5 -8
  32. package/pipeline/scripts/keychain.py +76 -14
  33. package/pipeline/scripts/learn-from-transcripts.mjs +625 -0
  34. package/pipeline/scripts/learning-curve.mjs +22 -4
  35. package/pipeline/scripts/learnings-ledger.mjs +86 -12
  36. package/pipeline/scripts/note-session.sh +187 -0
  37. package/pipeline/scripts/observations.mjs +347 -0
  38. package/pipeline/scripts/offload-ref.sh +45 -2
  39. package/pipeline/scripts/pre-commit-check.sh +31 -1
  40. package/pipeline/scripts/scan-agent-config.sh +12 -3
  41. package/pipeline/scripts/skill-siblings.mjs +187 -0
  42. package/pipeline/scripts/triage-memory.mjs +73 -9
  43. package/pipeline/skills/.skill-manifest.json +1 -1
  44. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -1
  45. package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +28 -0
  46. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +37 -6
  47. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +9 -0
@@ -25,6 +25,11 @@
25
25
  * query --issue "<text>" [--file-glob "Auth*"] [--top N]
26
26
  * [--repo-slug <slug>] [--json]
27
27
  * show --id <id> [--repo-slug <slug>]
28
+ * timeline --anchor <id> [--before N] [--after N] [--repo-slug <slug>]
29
+ * What surrounded a finding when it was recorded. The corpus is
30
+ * append-only and written in order, so line adjacency is
31
+ * chronological adjacency - the neighbours came from the same
32
+ * review pass, and are usually what makes an old row legible.
28
33
  * Print the full row behind a pointer emitted by query/profile.
29
34
  * path [--repo-slug <slug>] Print the resolved corpus file path
30
35
  * stats [--repo-slug <slug>] Print corpus row counts grouped by classification
@@ -295,8 +300,11 @@ function ingest() {
295
300
  }
296
301
  }
297
302
  process.stdout.write(JSON.stringify({ ok: true, slug, ingested: written }) + "\n");
298
- if (written === 0) process.exit(2);
299
- process.exit(0);
303
+ // exitCode, not exit(): even this one-line write is on the same rule, and a
304
+ // rule with an exception is the one that gets copied into the next branch.
305
+ // 2 = nothing new, which callers treat as an idempotent success.
306
+ process.exitCode = written === 0 ? 2 : 0;
307
+ return;
300
308
  }
301
309
 
302
310
  function query() {
@@ -308,7 +316,8 @@ function query() {
308
316
  const corpus = readCorpus(slug);
309
317
  if (corpus.length === 0) {
310
318
  process.stdout.write(JSON.stringify({ ok: true, slug, hits: [], total: 0 }) + "\n");
311
- process.exit(2);
319
+ process.exitCode = 2;
320
+ return;
312
321
  }
313
322
  const qTerms = tokens(issueText);
314
323
  const fileRe = fileGlob ? globToRegExp(fileGlob) : null;
@@ -380,7 +389,8 @@ function query() {
380
389
  process.stdout.write(
381
390
  JSON.stringify({ ok: true, slug, total: filtered.length, hits: ranked }) + "\n",
382
391
  );
383
- process.exit(0);
392
+ process.exitCode = 0;
393
+ return;
384
394
  }
385
395
 
386
396
  // Drill-down: a compact prior-art line carries only its id, so the agent that
@@ -394,17 +404,65 @@ function showCmd() {
394
404
  const row = readCorpus(slug).find((r) => rowId(r) === want);
395
405
  if (!row) {
396
406
  process.stdout.write(JSON.stringify({ ok: false, slug, id: want, found: false }) + "\n");
397
- process.exit(2);
407
+ process.exitCode = 2;
408
+ return;
398
409
  }
399
410
  process.stdout.write(JSON.stringify({ ok: true, slug, id: want, row }) + "\n");
400
- process.exit(0);
411
+ process.exitCode = 0;
412
+ return;
413
+ }
414
+
415
+ /**
416
+ * Neighbourhood: what surrounded a finding when it was recorded.
417
+ *
418
+ * Search answers "which rows match", and that is a different question from
419
+ * "what else happened around this one". A finding almost never travels alone -
420
+ * the same review pass produced the ones next to it, and those are often the
421
+ * context that makes it legible a year later. Without this, the only way to ask
422
+ * was to fetch the whole corpus and page through it, so nobody asked.
423
+ *
424
+ * The corpus is append-only and written in order, so line adjacency IS
425
+ * chronological adjacency. That is the whole implementation: no index, no
426
+ * timestamp arithmetic, no second data structure to keep in sync.
427
+ */
428
+ function timelineCmd() {
429
+ const id = flags.anchor && flags.anchor !== true ? String(flags.anchor) : null;
430
+ if (!id) die("timeline needs --anchor <id>", 1);
431
+ const slug = flags["repo-slug"] ? String(flags["repo-slug"]) : repoSlug();
432
+ const before = flags.before && flags.before !== true ? Math.max(0, Number(flags.before)) : 3;
433
+ const after = flags.after && flags.after !== true ? Math.max(0, Number(flags.after)) : 3;
434
+ const want = id.startsWith("T:") ? id : `T:${id}`;
435
+ const corpus = readCorpus(slug);
436
+ const at = corpus.findIndex((r) => rowId(r) === want);
437
+ if (at === -1) {
438
+ process.stdout.write(JSON.stringify({ ok: false, slug, anchor: want, found: false }) + "\n");
439
+ process.exitCode = 2;
440
+ return;
441
+ }
442
+ const from = Math.max(0, at - before);
443
+ const to = Math.min(corpus.length, at + after + 1);
444
+ const rows = corpus.slice(from, to).map((r, i) => ({
445
+ id: rowId(r),
446
+ offset: from + i - at,
447
+ task_id: r.task_id,
448
+ classification: r.classification,
449
+ severity: r.severity,
450
+ file: r.file,
451
+ issue: r.issue,
452
+ }));
453
+ process.stdout.write(
454
+ JSON.stringify({ ok: true, slug, anchor: want, before, after, rows }, null, 2) + "\n",
455
+ );
456
+ process.exitCode = 0;
457
+ return;
401
458
  }
402
459
 
403
460
  function pathCmd() {
404
461
  process.stdout.write(
405
462
  corpusPath(flags["repo-slug"] ? String(flags["repo-slug"]) : repoSlug()) + "\n",
406
463
  );
407
- process.exit(0);
464
+ process.exitCode = 0;
465
+ return;
408
466
  }
409
467
 
410
468
  function statsCmd() {
@@ -424,7 +482,8 @@ function statsCmd() {
424
482
  out.bytes = s.size;
425
483
  }
426
484
  process.stdout.write(JSON.stringify(out) + "\n");
427
- process.exit(0);
485
+ process.exitCode = 0;
486
+ return;
428
487
  }
429
488
 
430
489
  switch (SUB) {
@@ -437,6 +496,9 @@ switch (SUB) {
437
496
  case "show":
438
497
  showCmd();
439
498
  break;
499
+ case "timeline":
500
+ timelineCmd();
501
+ break;
440
502
  case "path":
441
503
  pathCmd();
442
504
  break;
@@ -444,6 +506,8 @@ switch (SUB) {
444
506
  statsCmd();
445
507
  break;
446
508
  default:
447
- process.stderr.write("usage: triage-memory.mjs <ingest|query|show|path|stats> [flags]\n");
509
+ process.stderr.write(
510
+ "usage: triage-memory.mjs <ingest|query|show|timeline|path|stats> [flags]\n",
511
+ );
448
512
  process.exit(64);
449
513
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-09T17:46:50Z",
3
+ "generatedAt": "2026-09-09T18:15:55Z",
4
4
  "skillCount": 207,
5
5
  "entries": [
6
6
  {
@@ -12,7 +12,7 @@ argument-hint: 'bugs | best-practices | drift | multi-agent-toolkit | run-errors
12
12
 
13
13
  Deep-analyses the current project, extracts the global best-practices worth adopting (adapted to our stack), hunts real bugs and improvement areas, checks whether any skills we derived from an upstream source have drifted, researches the companion multi-agent-toolkit MCP server against current MCP practice, scores everything, drafts a single prioritized plan, asks the user for approval, applies the approved items, and asks about sync at the end.
14
14
 
15
- **Input**: $ARGUMENTS (optional - area to focus on: "security", "performance", "tests", "bugs", "best-practices", "drift", "multi-agent-toolkit", etc.)
15
+ **Input**: $ARGUMENTS (optional - `backlog` runs Step 0e alone; otherwise an area to focus on: "security", "performance", "tests", "bugs", "best-practices", "drift", "multi-agent-toolkit", etc.)
16
16
 
17
17
  ## Flow
18
18
 
@@ -134,6 +134,14 @@ Each line is one run: `{ t, id, u, c, rp, ph, v, errs[] }`. The `errs[]` entries
134
134
 
135
135
  This band mirrors the admin dashboard's "Gelişim alanları" panel, but reads the local ledger so it needs no auth and works offline. If `$ARGUMENTS` names a focus area, still read the ledger - a recurring run error in that area is the strongest possible signal.
136
136
 
137
+ Then read what the transcripts themselves record, which is the half no model noticed:
138
+
139
+ ```bash
140
+ node "$HOME/.claude/scripts/learn-from-transcripts.mjs" --json
141
+ ```
142
+
143
+ It correlates rather than interprets - a failed read against the read that then worked, an identical invocation that kept failing, a search that was too narrow, a call the user refused twice, a file too big to read whole. Dry run by default; `--apply` writes them to the learnings ledger as `source: transcript-mining` and renders a marked block in `CLAUDE.local.md`. Zero candidates alongside a non-zero `toolResultsExamined` means there was nothing to find; zero of both means the read is broken.
144
+
137
145
  Output (plan band F):
138
146
 
139
147
  ```
@@ -149,6 +157,20 @@ Rules for this band:
149
157
  - Never quote a user's identity as blame. The `u` field is for counting distinct affected users, not for naming anyone in the plan.
150
158
  - If the ledger is empty or absent, skip band F silently - it is additive signal, never a gate.
151
159
 
160
+ ## Step 0e: BACKLOG - friction this pipeline already noticed
161
+
162
+ Every other band in this command derives its findings from scratch, which is right for a sweep and has one consequence nobody chose: a friction noticed mid-task is gone by the next run unless it was fixed within the hour. Most are not, because stopping mid-task to fix them is the wrong call. So they are written down when noticed and decided here.
163
+
164
+ ```bash
165
+ node "$HOME/.claude/scripts/observations.mjs" scan --status open --json
166
+ ```
167
+
168
+ Exit 3 is `SCAN BROKEN` - files on disk, none parsed. That is a broken reader, not an empty queue: halt the band and say so rather than reporting "0 open".
169
+
170
+ Split every open observation into actionable / sibling-propagation / to-decline, and resolve the sibling surfaces mechanically with `skill-siblings.mjs`, never from memory - a command is authored in `commands/`, mirrored into `skills/shared/core/`, and installed again into three host trees, so a fix applied to one copy drifts from the rest with every gate still green. Output as plan band G, same table shape as the other bands.
171
+
172
+ `/multi-agent:refactor backlog` runs this band alone. The full sweep is unchanged and still includes it. Buckets, the status vocabulary, why `parked` needs a named unblocking event, and how to write an observation: `$HOME/.claude/multi-agent-refs/refactor/observations.md`.
173
+
152
174
  ## Step 1: SCAN
153
175
 
154
176
  ```
@@ -38,6 +38,34 @@ Text (default): colored, `task_id · project · branch · matches` + title + dat
38
38
 
39
39
  JSON: `{ query, matched_tasks, tasks: [{ task_id, project, branch, date, matches, score, title, snippet }] }`.
40
40
 
41
+ ## Four layers, and what each one costs
42
+
43
+ Retrieval is cheap or expensive depending on which layer answers, and the order
44
+ matters more than the query does. Filter first, fetch second - inverting these
45
+ is how a single lookup eats a phase's whole context budget.
46
+
47
+ | # | Call | Returns | Rough cost per hit |
48
+ |---|---|---|---|
49
+ | 1 | `search` / `--semantic` | pointers: id, file, one-line issue | ~50-100 tokens |
50
+ | 2 | `timeline --anchor <id>` | the rows recorded around one hit | ~50-100 tokens each |
51
+ | 3 | `show --id <id>` | one full row | ~500-1000 tokens |
52
+ | 4 | the offloaded artefact behind a `[[ref:<id>]]` | the raw payload | thousands |
53
+
54
+ Never start at layer 3 or 4. Narrow at layer 1, widen at layer 2 only around the
55
+ hits that survived, and fetch full rows only for the handful that are actually
56
+ going to be read. A `show` on twenty ids costs as much as the entire search that
57
+ found them and answers a question nobody asked.
58
+
59
+ Layer 2 is the one that gets forgotten. A finding rarely stands alone: the same
60
+ review pass produced its neighbours, and those are usually what makes an old row
61
+ legible. Both stores answer it, and the corpus is append-only so line adjacency
62
+ IS chronological adjacency:
63
+
64
+ ```bash
65
+ node "$HOME/.claude/scripts/triage-memory.mjs" timeline --anchor T:<id> --before 3 --after 3
66
+ node "$HOME/.claude/scripts/learnings-ledger.mjs" timeline --anchor L:<id> --before 3 --after 3
67
+ ```
68
+
41
69
  ## Steps
42
70
 
43
71
  1. Script:
@@ -227,7 +227,7 @@ Standard key names (used when creating NEW tokens). Discovery may find existing
227
227
  | `figma` | `${USER}_Figma_Access_Token` | PAT | Figma Developer Settings (max 90 days) |
228
228
  | `figma_mcp` | `${USER}_Figma_Mcp_Access_Token` | OAuth | Automatic via Claude Code Figma MCP remote auth |
229
229
  | `fortify` | `${USER}_Fortify_Access_Token` | API Token | Fortify SSC -> Token Management (VPN required) |
230
- | `firebase` | `${USER}_Firebase_Access_Json` | Firebase JSON (base64) | Firebase Console -> Project settings -> Service accounts -> Generate new private key. Project id is parsed from the decoded JSON. |
230
+ | `firebase` | `${USER}_Firebase_Access_Json` | Firebase service-account JSON | Firebase Console -> Project settings -> Service accounts -> Generate new private key. Store the file as issued; project id is read straight out of it. |
231
231
  | `jenkins` | `${USER}_Jenkins_Access_Token` | API Token | Jenkins -> User -> Configure -> API Token |
232
232
  | - | Git Identities | JSON (preferences) | Stored in `$HOME/.claude/multi-agent-preferences.json` - not Keychain |
233
233
 
@@ -243,14 +243,11 @@ pbcopy < /dev/null # clear clipboard
243
243
  # Plain text (direct value)
244
244
  ~/.copilot/lib/credential-store.sh set "<SERVICE_NAME>" "<VALUE>"
245
245
 
246
- # JSON file (base64 encode)
247
- ~/.copilot/lib/credential-store.sh set "<SERVICE_NAME>" "$(base64 < /path/to/file.json)"
246
+ # JSON file (stored as issued - the store round-trips newlines byte-exactly)
247
+ ~/.copilot/lib/credential-store.sh set "<SERVICE_NAME>" "$(cat /path/to/file.json)"
248
248
 
249
249
  # Read
250
250
  ~/.copilot/lib/credential-store.sh get "<SERVICE_NAME>"
251
-
252
- # Decode JSON
253
- ~/.copilot/lib/credential-store.sh get "<SERVICE_NAME>" | base64 -d
254
251
  ```
255
252
 
256
253
  For maximum stdin hygiene (the secret never lands on argv / shell history), use the Python helper directly with the `-` sentinel:
@@ -325,6 +322,40 @@ A credential that resolves but is rejected (401/403) follows the Expired-token
325
322
  decision (Regenerate / Use a different token / Skip and continue) rather than
326
323
  being silently dropped.
327
324
 
325
+ ### Firebase / Crashlytics Access (optional, any platform)
326
+
327
+ Discovery maps a stored service-account JSON like any other credential, but it
328
+ cannot see the thing that decides whether Crashlytics is readable: a freshly
329
+ generated `firebase-adminsdk-*` account authenticates perfectly and holds no
330
+ Crashlytics role. Measure it, do not assume it:
331
+
332
+ ```bash
333
+ bash "$HOME/.claude/lib/fetch-crashlytics.sh" --probe
334
+ ```
335
+
336
+ Ask (picker): **Complete tier 1** / **Set up tier 2** / **Skip**.
337
+
338
+ **Tier 1 - service account.** Store the JSON exactly as Firebase Console issued
339
+ it; it is multi-line and the store round-trips it byte for byte. Never base64 it -
340
+ a wrapper on the way in that nothing unwraps on the way out is how this path
341
+ stayed broken. When the probe says `tier-1-no-grant`, the fix is one line to an
342
+ Owner: grant `roles/firebasecrashlytics.viewer` on the project to that service
343
+ account. Read-only, and narrower than `roles/firebase.viewer`.
344
+
345
+ **Tier 2 - interactive session + MCP.** `firebase login` opens a browser and does
346
+ not work from inside an agent harness; the user runs it in their own terminal.
347
+ Both halves are required - a login with no MCP server registered reaches nothing.
348
+ It is the bridge while a role request moves, never the durable answer: autopilot
349
+ and url-enrichment run with nobody at the keyboard.
350
+
351
+ **appId discovery.** `scripts/firebase-app-discovery.sh <repo> --json` reads the
352
+ bundle-id / appId pairs the repo's `GoogleService-Info*.plist` and
353
+ `google-services.json` already carry, shaped for
354
+ `prefs.global.firebase.accounts[].apps[]`. Merging them saves the fetcher a
355
+ Management API round trip per run; skipping it changes nothing else.
356
+
357
+ Full mechanics and the v1alpha caveat: `multi-agent-refs/setup/firebase.md`.
358
+
328
359
  ### Security Rules
329
360
 
330
361
  - Never paste tokens into terminal or chat - use `pbpaste` for input
@@ -36,6 +36,15 @@ Show every active and completed task as a table.
36
36
  💡 log #1 | resume #N | kill #N
37
37
  ```
38
38
 
39
+ 6. **What is waiting to be learned** (one line, only when non-zero):
40
+
41
+ ```bash
42
+ node "$HOME/.claude/scripts/learn-from-transcripts.mjs" --json 2>/dev/null
43
+ node "$HOME/.claude/scripts/observations.mjs" scan --status open --json 2>/dev/null
44
+ ```
45
+
46
+ Report `N lesson(s) minable from transcripts - /multi-agent:refactor to review` and `N open pipeline observation(s)` when either count is above zero, and print nothing when both are zero. Neither runs a model and neither writes: the miner is dry-run by default. Zero candidates with a non-zero `toolResultsExamined` means nothing to find; zero of both means the read is broken, which is worth saying rather than reporting a clean queue.
47
+
39
48
  5. **Short command suggestions** - Based on state:
40
49
  - If there is a paused task → suggest `resume #N`
41
50
  - If there is a done task → suggest `log #N`