@mmerterden/multi-agent-pipeline 16.21.0 → 16.23.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 (38) hide show
  1. package/CHANGELOG.md +208 -103
  2. package/README.md +5 -7
  3. package/README.tr.md +5 -7
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/install/templates/copilot-instructions.md +17 -2
  7. package/package.json +1 -1
  8. package/pipeline/commands/multi-agent/SKILL.md +0 -5
  9. package/pipeline/commands/multi-agent/help/SKILL.md +0 -10
  10. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  11. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -1
  12. package/pipeline/commands/multi-agent/resume-local/SKILL.md +2 -2
  13. package/pipeline/commands/multi-agent/sync/SKILL.md +13 -13
  14. package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -12
  15. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  16. package/pipeline/multi-agent-refs/phases/phase-0-init.md +7 -2
  17. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -1
  18. package/pipeline/multi-agent-refs/phases/phase-7-report.md +8 -1
  19. package/pipeline/multi-agent-refs/tracker-contract.md +46 -0
  20. package/pipeline/schemas/agent-state.schema.json +1 -1
  21. package/pipeline/schemas/token-budget.json +3 -3
  22. package/pipeline/scripts/phase-tracker.sh +245 -3
  23. package/pipeline/skills/.skill-manifest.json +8 -24
  24. package/pipeline/skills/.skills-index.json +2 -46
  25. package/pipeline/skills/shared/README.md +4 -8
  26. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +0 -8
  27. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +1 -1
  28. package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +1 -1
  29. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +12 -12
  30. package/pipeline/skills/skills-index.md +2 -6
  31. package/pipeline/commands/multi-agent/dev/SKILL.md +0 -17
  32. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +0 -23
  33. package/pipeline/commands/multi-agent/dev-local/SKILL.md +0 -17
  34. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +0 -21
  35. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +0 -19
  36. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +0 -25
  37. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +0 -19
  38. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +0 -23
package/CHANGELOG.md CHANGED
@@ -16,6 +16,42 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [16.23.0] - 2026-09-09
20
+
21
+ ### Fixed
22
+
23
+ - **The progress signal reached stdout and stopped there.** `phase-tracker.sh render` prints a bordered card at every phase boundary and it was always correct on disk. It was also invisible: the hosts collapse tool output into `Ran 8 shell commands`, so the card the contract calls "the last tool result in the chat" has not been in the chat for a while. A run showed its pickers, went quiet for forty minutes, and finished without ever drawing its shape.
24
+
25
+ Each host has a widget and now the tracker names its calls instead of hoping they are remembered. `phase-tracker.sh tiles` reads the phase set already registered by `add` and prints the registration calls for the host it runs from: a `TaskCreate` list on Claude Code, one `update_plan` payload on Codex, and on Copilot CLI - which has no task widget at all - the instruction to reprint the card inside the reply, plus the card. Every `update` then prints a `-- NEXT (required) --` block carrying that host's mirror call. Phase 0 Step −1 calls `tiles` right after the `add` loop; the Copilot instructions template does the same.
26
+
27
+ - **`phase-tracker.sh tokens` was documented, linted for, and never called.** Not once: no run in the local log tree had a `tokens_in`, `tokens_out` or `model` field on any phase, going back weeks. Every phase priced at `-`, every required completion line printed without its numbers, and every gate stayed green - because `smoke-tracker-tokens-invocation.sh` lints that the phase docs _mention_ the call, which is a claim about prose, not about a run. `payload-contracts.md` had already written that limitation down.
28
+
29
+ The gate moved to where the call would have landed: `update <N> completed` now **exits 3** for phases 1-4 when nothing was recorded, prints the two commands that clear it, and leaves the phase exactly as it was - record, then re-run, nothing lost. A phase that genuinely ran no LLM call completes with `--no-llm`, which is recorded so the report can tell "nothing" from "unknown". Phases 0 and 5-7 are orchestration and are not gated, and `skipped` never is, because Short skips 1 and 2 by design.
30
+
31
+ - **A full model id silently cost nothing.** `cost-table.json` is keyed by family (`opus`, `sonnet`, `gpt-5.6`) while the name an agent has at hand is `claude-opus-5`, and the mismatch priced the phase at `-` with no complaint anywhere. `model` now resolves the family and says so out loud, listing the priced names, when it cannot.
32
+
33
+ ### Added
34
+
35
+ - **`phase-tracker.sh report` - the closing report the run never printed.** Per phase: status, elapsed, tokens in/out, model and USD; then the run total, and an explicit list of any phase it could not price rather than a silent gap. Phase 7 prints it beside `render-work-summary.sh` (scope delivered, changed files with line counts, PR and commit links) so the end of a run answers both questions at once - what the pipeline spent, and what it changed. `format_span` gives the total an hours bucket; the per-phase card keeps minutes.
36
+
37
+ - **`smoke-tracker-widget.sh`** - 31 checks over the parts that were failing quietly: `tiles` emits the right call for each of the three hosts and starts at phase 0, the accounting gate fires and is recoverable, `--no-llm` is honoured and recorded, a full model id resolves to a priced family, every `update` carries its host's mirror call, the report prices what it can and names what it cannot, and the phase docs actually make these calls.
38
+
39
+ ### Removed
40
+
41
+ - **The four `dev` redirect stubs are gone from the command surface.** `16.0.0` turned pipeline depth into a question the run asks at Phase 0 Step 7.5 and left `multi-agent-dev`, `multi-agent-dev-autopilot`, `multi-agent-dev-local` and `multi-agent-dev-local-autopilot` behind as stubs that printed a redirect and nothing else. Each one said, in its own body, that it would be deleted in the next minor. Twenty-one minors later they were still in the picker: typing `/multi-agent:` still offered `dev`, still described it as removed, and the description was the only thing it did.
42
+
43
+ Deleted from every surface that carries them - Claude Code commands, the shared skill tree, the Copilot dash-command skills, the Codex refs mirror - along with the generated indexes (`.skills-index.json`, `skills-index.md`, `shared/README.md`, `.skill-manifest.json`, now 207 skills rather than 211) and the migration lines in `multi-agent-help` and the Copilot instructions template. `smoke-pipeline-surface.sh` check 5b, the gate that fails when a shipped file points a reader at a retired name, loses the four exemptions that existed only because the stubs and their generated index rows were allowed to name themselves; it is strictly tighter now.
44
+
45
+ Nothing about the surface changes for anyone who has already migrated: the break shipped in `16.0.0`, this is the tombstone coming down. `/multi-agent` and `/multi-agent:local` still ask Full or Short, and Short is still what the removed flag used to mean.
46
+
47
+ - **The four names themselves, everywhere they were still readable.** Deleting the directories left the names sitting in prose, and prose is what a user acts on. Gone from the dispatcher routing table (the `--dev` / `dev-*` redirect row), from `modes.md` and `phase-3-dev.md`, from `resume-local` and `local-autopilot` (which described themselves in terms of a command that no longer exists), and from the `ios-coding-standard` picker description, which had been telling readers to hand their remediation plan to `dev/dev-local` - a description is the one piece of a command everybody reads.
48
+
49
+ `examples/03-dev-mode-fast-path.md` was the worst of them and no gate could see it: `smoke-pipeline-surface.sh` check 2 scanned `pipeline/`, `docs/`, `install/` and `test/`, never `examples/`, so a whole worked example kept teaching `--dev` fourteen minors after the flag was removed - and contradicted itself while doing it, listing Phase 4 as skipped when Review has not been skippable since `14.0.0`. It is now `examples/03-short-run-fast-path.md`, written against the depth question, with the phase list and the transcript corrected. The `--dev` invocations in examples 01 and 02 and in the bug-report issue template went with it.
50
+
51
+ With nothing left to detect, the deprecation-era gates came out too: `smoke-pipeline-surface.sh` loses check 2 (the `--dev` flag scan), check 5b (the retired-name scan) and both planted probes, and `smoke-mode-dispatch-drift.sh` loses the two loops that named the four modes - the generator's unknown-mode guard already covers them. The gate is 203 lines down to 98 and every remaining check asserts something that can still be false.
52
+
53
+ Left alone on purpose: `CHANGELOG.md`, `CHANGELOG-archive.md`, `ROADMAP.md`, `docs/internal/` and the calibration log in `token-budget.json` are history, and rewriting them would misrepresent what shipped when. So is the runtime label `"dev"` that `state.onlyDevelop` still produces for the cost report and the component-dispatch payload: that is a run shape, not a command name, and renaming it would break the plugin contract and orphan every log already written.
54
+
19
55
  ## [16.21.0] - 2026-09-08
20
56
 
21
57
  ### Added
@@ -39,7 +75,6 @@ Internal file-layout changes that don't affect the slash-command surface are sti
39
75
  - **`multi-agent:setup` Step 8 and the picker-contract stop miscounting the hooks.** Both said two gates ship, and the setup step called the secret scan "the only deterministic gate that is OS-enforceable as a hook" - untrue since `agent-guard.sh` landed. Three ship now, and both places say so, along with which side of the run each one inspects.
40
76
  - **`smoke-gate-hooks.sh` covers all three gates** rather than two, and asserts the shipped defaults are safe (the read gate off, phase 3 exempt) - a read gate that shipped switched on would have been a breaking change disguised as a merge.
41
77
 
42
-
43
78
  ## [16.20.0] - 2026-09-07
44
79
 
45
80
  A comparison against `msitarzewski/agency-agents`, a 150k-star persona catalog, found nothing to copy wholesale and three things to fix: the personas were never registered, the circuit-breaker was prose, and a review finding had no identity from one round to the next. This release gives findings that identity and builds the loop controls, the handoff records and the evidence rules on top of it.
@@ -175,7 +210,6 @@ A review of the three repositories together (pipeline, toolkit MCP, plugin marke
175
210
 
176
211
  - **Usage reporting only ever reported runs that finished.** The reporter was called from Phase 7, and Phase 7 is the phase a run is least likely to reach: across the whole local log history, exactly one run got there. So the admin dashboard read zero of everything while the table held real rows, and the question it exists to answer - who is using this - had no answer. Phase 0's exit gate now reports the run as started, on the same upsert key, so Phase 7 replaces that row with the final state and nothing is double-counted. The read side of the same bug is fixed in the dashboard: its rollup folded only `terminal` rows, and an unfinished run was scored `fail`.
177
212
 
178
-
179
213
  ### Changed
180
214
 
181
215
  - **`/multi-agent:help` now covers the plugins and the toolkit MCP.** It named both in passing - a `refactor` line, a `sync` line, a `design-check` footnote - and never said what they are or how to call one. The catalog gained a Plugins & tools block in both languages: how a stack skill is invoked, which plugins are always on, and the MCP's 83 tools grouped by what they drive (device control, web, design comparison, store and accessibility audits), with the note that its registration survives uninstall. Paid for by compressing the `design-check` entry, which was restating its own contract in fifteen lines: 353 tokens reclaimed before a byte was added, so the file sits within its 8500 ceiling. The tool count that went in with it was wrong and is now absent: `rules/outside-the-pipeline.md` said 83 while the server serves 84, and the pipeline cannot check either number because it does not vendor the toolkit. Both places now say 80+, which stays true; the exact count lives in the toolkit's own README, where a gate boots the server and compares `tools/list` against it.
@@ -197,22 +231,21 @@ A review of the three repositories together (pipeline, toolkit MCP, plugin marke
197
231
 
198
232
  ### Added
199
233
 
200
- - **`/multi-agent:graph` - a deterministic code graph, built without an LLM.** Phase 1 re-scanned the repo with grep and read on every task and defaulted to the "very thorough" tier on feature work; Phase 7 wrote `architecture.md` from one task's window and let it age for 90 days. Both wanted the same answer: where does this live, and what depends on it. `graph-build.mjs` extracts symbols, imports and references by regex over comment-stripped source into `~/.claude/knowledge/<project>/code-graph.json`; `graph-query.mjs` runs a token-budgeted traversal, `graph-affected.mjs` walks it backwards for impact, `graph-report.mjs` renders `GRAPH_REPORT.md`, and `validate-code-graph.mjs` fails a build whose edges point at missing nodes. Zero runtime dependencies, zero API cost, read-only on the repo. Design reference: `Graphify-Labs/graphify`; the reasoning, the trade and the numbers are in `docs/adr/0010-own-code-graph.md`.
234
+ - **`/multi-agent:graph` - a deterministic code graph, built without an LLM.** Phase 1 re-scanned the repo with grep and read on every task and defaulted to the "very thorough" tier on feature work; Phase 7 wrote `architecture.md` from one task's window and let it age for 90 days. Both wanted the same answer: where does this live, and what depends on it. `graph-build.mjs` extracts symbols, imports and references by regex over comment-stripped source into `~/.claude/knowledge/<project>/code-graph.json`; `graph-query.mjs` runs a token-budgeted traversal, `graph-affected.mjs` walks it backwards for impact, `graph-report.mjs` renders `GRAPH_REPORT.md`, and `validate-code-graph.mjs` fails a build whose edges point at missing nodes. Zero runtime dependencies, zero API cost, read-only on the repo. Design reference: `Graphify-Labs/graphify`; the reasoning, the trade and the numbers are in `docs/adr/0010-own-code-graph.md`.
201
235
  - **Phase 1 Step 2.6 and Phase 7 graph refresh, behind `prefs.global.codeGraph.enabled` (default `false`).** When on, Phase 1 queries the graph and hands Explore a ranked starting set instead of a full scan, and Phase 7 rebuilds the graph after the branch changed code. Off, the pipeline behaves exactly as before. Measured on a 4,300-file Swift app at a fixed 30k retrieval budget: 80.4% coverage at 18,465 tokens against 66.0% at 24,555 for grep-and-read. The win is entirely in domain-word searches (63.3% vs 32.0% at under half the cost); when the task already names an exact type, `grep -lw` is still slightly better and slightly cheaper, and the command says so.
202
236
  - **`_path-match.mjs`.** The glob matcher `test-gap-scan.mjs` carried inline is now shared with the graph engine, so the two cannot drift apart on what a repo excludes.
203
237
  - **Code-graph rules for Android, Node and Python.** Four stacks now build: measured on a 4,229-file Kotlin app (25,234 nodes, 70,897 edges), this repo (126 sources) and a 43-file Python service, each validated and each hub list checked by hand rather than assumed correct. Two engine defects surfaced doing it, both of which iOS had been getting away with. Nesting was read from the pattern's match column, so `public final class Foo` counted as nested because the match starts at `class` seven columns in; it now reads the line's indentation. And the import pass read a fully stripped body, which blanks a JavaScript module specifier because the specifier IS a string literal - every import edge in that stack would have vanished with nothing to notice it. iOS was re-gated after both and scored identically (80.4% at 18,467 tokens).
204
238
  - **A nested declaration is a node but never a reference target.** Kotlin sealed cases and Python inner classes are named after the concept they model - `Icon`, `Color`, `Success`, `Disabled` - and each is declared exactly once, so the ambiguity rule never caught them. Every Kotlin file that merely mentioned Compose's `Color` gained an edge to one app's nested case: 4,072 false edges out of 74,969, and four of the top eight architectural hubs were wrong. They stay findable by name through their `defines` edge.
205
- - **`prefs.global.modelFallback.fableEnabled` (default `true`) - the fable rung as a switch, not only as a fallback.** Every other trigger in the fallback contract reacts to something going wrong; this one asserts up front that a rung is not in play, so there is no dispatch attempt and no error to recover from. `false` starts `ios/android/backend-architect`, `code-reviewer` and triage on `opus`. Claude Code only: Copilot CLI does not offer Fable 5, and on Codex CLI the `fable` rung means `gpt-5.6 @ xhigh`, a different vendor's model on a different account, which the switch deliberately leaves alone. Turning it off also collapses the Phase 4 Claude Code panel to two reviewers, because Reviewer 1 lands on the `opus` Reviewer 2 already holds and one model dispatched twice is not cross-model review; `consensus.reviewerCount` records `2`. Set `costBudget.priceAt` to `opus` alongside it, or the estimate keeps pricing at a rung nothing runs on and trips the ceiling early.
239
+ - **`prefs.global.modelFallback.fableEnabled` (default `true`) - the fable rung as a switch, not only as a fallback.** Every other trigger in the fallback contract reacts to something going wrong; this one asserts up front that a rung is not in play, so there is no dispatch attempt and no error to recover from. `false` starts `ios/android/backend-architect`, `code-reviewer` and triage on `opus`. Claude Code only: Copilot CLI does not offer Fable 5, and on Codex CLI the `fable` rung means `gpt-5.6 @ xhigh`, a different vendor's model on a different account, which the switch deliberately leaves alone. Turning it off also collapses the Phase 4 Claude Code panel to two reviewers, because Reviewer 1 lands on the `opus` Reviewer 2 already holds and one model dispatched twice is not cross-model review; `consensus.reviewerCount` records `2`. Set `costBudget.priceAt` to `opus` alongside it, or the estimate keeps pricing at a rung nothing runs on and trips the ceiling early.
206
240
 
207
241
  ### Fixed
208
242
 
209
243
  - **`derivedSkillSources` could not say which repo held the derived copies.** `localPath` reads as relative to the current repo, so both entries pointed at the UPSTREAM tree and stayed wrong through a plugin rename: the drift check was comparing a path that did not exist. Added `localRepo`, corrected both entries, and re-derived their skill lists from the tree instead of from memory.
210
244
  - **Two `excludePathGlobs` shapes were silently inert.** A pattern with a wildcard and no `**` fell through to a literal `endsWith` / `includes` test against the pattern text, which can never be true: nothing ends with the characters `*.d.ts`. So `*.d.ts` in the node rules and `*.egg-info/` in the python rules excluded nothing, and generated typings and build residue reached every consumer of those rules, `test-gap-scan.mjs` included. Both shapes now translate to an anchored regex, and eight glob cases are pinned by the smoke.
211
245
  - **`--all-data` left the knowledge base behind.** Full uninstall removed the log root and the per-repo memory root but never `~/.claude/knowledge/`, which holds `architecture.md`, `patterns.md`, `gotchas.md`, `decisions.md` and now `code-graph.json`. That is prose about a private codebase plus every symbol name in it, and it is the data a user most likely means by "remove everything". A default uninstall still keeps all of it, and memory written inside a repo (`$PROJECT_ROOT/.multi-agent/memory/`) is still never touched by either mode: it lives in the user's working tree and may be committed.
212
- - **`modelFallback.floorModel` could not be set.** The fallback contract has documented it since the two-step ladder landed, but it was absent from `prefs.schema.json`, whose `modelFallback` object forbids extra keys - so a prefs file that followed the documented example failed validation. Declared, defaulted to `haiku`, and added to the template.
246
+ - **`modelFallback.floorModel` could not be set.** The fallback contract has documented it since the two-step ladder landed, but it was absent from `prefs.schema.json`, whose `modelFallback` object forbids extra keys - so a prefs file that followed the documented example failed validation. Declared, defaulted to `haiku`, and added to the template.
213
247
  - **The consensus rule still claimed two reviewers on Claude Code.** v16.12.0 added the third reviewer and updated the matrix but not the `reviewerCount` line 300 lines below it. `smoke-cross-cli-behavior.sh` reads the matrix, so it never noticed. The line now says what it should have said all along: the count is the reviewers that actually dispatched, not the configured maximum.
214
248
 
215
-
216
249
  ## [16.12.0] - 2026-08-27
217
250
 
218
251
  ### Added
@@ -241,14 +274,13 @@ A review of the three repositories together (pipeline, toolkit MCP, plugin marke
241
274
  - The analysis ref ceiling moves 148000 -> 154000 across two raises in one session. Everything that could live outside the count already does: the scan in `figma-screenshot.sh`, the reviewer rubric in `analysis/review.md` which a subagent loads and the orchestrator does not, and all four new gates in `validate-analysis-doc.mjs`. The tree was measured for reclaimable duplication first and had none.
242
275
  - `smoke-validate-analysis-doc.sh` replaces four section-number pairing assertions with the `AS-NN` contract in both directions.
243
276
 
244
-
245
277
  ## [16.11.0] - 2026-08-27
246
278
 
247
279
  ### Changed
248
280
 
249
281
  - **Picker option labels now follow `outputLanguage`.** Only the `AskUserQuestion` `header` chip stays English, because it is capped at 12 characters and most Turkish equivalents overflow it. A Turkish run used to render a Turkish question with English buttons, which reads as a half-translated dialogue rather than a contract. `rules.md` carries the new per-field matrix; `picker-contract.md` carries what the caller now owns: branch on which option was picked, never on its rendered text, and pass `default` / `ASK_CHOICE_DEFAULT` as a 1-based index. The host's own **Other** row is injected in English and no run can localize it.
250
282
  - **`Frontend` is `Web` across the pipeline** - the platform picker, the stack command, the schemas, the conventions tables, the phase docs and `frontend-guide.md`, now `web-guide.md`. The published `ai-frontend-toolkit` plugin id, the `frontendRepos` / `frontendRoots` prefs keys and the `frontend` stack alias all keep working: `web` is canonical and `frontend` still resolves, so nothing written before this release stops loading. `webRepos` / `webRoots` are the new spelling; the old pair is documented as deprecated rather than removed.
251
- - **A repo-less analysis run keeps its channel split.** It used to flatten everything into one document because there were no repo conventions to project onto. But a phone screen and a browser screen carry different requirements whether or not a repository exists; only the *projection* half of the split needed conventions. Channels are now derived from the evidence (`intake.md` Step 3 carries the signal table) and one document is emitted per channel. `mobile` stays one channel rather than iOS plus Android, since without conventions nothing tells the two apart.
283
+ - **A repo-less analysis run keeps its channel split.** It used to flatten everything into one document because there were no repo conventions to project onto. But a phone screen and a browser screen carry different requirements whether or not a repository exists; only the _projection_ half of the split needed conventions. Channels are now derived from the evidence (`intake.md` Step 3 carries the signal table) and one document is emitted per channel. `mobile` stays one channel rather than iOS plus Android, since without conventions nothing tells the two apart.
252
284
 
253
285
  ### Fixed
254
286
 
@@ -270,7 +302,6 @@ A review of the three repositories together (pipeline, toolkit MCP, plugin marke
270
302
  - The analysis ref ceiling moves 145000 -> 148000. Unlike the v16.6.0 raise this one is not compensating for a measurement error: the tree gained three features and the number is right. About 1.4 kB of new prose was trimmed back before the ceiling was touched.
271
303
  - `smoke-language-matrix.sh` and `smoke-language-axis.sh` now assert the new rule in both directions - label follows `outputLanguage`, header stays English - and three more phrasings were added to the contradiction list after one slipped past the old set.
272
304
 
273
-
274
305
  ### Fixed
275
306
 
276
307
  - **The website sync committed under whatever identity the run carried, and the site silently stopped updating.** Step 4 ran a bare `git commit`, so the commit took the active account's address. The deploy platform builds only a commit whose author is a contributor on the project; any other author is accepted by the push and then never built - the deployment is created, reports `readyState: BLOCKED` (rendered by the CLI as `UNKNOWN` with a 0ms build), and the live site keeps serving the previous version. v16.4.0 and v16.5.0 were both pushed that way, neither was ever built, and both syncs reported the website as done.
@@ -285,7 +316,7 @@ A review of the three repositories together (pipeline, toolkit MCP, plugin marke
285
316
 
286
317
  ### Fixed
287
318
 
288
- - **`write-state.mjs` could delete a live lock and lose a writer's update.** The stale-lock reclaim deleted by path: between judging a lock stale and unlinking it, the holder can release and a third writer can acquire a fresh one, so the unlink removed a *live* lock and two writers then held it. It is the same failure the PID-window comment in that file already describes, at a different point in the acquire loop, and it survived because it only reproduces under load - `smoke-write-state.sh` failed inside a full gate run and passed 12/12 when run alone. Reclaim is now by identity: the inode and mtime judged stale must still be the file at that path, otherwise it belongs to somebody else and is left alone. Twelve runs under artificial load are clean, which is evidence and not proof - a race cannot be proven absent.
319
+ - **`write-state.mjs` could delete a live lock and lose a writer's update.** The stale-lock reclaim deleted by path: between judging a lock stale and unlinking it, the holder can release and a third writer can acquire a fresh one, so the unlink removed a _live_ lock and two writers then held it. It is the same failure the PID-window comment in that file already describes, at a different point in the acquire loop, and it survived because it only reproduces under load - `smoke-write-state.sh` failed inside a full gate run and passed 12/12 when run alone. Reclaim is now by identity: the inode and mtime judged stale must still be the file at that path, otherwise it belongs to somebody else and is left alone. Twelve runs under artificial load are clean, which is evidence and not proof - a race cannot be proven absent.
289
320
 
290
321
  - **Counts that had drifted from the tree.** `skills-index.md` and `skills/shared/README.md` still said 208 skills against 210 on disk; both are generated, so they were regenerated rather than hand-edited. `/multi-agent:update` quoted "245 scripts, 208 skills" for what an install lays down; it is 263 and 210. A comment in `smoke-command-inventory.sh` used "51 commands" as its example, which is the kind of number that goes stale the moment a command lands - it now says what it means without pinning a figure.
291
322
 
@@ -295,7 +326,7 @@ Three debts the last few releases kept naming, closed.
295
326
 
296
327
  ### Added
297
328
 
298
- - **`smoke-help-sync.sh` pairs each command's own description with the line `/multi-agent:help` shows for it.** Three releases running shipped the same defect: 16.7.1 (two commands absent from help entirely), 16.8.1 (the analysis entry describing behaviour from two releases earlier), 16.9.0 (a telemetry block still saying "optional, opt-in" after the default flipped). Every other gate was green through all three, because none of them asked whether the sentence a user reads still matches what the command does. The gate hashes both sides and fails when one moved without the other. Its limit is written into the file: it cannot tell whether either text is *correct* - a maintainer who edits both to say something equally wrong still passes. What it forces is that the pair gets looked at together, which is exactly what did not happen those three times.
329
+ - **`smoke-help-sync.sh` pairs each command's own description with the line `/multi-agent:help` shows for it.** Three releases running shipped the same defect: 16.7.1 (two commands absent from help entirely), 16.8.1 (the analysis entry describing behaviour from two releases earlier), 16.9.0 (a telemetry block still saying "optional, opt-in" after the default flipped). Every other gate was green through all three, because none of them asked whether the sentence a user reads still matches what the command does. The gate hashes both sides and fails when one moved without the other. Its limit is written into the file: it cannot tell whether either text is _correct_ - a maintainer who edits both to say something equally wrong still passes. What it forces is that the pair gets looked at together, which is exactly what did not happen those three times.
299
330
 
300
331
  ### Changed
301
332
 
@@ -310,7 +341,7 @@ Three debts the last few releases kept naming, closed.
310
341
 
311
342
  ### Added
312
343
 
313
- - **`/multi-agent:analysis` emits a run record.** It is a pipeline of its own now - its own phases, its own gates, its own report - but telemetry was only wired into the dev pipeline's Phase 7, so analysis runs were invisible. The panel showed dev work only, and the command people reach for *before* writing any code did not exist in the usage data. Phase 5 now writes its own `agent-state.json` through `write-state.mjs` (atomic, lock-guarded, so concurrent runs cannot wipe each other) and calls the reporter. `mode` carries the profile, which is what tells one analysis run from another: a `global` run and a `corporate` run are different work. `project` is `null` on a stack-optional run - there is no repository to name - and is digested before it leaves the machine either way. The reporter stays best-effort: a failure there never touches the run, whose summary is already printed.
344
+ - **`/multi-agent:analysis` emits a run record.** It is a pipeline of its own now - its own phases, its own gates, its own report - but telemetry was only wired into the dev pipeline's Phase 7, so analysis runs were invisible. The panel showed dev work only, and the command people reach for _before_ writing any code did not exist in the usage data. Phase 5 now writes its own `agent-state.json` through `write-state.mjs` (atomic, lock-guarded, so concurrent runs cannot wipe each other) and calls the reporter. `mode` carries the profile, which is what tells one analysis run from another: a `global` run and a `corporate` run are different work. `project` is `null` on a stack-optional run - there is no repository to name - and is digested before it leaves the machine either way. The reporter stays best-effort: a failure there never touches the run, whose summary is already printed.
314
345
 
315
346
  ### Fixed
316
347
 
@@ -470,7 +501,7 @@ Four ideas taken from github/spec-kit, obra/superpowers, karpathy/llm-council an
470
501
  platform's promised files enter one denominator instead of the first one
471
502
  standing in for the run.
472
503
  - **A plan with zero steps passed.** `todos: []` reported `0/0 steps accounted
473
- for` and exited 0, so a Phase 2 that produced nothing - or a state whose todos
504
+ for` and exited 0, so a Phase 2 that produced nothing - or a state whose todos
474
505
  were cleared - read as a fully delivered plan. An empty plan is now exit 2,
475
506
  the same as a missing one.
476
507
  - **A malformed reviewer dispatch vanished.** A reviewer whose `findings` came
@@ -714,10 +745,12 @@ Found by reviewing v16.0.0 after it shipped. Four defects, three of them mine fr
714
745
  ## [15.21.0] - 2026-08-23
715
746
 
716
747
  ### Added
748
+
717
749
  - **A confirmation pass that shows what was derived and asks only what was not.** Phase 2 Step 0.9 runs before planning: the platform set, the seven convention groups, the existing components, the localization keys and the analytics events all came out of the repos in Phase 1, so they are shown for confirmation rather than asked. Only Section 20 rows are asked, through the resolve engine, one row with at most three source-labeled candidates plus Defer - and never an invented one. It sits here rather than in Phase 4 because Phase 4 runs after development, where an answer arrives too late to change anything.
718
750
  - A corrected derived value rewrites its Pass B footnote as `^[user-override: resolved <date>]` (Locked 24), so where a value came from stays traceable even after a human overrode it.
719
751
 
720
752
  ### Changed
753
+
721
754
  - **The analysis stopped asking for the platform.** Every repo selected in Phase 0 already carries a stack tag from the project scan, so the platform set is the distinct tags of the selected repos. It is derived and shown in the breadcrumb; the question survives only as a fallback for an untaggable repo or a user who wants fewer platforms than the repos imply.
722
755
  - **The analysis stopped asking for repos.** That was the third place asking the same thing - Phase 0 Step 2 selects projects, `_dev-context.md` adds editable siblings, and analysis re-derived its own list on top. Repos now come from Phase 0, and the platform-to-repo mapping falls out of the stack tags.
723
756
  - `prefs.projects[<key>].frontendRepos[]` moved into `_dev-context.md`. Only the analysis command read it, which meant the pipeline's own dev-context picker could never offer a frontend repo - they are rarely submodules, so submodule detection never finds them.
@@ -726,45 +759,54 @@ Found by reviewing v16.0.0 after it shipped. Four defects, three of them mine fr
726
759
  ## [15.20.0] - 2026-08-23
727
760
 
728
761
  ### Added
762
+
729
763
  - **The analysis can read a Word file now.** Nothing in the pipeline handled `.docx`, which is the format most feature specs actually arrive in - a spec had to be pasted or re-typed into Confluence before the analysis could cite it. `pipeline/lib/fetch-document.sh` handles `.docx`, `.pdf`, `.md` and `.txt`, as a local path or a URL, and the extractor gained a `document` type that recognises both. It is distinct from `generic-doc`, which is an HTML page: this one is a file that must be converted before it can be read.
730
764
  - **No new dependency for the common case.** A `.docx` is a zip, so `word/document.xml` is parsed with the python3 standard library the lib layer already requires; the same code path runs on macOS, Linux and Windows. Requiring pandoc or libreoffice would have been the wrong dependency for a tool that must work everywhere. PDF has no stdlib path: `pdftotext` is used when present and its absence is a soft skip (exit 6, `converter-not-available`), which on Windows is the normal case, not an error. The converter binary is configurable via `DOCUMENT_PDFTOTEXT`, which is also how the smoke exercises the degrade path without breaking `PATH`.
731
765
  - **The analysis test plan is now the TDD RED input.** Phase 3's pre-flight read the concept table and even claimed test method names come from the analysis, while nothing read Section 15 - so RED invented its own tests and the carefully written test matrix never reached development. Step 5b loads it into `state.dev.testPlan[]` and RED writes those rows. Phase 4 step 1.45 then cross-checks every planned row against a real test: missing is `important`, present-but-asserting-something-else is `blocking`. That is what makes "analysis quality is output quality" a finding rather than a slogan.
732
766
 
733
767
  ### Fixed
768
+
734
769
  - The local-document matcher also matched the path inside a URL, so `https://x/api.pdf` produced a phantom local file `//x/api.pdf`. Matches overlapping a URL span are skipped; the smoke asserts a document URL yields exactly one entry.
735
770
 
736
771
  ## [15.19.0] - 2026-08-23
737
772
 
738
773
  ### Added
774
+
739
775
  - **Business rules are written in EARS now.** Section 4.4 rule statements were free prose, so Locked 31's "two readers must not disagree on pass/fail" was carried only by the acceptance criterion, not by the rule it came from. EARS (Easy Approach to Requirements Syntax, IEEE RE'09, five patterns) fixes the clause order and the keyword set, which is what removes the ambiguity. EARS states the rule, Gherkin still states how you check it - Locked 13 widens, it does not change.
740
776
  - **Section 15.7, manual test scenarios.** The pipeline already emitted this format as the Jira test-scenario comment from `resume-local`; the analysis had no place for it, so the document a QA engineer needs was the one thing the spec did not carry. Defined once, read by both. Every MT row carries its `BR-` id, and the validator enforces it: a scenario nobody can trace to a rule is a scenario nobody can tell is stale.
741
777
  - **Section 6.X is the whole variant axis, not the part this screen used.** New Phase 1b.2 walks each Code Connect-bound instance to its main component and reads `componentPropertyDefinitions`, so "used subset" finally has a set to be a subset of. Locked 29 promised this table for four releases with no step that could produce it. It has to happen in Phase 1: Locked 30 forbids Figma access afterwards, so an axis missed here is missed for the run.
742
778
  - **Three layer headings**: `Bölüm A - Analiz` (what to build), `B - Teknik Analiz` (what is true), `C - Geliştirme Analizi` (how to build it), with a boundary rule - remove the row, what becomes unclear - and the corollary that A carries no technology name and C no business rationale. Additive `#` headings only: section numbers are referenced in 172 places including two validators, so nothing renumbers, and Confluence gains a two-level table of contents for free.
743
779
 
744
780
  ### Fixed
781
+
745
782
  - **A layer heading could have satisfied a required section.** `validate-analysis-doc.mjs` matched required sections by substring over every `#{1,3}` heading, so a layer named `Bölüm B - Mimari ve Teknik` would have reported the architecture requirement as met with Section 13 absent. The matcher now only reads numbered section headings. Caught while adding the headings, not after shipping them.
746
783
 
747
784
  ### Changed
785
+
748
786
  - UI test scenarios default ON for `taskType == component` and for any task carrying a Figma reference. They were opt-in everywhere, so UI work started with UI tests switched off.
749
787
  - The doc validator gained the 15.7 and 6.X rules; `smoke-validate-analysis-doc.sh` grew from 10 to 13 assertions, one per new contract, each planted-and-proven rather than asserted.
750
788
 
751
789
  ## [15.18.0] - 2026-08-22
752
790
 
753
791
  ### Added
792
+
754
793
  - **`/multi-agent:analysis` is a pipeline mode now, not a command standing beside the pipeline.** It runs on the same 8-phase machinery - tracker tiles, `:resume`, the cost ledger, the channels report - with four phases reinterpreted the way `--dev` reinterprets Phase 3: Phase 3 and Phase 5 skip, Phase 4 reviews the document instead of a diff, Phase 6 publishes instead of committing. Phase set 0/1/2/4/6/7, generated by `gen-mode-dispatch.mjs --mode=analysis` and drift-checked like every other mode.
755
794
  - Phase 4 in analysis mode asks reviewers one question: could an implementer build the right thing from this document alone? A finding is anything that would force them to guess. The Section 20 walk runs there too, and a deferred row reports `review_blocking` rather than quietly staying open.
756
795
  - No `local` or `autopilot` variant, on purpose: worktree isolation buys nothing when no code is written, and the intake, the convention preview and the open-question walk are interactive by nature.
757
796
 
758
797
  ### Changed
798
+
759
799
  - The engine keeps moving out of commands and into `multi-agent-refs/analysis/`: `intake.md` and `resolve.md` join `locked/evidence/synthesis/render`. `analysis-resolve` and pipeline Phase 4 now mount the same resolution walk instead of describing it twice, and `analysis/SKILL.md` fits under the 6000 hard cap that applied once its grace entry was retired.
760
800
  - Phase-doc token budget: total 54900 -> 55250. Compression came first and three times, twice on the new prose and once on old: both mode branches shrank by pointing at the refs that hold the actual walks, and the front-matter parse contract stopped being spelled out identically in two pre-flights.
761
801
 
762
802
  ## [15.17.0] - 2026-08-22
763
803
 
764
804
  ### Fixed
805
+
765
806
  - **The full pipeline demanded a document nothing produced.** Phase 2 and Phase 3 pre-flights have BLOCKED on `analysis/<feature-slug>-<platform>.md` since v9.0.0, and Phase 1 never wrote it - its output was `analysis.json`, a different artefact. So a full run either aborted at Phase 2 telling the user to go run `/multi-agent:analysis` by hand, or the model quietly ignored its own BLOCKING contract. Phase 1 Step 4 now produces the document, and both pre-flights read `state.analysis.docStatus` instead of guessing from the filesystem: `produced` / `reused` continue, `not-applicable` is a legitimate skip (bugfix or chore with no Figma reference), and only a contract breach aborts. Neither phase sends the user to another command any more, because producing the file is Phase 1's job.
766
807
 
767
808
  ### Changed
809
+
768
810
  - **The analysis engine moved out of the command and into on-demand refs.** `multi-agent-refs/analysis/{locked,evidence,synthesis,render}.md` now carry the 31 Locked decisions, the evidence gathering, the two-pass synthesis and the render/publish flow. `/multi-agent:analysis` keeps them as its contract and Phase 1 loads the same four files, so there is one engine with two entry points rather than a command the pipeline cannot reach. Side effect worth naming: `analysis/SKILL.md` went from 18081 to 5974 tokens and its lint grace entry (ceiling 18500) is retired - the grace list only ratchets down.
769
811
  - Whether the document is produced is decided from signals Phase 0 already computed, so no new question: `feature` / `refactor` / `component` always, `bugfix` / `chore` only with a Figma reference. An existing document whose `evidence_digest` still matches is reused rather than regenerated (Locked 27).
770
812
  - `analysis-output.schema.json` gains `docStatus`, `docPath[]` and `openQuestions[]` - the fields the two pre-flights branch on.
@@ -773,18 +815,21 @@ Found by reviewing v16.0.0 after it shipped. Four defects, three of them mine fr
773
815
  ## [15.16.1] - 2026-08-22
774
816
 
775
817
  ### Fixed
818
+
776
819
  - **The humanizer punctuation check never ran on macOS.** Two SKILL files told the agent to verify the emitted document with `grep -P '[\x{2013}...]'`. BSD grep has no `-P`, so on the pipeline's primary platform the command errored out and "returns zero matches" was trivially true - the policy's only mechanical check was a shipped no-op. Both now call the deterministic Node validator that already implements the same policy (`validate-analysis-doc.mjs`, `validate-complaint-doc.mjs`), which behaves identically on macOS, Linux and Windows.
777
820
  - The repo already banned `grep -P` (`smoke-shell-portability.sh`, "no guarded form"), and that gate was green the whole time: it only scanned `*.sh`. A markdown instruction file is executed too - an agent reads `grep -P ...` and runs it verbatim - so the scanner now covers `commands/`, `multi-agent-refs/` and `skills/` markdown as well. It distinguishes a prohibition from an invocation, so a line that forbids the construct still passes. Adding it immediately surfaced a second instance in `complaint-analysis/SKILL.md` that no one had noticed, which is the argument for the gate.
778
821
 
779
822
  ## [15.16.0] - 2026-08-22
780
823
 
781
824
  ### Added
825
+
782
826
  - **More than one Firebase project per team.** `keychainMapping.firebase` held exactly one service-account key, which is wrong for the normal case: a legacy app next to its redesign, or staging next to production, each with its own key. A crash URL from the project you did not pick failed the `project_id` check and reported it as a configuration error, which it was, but not the one the message suggested. `global.firebase.accounts[]` maps `projectId` to a keychain key, `fetch-crashlytics.sh` reads the projectId out of the console URL and picks the matching account, and the single slot stays the fallback so a one-project setup needs no config at all. `/multi-agent:setup` now loops the Firebase pass (`Add another Firebase project? [y/N]`), reading `project_id` from each decoded JSON rather than asking for it.
783
827
  - A `project_id` mismatch now names the key it used and prints the `accounts[]` entry to add. With several projects in play, "project mismatch" alone does not say whether the URL is wrong or the mapping is incomplete.
784
828
 
785
829
  - **Jira project keys are discovered instead of recalled.** The token is saved and the host is known by the time setup asks for a project key, so it now asks Jira: one search for issues the person assigned or reported, most-recently-updated first, and the distinct project keys become a picker. A corporate instance has thousands of projects and a typed key is a typo that routes branches and new issues at the wrong board. The free-text prompt stays as the fallback for no-VPN and fresh accounts, and the per-repo mapping offers the discovered keys rather than asking for them again.
786
830
 
787
831
  ### Changed
832
+
788
833
  - `setup/SKILL.md` lost its third copy of the service-ID table and its second copy of the `keychainMapping` shape; both live in `refs/keychain.md`, which the flow already cites. What stayed is the column nothing else had: where to generate each token. The Firebase host-exemption note also stopped being stated twice, two paragraphs apart.
789
834
  - The App Store Connect tier reasoning moved into the ref that owns that flow, and `refs/keychain.md` gained the four App Store Connect standard key names so it is now the complete answer for every service setup points at.
790
835
  - `smoke-url-enrichment.sh` follows the type label to where it now lives and adds eight assertions: five for the multi-account contract (including that the single-slot fallback survives) and three for Jira discovery and the completeness of the key-name reference.
@@ -792,30 +837,36 @@ Found by reviewing v16.0.0 after it shipped. Four defects, three of them mine fr
792
837
  ## [15.15.0] - 2026-08-22
793
838
 
794
839
  ### Added
840
+
795
841
  - **Graylog has two instances now, because it always did.** Test and production are separate Graylog deployments, and a trx id minted by a tester does not exist in production - so searching production alone answered "no logs" for a complaint that was fully logged one host over, and that answer was indistinguishable from a genuine miss. `hosts.graylogTest` and the optional `keychainMapping.graylog_test` (which falls back to the production key, correct for shared-token deployments) make the second instance addressable. `fetch-graylog.sh --env auto` is the new default: production first, test when production returns nothing or is unreachable. `--env prod` / `--env test` pin one.
796
842
  - **The payload names the instance that answered** (`source.environment`, `source.searchedEnvironments[]`), and `/multi-agent:complaint-analysis` now has to cite it. A production complaint corroborated only by test logs is `insufficient-evidence`, not a confirmed `bff` fault, and the old payload gave the triage no way to tell those apart.
797
843
  - `/multi-agent:setup` asks for the two things the previous release added a consumer for but no collector: the Graylog test host plus its optional separate token, and `fortify.versionIds`. `versionIds` shipped in 15.14.0 as the only way to resolve an instance-id-only Fortify ticket, and nothing asked for it, so that path silently no-opped for everyone.
798
844
  - **`smoke-graylog-environments.sh`**, 15 assertions over the resolution rules that are easy to get subtly wrong: auto stops at production when production answers, falls back on empty AND on unreachable, degrades (never blocks) when both are down, treats a pinned environment with no host as exit 6 rather than silently searching the other one, and lets a 401 on one instance fall through instead of masking a working answer from the other.
799
845
 
800
846
  ### Fixed
847
+
801
848
  - A pinned `--env` with no configured host now exits 6 naming the exact pref, instead of falling back to whichever host happened to be set. Attaching test logs to a production complaint is worse than attaching none.
802
849
 
803
850
  ### Changed
851
+
804
852
  - `fetch-graylog.sh` resolves host and token per environment instead of once at the top, and reads prefs through one helper rather than three near-identical inline python blocks.
805
853
 
806
854
  ## [15.14.0] - 2026-08-22
807
855
 
808
856
  ### Added
857
+
809
858
  - **A release can now be required, not just available.** `/multi-agent:update` stays exactly what it was, and most releases change nothing: the run-start check keeps reading `dist-tags.latest` and keeps asking politely. What is new is a second tag. `npm dist-tag add <pkg>@<version> required` names the oldest version a user may run, and an install below that floor is not behind, it is wrong - it would produce work against a contract that no longer holds, which then has to be redone. Below the floor, Phase 0 Step 0.6 halts: it runs the update flow and stops, and the user re-issues the command on the new version. It does not continue on the freshly updated install, because this run's phase docs, refs and scripts were already loaded from the old one, and that is the drift the floor exists to prevent. Interactive and autopilot behave identically - there is nothing to decide.
810
859
  - **`require-supported-version.sh`** turns the signal into an exit code for shell callers: 0 = proceed, 3 = halt, with `force|<local>|<latest>|<required>` on stdout and a human block on stderr. It shares `update-check.sh`'s cache, so a second command inside the TTL window costs no network call.
811
860
 
812
861
  ### Fixed
862
+
813
863
  - **The Firebase Crashlytics fetcher called an endpoint that does not exist.** It built the app reference by hand as `<platform>:<bundle>` and asked for `/v1alpha/projects/<p>/apps/<ref>/issues/<issueId>`. The appId is opaque (`1:1234567890:ios:abcdef`) and cannot be derived from a bundle, and v1alpha has no get-issue-by-id route, so every fetch failed - and the failure was reported as `api-not-enabled`, which sent anyone debugging it to look at Google's API allowlist instead of at the URL. It now resolves the real appId through the Firebase Management API (`iosApps` / `androidApps`, matching `bundleId` / `packageName`), then reads `reports/topIssues` for the summary and metrics and `events?filter.issue.id=<id>` for the newest event. The payload gains what that event carries and the old shape could not: the full `stackTrace[]`, the breadcrumb and log timeline with screen names, session and occurrence counts, and the console URI. A multi-app project with no bundle match exits 3 as `app-not-found` rather than picking a neighbouring app.
814
- - **Fortify findings that arrive without a URL are no longer invisible.** A scanner-to-tracker bridge writes the instance id and the `file:line` into the ticket and never writes the SSC link - it knows the id, and the person reading the ticket never needed the URL. The extractor was URL-only, so those tickets produced an empty `contextLinks[]`, Phase 0 skipped the deep fetch, and Phase 4's Fortify gate reported `n/a` on a ticket that exists *because of* a security finding. It now also matches a labelled `Fortify Instance ID` / `issue instance id` / `fortify id` and emits a URL-less entry, the same shape graylog trx ids already used. `fetch-fortify.sh --instance-id <id>` resolves the project version by asking each id in `prefs.global.fortify.versionIds` in order. To stay off prose, the id must be at least 16 characters and contain a digit, and the bare label `instance id` is deliberately not matched.
864
+ - **Fortify findings that arrive without a URL are no longer invisible.** A scanner-to-tracker bridge writes the instance id and the `file:line` into the ticket and never writes the SSC link - it knows the id, and the person reading the ticket never needed the URL. The extractor was URL-only, so those tickets produced an empty `contextLinks[]`, Phase 0 skipped the deep fetch, and Phase 4's Fortify gate reported `n/a` on a ticket that exists _because of_ a security finding. It now also matches a labelled `Fortify Instance ID` / `issue instance id` / `fortify id` and emits a URL-less entry, the same shape graylog trx ids already used. `fetch-fortify.sh --instance-id <id>` resolves the project version by asking each id in `prefs.global.fortify.versionIds` in order. To stay off prose, the id must be at least 16 characters and contain a digit, and the bare label `instance id` is deliberately not matched.
815
865
  - **`prefs.global.fortify` did not exist.** Phase 4 Gate 5 documented `fortify.alwaysCheck` as its opt-in from the day it shipped, but `global` is closed to additional properties and the schema had no `fortify` object, so setting it failed validation - the gate could only ever run off a referenced URL. The object now exists with `alwaysCheck` and `versionIds`.
816
866
  - Fortify URLs of the form `/ssc/html/ssc/version/<id>` and `#/version/<id>` now yield a version id. Only the API shape `/projectVersions/<id>` was matched before, so the UI links people actually paste parsed to `projectId: null`.
817
867
 
818
868
  ### Changed
869
+
819
870
  - `update-check.sh` reads both tags in one call and now uses the abbreviated packument (60 kB instead of 250 kB for the same answer). Its own contract is unchanged and deliberately so: it still always exits 0, still says nothing when the registry is unreachable, and still emits `<local>|<latest>` for a plain update. The floor appends a third field, `force`, which a `cut -f1`/`-f2` reader ignores. The cache file grows a third field too; a two-field cache written by an older install still reads, and its missing floor means "unknown", never "none".
820
871
  - **Fail-open, on purpose.** Offline, a blocked registry, an undeterminable local version, a `required` tag published above `latest`, or no tag at all: every one of these exits 0. A version gate that bricks the pipeline on a flaky network is worse than the drift it guards against.
821
872
  - **Not opt-out.** `updateCheck.enabled: false` silences the advisory "update available" prompt, which is what it always meant; it does not lift a floor. The single override is the env var `MULTI_AGENT_ALLOW_OUTDATED=1`, which exits 0 with a warning and is logged in the run record, so a broken release cannot strand someone mid-incident. Exempt commands - `update`, `setup`, `uninstall`, `help`, `status`, `log`, `search`, `routines`, `forget`, `language` - are the remedy or cannot depend on a contract.
@@ -827,126 +878,151 @@ Found by reviewing v16.0.0 after it shipped. Four defects, three of them mine fr
827
878
  ## [15.13.0] - 2026-08-22
828
879
 
829
880
  ### Fixed
881
+
830
882
  - **Three settings that were declared and did nothing are now wired up.** `learningsLedger.maxBriefEntries` had a default of 20 while both phase docs hardcoded `--max 20`, so raising it changed nothing. `testGap.scanTree` and `testGap.promoteSeverity` were declared in the schema AND implemented as `--scan-tree` / `--severity-promote` in the scanner, with nothing in between reading the pref and passing the flag: the plumbing existed at both ends and the middle was missing. A user who set any of the three got the default back with no error and no warning, and the schema told them they had done it right.
831
883
 
832
884
  ### Added
885
+
833
886
  - **`smoke-prefs-consumed.sh`: every setting the schema declares must be read by something.** This class has now shipped five times - the two `contextOffload` fields fixed in 15.11.0 and the three above - which is enough to gate rather than to keep catching by hand. The check walks the nested schema and asserts each leaf key is mentioned as a whole word somewhere outside `schemas/`. Deliberately loose: it asks "did anyone wire this up", not "is the wiring correct", because a stricter rule would have to understand shell, JS and markdown, and a gate that is wrong is worse than one that is broad. `keychainMapping.*` is exempt with a reason - those are resolved dynamically, so the literal name never appears in code by design. Verified by planting a setting nothing reads and watching the gate go red.
834
887
 
835
888
  ### Changed
889
+
836
890
  - Phase-doc token budget: total 53950 -> 54050. The 94 tokens are the wiring itself, not prose - two `--max` substitutions and a three-line flag block. Compression came first and twice: the rationale moved into the new gate's header, where it is enforced rather than described, and a `--severity-promote` table row was dropped because the invocation above it now shows the flag and names the pref that triggers it.
837
891
 
838
892
  ## [15.12.2] - 2026-08-22
839
893
 
840
894
  ### Fixed
895
+
841
896
  - **`grep -P` is banned in shell, and the ban is enforced.** Unlike the other divergences the portability gate tracks, this one has no guarded form: BSD grep has no `-P` at all, so it exits 2 with "invalid option", and the `2>/dev/null` that nearly every check carries turns that error into an empty result, which reads as "found nothing". A check written that way passes on every input including the one it was meant to catch. Two gates in this repo did exactly that, were handed a file with the defect deliberately planted in it, and reported a clean tree. The scan covers every shell file under `scripts/` and `lib/`, not only the shipped ones, because the two that broke were smoke gates that never ship - and a gate that cannot fail is worse than no gate, since it is trusted. The check opens by proving its own detector fires on a planted invocation.
842
897
  - **Always-loaded context is back under budget with its designed headroom.** The fixed per-run load had reached 60000 of a 60000 ceiling, so the next edit to either tracked file would have broken the gate. The cause was the pattern the gate exists to catch: `core/multi-agent/SKILL.md` carried a full transcription of the Phase 0 contract, restating all eight steps that `refs/phases/phase-0-init.md` already defines, while its own text said the transcription "does not replace the contract, read the ref". Its citations into that ref had also drifted - `L221` now points at the token pre-check, not the branch picker it claimed. Every rule was verified present in the ref before cutting (per-repo branch picker, shared branch name with per-repo collision, per-repo identity, serial per-repo worktrees). What stays in the always-loaded file is the part that enforces rather than describes: the blocking exit gate and the credential-inventory rule. 2422 bytes reclaimed, load now 57576 of 60000. The ceiling was not raised.
843
898
 
844
899
  ## [15.12.1] - 2026-08-22
845
900
 
846
901
  ### Fixed
847
- - **The pre-push gate no longer runs inside the push.** Git opens the connection to the remote before the hook fires, so the six-minute chain 15.12.0 wired in idled that connection until the server dropped it: the first two attempts to push the rename died on a broken pipe with every gate green. The hook is now verify-only and instant - it checks a stamp keyed to the exact tree (HEAD plus a hash of the working tree) and refuses the push when there is none. `npm run gate` produces the stamp. Refusing is the point: no stamp means nothing has verified this tree, and the reason this file exists is that nothing else will.
848
-
849
902
 
903
+ - **The pre-push gate no longer runs inside the push.** Git opens the connection to the remote before the hook fires, so the six-minute chain 15.12.0 wired in idled that connection until the server dropped it: the first two attempts to push the rename died on a broken pipe with every gate green. The hook is now verify-only and instant - it checks a stamp keyed to the exact tree (HEAD plus a hash of the working tree) and refuses the push when there is none. `npm run gate` produces the stamp. Refusing is the point: no stamp means nothing has verified this tree, and the reason this file exists is that nothing else will.
850
904
 
851
905
  ## [15.12.0] - 2026-08-22
852
906
 
853
907
  ### Changed
908
+
854
909
  - **The companion MCP server is now `@mmerterden/multi-agent-toolkit-mcp` (v3.0.0), registered as `multi-agent-toolkit`.** The old name read as internal scaffolding; the server is standalone (three runtime dependencies, 83 tools, no coupling to any orchestrator) and the name now says which family it belongs to. The MCP tool namespace moves with it: every `mcp__dev-toolkit__*` reference across 24 files is now `mcp__multi-agent-toolkit__*`, because a host derives the tool prefix from the server name and the old prefix would have addressed a server that no longer answers.
855
910
  - **Existing registrations are migrated, not duplicated.** A host keys its registration by name, so a rename does not upgrade an entry in place: without a migration an install ends up with both `dev-toolkit` (pointing at the now-frozen 2.26.0) and `multi-agent-toolkit`, two servers advertising the same 83 tools with the host choosing between them. The installer removes the legacy entry before adding the new one, and `uninstall` clears both names - "removes the pipeline's footprint" has to mean the footprint it ever had.
856
911
  - Declared minimums move to `v3.0.0+`; 2.x only ever existed under the old package name.
857
912
 
858
913
  ### Migration
914
+
859
915
  - `@mmerterden/dev-toolkit-mcp` stays published at 2.26.0 and is deprecated with a pointer. Nothing is unpublished, so a pinned consumer keeps resolving. The toolkit also keeps `dev-toolkit-mcp` as a second `bin` alias.
860
916
  - A hand-registered client that the pipeline installer does not manage needs `<cli> mcp remove dev-toolkit` once.
861
917
 
862
918
  ### Fixed
919
+
863
920
  - **`pre-push-check.sh` ran three of the eleven gate steps.** It was missing both linters, three of the four evals, `validate-prefs` and `scorecard`, and drove the smoke suites through the bare `for f in smoke-*.sh` loop that `run-smokes.mjs` was written to replace - the loop that cannot tell a passing suite from one that exited 0 having asserted nothing. It now runs `npm test`, defined once in `package.json` so the hook cannot drift behind the chain, plus eslint and the personal-data scan. Because a full run takes about six minutes and git has already opened the connection to the remote by the time the hook fires, the verdict is cached against the exact tree that produced it (HEAD plus a hash of the working tree): re-pushing an unchanged tree is instant, one edited byte re-runs everything. Without that, the first push after the fix died on a broken pipe with every gate green. Verified by planting a defect that only the previously-missing steps catch: the old subset reported 434 unit tests passing and a clean tree; the new gate blocked the push. The hook's header also claimed the repo has no CI, which stopped being true some time ago.
864
921
 
865
922
  ## [15.11.0] - 2026-08-21
866
923
 
867
924
  ### Changed
925
+
868
926
  - **Nothing the pipeline posts carries a decorative glyph any more.** The work summary rendered task marks and a phase strip in checkmarks and hourglasses, the PR review emitter prefixed every finding with a coloured dot and signed it with a robot, and the GitHub issue Progress table was three traffic lights. All of it now reads in words: `[done]` / `[pending]` for tasks, `done · active · failed · skipped · pending` for the phase strip, `done` / `partial` / `pending` for the issue flags, and the severity is the bold label it always was. `channels/jira.md` had banned decorative glyphs in a comment body for releases while the renderers filled it with them; the rule and the emitters now agree, and `smoke-channel-glyphs.sh` holds them to it. The pipeline's own terminal output is deliberately out of scope: a `✓` in a console summary is a UI affordance, not a document somebody reads later.
869
927
 
870
928
  ### Fixed
929
+
871
930
  - **Jira no longer manufactures smileys the pipeline never typed.** Comments are posted as Jira wiki markup, and Jira's renderer converts `:)` `:D` `;)` and, far more easily hit in technical prose, `(x)` `(/)` `(!)` `(i)` `(y)` `(+)` `(on)` `(*)` into emoticon images at render time. Nothing escaped them. `channels/jira.md` now carries the escaping table and orders it after the markdown conversion and before the POST. This was never something the humanizer could fix: the text is legitimate, and `(x)` in a comparison table renders correctly on GitHub and Confluence - only the Jira conversion knows the target parser.
872
931
 
873
932
  ### Tests
933
+
874
934
  - `smoke-channel-glyphs.sh` (8 checks) and a glyph assertion in `smoke-work-summary.sh`. Both detect with node's `\p{Extended_Pictographic}` rather than `grep -P`: the first draft of the gate used a PCRE class, reported a clean tree with a checkmark deliberately planted in an emitter, and passed. Under `bash` on a stock macOS `grep` is BSD grep, which has no `-P` at all - it exits 2 with "invalid option", the `2>/dev/null` swallowed the message, and an empty result read as "no glyphs". The gate now opens by proving its own detector fires on a planted glyph before it trusts any verdict, and both gates were re-checked by planting one and watching them go red.
875
935
 
876
936
  ## [15.10.1] - 2026-08-21
877
937
 
878
938
  ### Fixed
939
+
879
940
  - **`uninstall --all-data` now removes the per-repo memory root.** `~/.claude/memory/multi-agent/` holds the learnings ledger and triage corpus: durable knowledge written in prose about the repos it was collected from, which on a corporate checkout is concrete information about the codebase. It survived even the full-cleanup mode, so "removes everything but your tokens" was not true in the one place a user is most likely to mean it. The default run still keeps it, alongside settings and logs, and both halves of that promise are now tested.
880
941
  - **Offloaded tool payloads are reclaimable.** v15.10.0 added `offload-ref.sh` without a way to clean up after it. In worktree modes the payloads die with the worktree, but the `--local` modes write into the real checkout, and because `.multi-agent/refs/` is gitignored the files never appear in `git status` and nothing reclaims them. New `gc-refs.sh`, dispatched as a third phase of `/multi-agent:garbage-collect`, sweeps them with the same contract as the /tmp sweeper: dry-run until `--yes`, a root guard that refuses `/` and `$HOME`, a grace window so a sweep cannot pull a ref out from under a running phase, and node-id matching so a file the user put in that directory survives. `--all` sweeps every checkout under `$HOME`.
881
942
 
882
943
  ## [15.10.0] - 2026-08-21
883
944
 
884
945
  ### Added
946
+
885
947
  - **Per-repo memory now recalls by relevance, not by recency.** Both memory stores ranked by something that was not relevance: `triage-memory.mjs query` scored a raw token overlap with no IDF, so a word present in every row ("view", "test", "error") counted as much as the one word that identified the bug, and `learnings-ledger.mjs brief` did not rank at all - it replayed the newest 20 entries. Past a few hundred rows both degrade the same way: the injected context stops being about the task in hand. New `pipeline/scripts/_retrieval.mjs` owns the arithmetic for both (the way `_cost.mjs` owns pricing): field-weighted BM25, exponential recency, and Reciprocal Rank Fusion with per-ranking damping so recency separates comparably relevant rows without ever promoting an unrelated one. The tokenizer indexes identifiers whole and split (`KeychainStore` is reachable from "keychain") and folds regular plurals, which is what lets a query phrased as prose reach a row that names a symbol. Zero dependencies: no embedding service, no vector store, no second model call. `prefs.global.memoryRecall.strategy: "legacy"` restores the old behaviour in one flag.
886
948
  - **`learnings-ledger.mjs profile` and drill-down pointers.** Durable knowledge is now two blocks instead of one, because relevance and prompt-cache reuse pull against each other. `profile` emits a task-INDEPENDENT `<repo-profile>` ordered by confidence, then kind, then statement - byte-stable across runs, so it belongs at the head of a phase prompt where an unchanged prefix is served from cache and grows into an asset as a repo is learned. `brief --task` emits `<task-relevant-memory>`, ranked against the task, and goes after the task text where a per-run difference costs nothing. Every rendered line ends with an `L:<id>` pointer instead of spelling out its evidence; `learnings-ledger.mjs show --id` and `triage-memory.mjs show --id` return the full row. `multi-agent-refs/prompt-assembly.md` carries the placement contract.
887
949
  - **`offload-ref.sh`: bulky tool payloads become a pointer plus a tail.** Phase 3 already teed its build output to a file, but nothing decided how much of that file reached the model, so in practice all of it did. The filter parks the full text at `.multi-agent/refs/<node_id>.md` (content-addressed, gitignored) and prints a `[[ref:<node_id>]]` stub with the last lines - where a failing build's error already is. The evidence gate keeps reading the whole log, so what counts as a verified pass is unchanged; only what reaches the prompt shrinks. Wired into Phase 3 builds and Phase 4 test output, opt-in via `prefs.global.contextOffload.enabled`, and a pass-through when off, so the pipe is always safe to write.
888
950
  - **Recall precision is measured.** Phase 1 and Phase 4 emit `memory.injected` / `memory.hit`, and `learning-curve.mjs` trends the ratio alongside the existing KPIs. Without the pair, a ranking change that injects the right rows and one that injects noise are indistinguishable from outside: both return five hits and exit 0. `smoke-learnings-ledger.sh` holds the emitter and the consumer to the same event names, so renaming one side fails a gate instead of silently emptying the column.
889
951
 
890
952
  ### Security
953
+
891
954
  - **Entry text cannot forge the boundary of the block it is injected into.** `from-triage` builds ledger statements out of a finding's own words, which are model output, so a statement is untrusted content placed inside a structure the reader parses. Statements are now stored as one line (the schema always said "in one line"; nothing enforced it), and the block delimiters are neutralised at render time in both the ledger blocks and `<repo-memory>`. Angle brackets that are not delimiters are untouched, so `Array<String>` still reads as itself.
892
955
 
893
956
  ### Changed
957
+
894
958
  - `memory-load.sh` takes optional task text and ranks the MEMORY.md pointers against it. The previous `head -30` was a truncation, not a summary - the thirty-first pointer was invisible however precisely it matched, so the block got less useful the longer a repo was worked on. With no task text the index order is unchanged.
895
959
  - Phase 4 prior-art lookup and the rejected-preference brief are both ranked against the findings under triage; a finding whose wording matches nothing now returns nothing instead of the three newest rows.
896
960
  - `prefs.global.contextOffload.minLines` and `tailLines` are read by `offload-ref.sh`. They shipped in the schema and were honoured by nothing, so a user who set `tailLines: 50` got 20 with no way to tell; config that does nothing documents a control that is not there. An explicit flag still beats the pref, and a non-numeric or zero value falls back to the shipped default.
897
961
  - Phase-doc token budget: total 53350 -> 53950. The new prose was compressed twice first (1168 tokens down to 580) by keeping the reasoning in `prompt-assembly.md` and the `offload-ref.sh` header, both outside the budget. Phase 3 and Phase 4 are left amber on their warn lines on purpose - that is the signal that those two docs are next for structural compression rather than another bump.
898
962
 
899
963
  ### Tests
964
+
900
965
  - `test/retrieval.test.mjs` (20 unit assertions on the ranking primitives), `pipeline/scripts/eval-recall.mjs` + `pipeline/eval/recall-cases.json` (8 end-to-end recall cases against a deliberately noisy corpus, each reporting what the pre-ranking scorer would have returned), `smoke-offload-ref.sh`, and new coverage in `smoke-learnings-ledger.sh` and `smoke-per-repo-memory.sh`.
901
966
 
902
967
  ## [15.9.1] - 2026-08-20
903
968
 
904
969
  ### Changed
970
+
905
971
  - **Telemetry logs the GitHub account name, never the git `identity.name`.** The reporter resolved the run's user to `identity.username || identity.name`, and since prefs identities carried no `username`, it fell back to `identity.name` - which can be a full corporate title/brand string, landing verbatim in the usage store. It now resolves to the identity's GitHub username, then the active `gh` account login resolved live, then null; the git `identity.name` is no longer a fallback. Self-registration (`/multi-agent:update` step 5b) resolves the same way.
906
972
 
907
973
  ## [15.9.0] - 2026-08-20
908
974
 
909
975
  ### Fixed
976
+
910
977
  - **Telemetry emitter and run scripts: 21 verified defects from a refactor bug hunt.** The emitter now reads `usageLog.optOut` as a hard block, refuses non-TLS endpoints so the write-only token never travels in cleartext, resolves the credential store and version marker across all host trees (Copilot/Codex-only installs), prices each phase at its own model rate instead of opus-for-all, keeps hyphenated MCP server names, drops plugins mapped to `false`, and gates before touching the keychain. `phase-tracker.sh` uses a per-process temp file so the fail-open lock cannot publish a torn state, honors `$TRACKER_FILE` on init, and builds OTEL attrs with jq. `build-stack-plugins.mjs` aborts on a flag given without a value and reports content-only changes in `--dry-run`; `localize-commands.mjs` is Windows- and CRLF-safe; `account-resolver.sh`, `channels-multi-repo.sh` and `figma-mcp-refresh.sh` gaps closed. Covered by `test/usage-report.test.mjs`.
911
978
 
912
979
  ### Changed
980
+
913
981
  - **`purge` and `uninstall` are no longer model-auto-invocable** (`disable-model-invocation: true`): the two irreversible, full-data-loss commands run only on an explicit user request.
914
982
  - **`humanizer` skill (v1.1.0):** a self-critique pass re-verifies the rewrite against the original (meaning preserved, nothing invented, patterns actually gone); trailing-participle and connective-padding patterns added.
915
983
  - Stale version tables refreshed: `SECURITY.md` supported-versions moves to the 15.x line; `ROADMAP.md` "Current Release" becomes a rolling "Recent Releases".
916
984
 
917
985
  ### Companion
986
+
918
987
  - **`dev-toolkit-mcp` v2.26.0** (shipped alongside): CallTool boundary now validates arguments against each tool's inputSchema (lenient-but-safe), closing the command-injection class where a string reached a numeric shell interpolation; every caller-derived path is single-quoted; a new gate backstops it. Backward-compatible, 83 tools unchanged.
919
988
 
920
989
  ## [15.8.1] - 2026-08-19
921
990
 
922
991
  ### Fixed
992
+
923
993
  - **Self-registration follows the endpoint redirect**: the default reporting host answers `/register` with a 308 to the canonical domain; the update step's curl now passes `-L`, so the token actually arrives instead of the redirect page. Without it, v15.8.0's self-registration silently reported "registration unreachable" on every machine.
924
994
 
925
995
  ## [15.8.0] - 2026-08-19
926
996
 
927
997
  ### Added
998
+
928
999
  - **Operational reporting self-registers on update**: when no ingest token is onboarded, `/multi-agent:update` requests a per-machine write-only token from the reporting endpoint's `/register` route, stores it only in the OS credential store, and enables `usageLog`. Registration failing (offline, endpoint down, ingest disabled) leaves reporting off with a one-line notice - never an error. Hard opt-out via `usageLog.optOut: true` blocks both the registration and the auto-enable; setup Step 2.7 (admin-issued token) still takes precedence. The emitter's payload is unchanged: coarse run metadata only, never prompts, code, diffs, or paths.
929
1000
 
930
1001
  ## [15.7.0] - 2026-08-19
931
1002
 
932
1003
  ### Added
1004
+
933
1005
  - **Setup walks missing credentials one by one**: Step 3 gained a strict sequential onboarding loop - fixed service order, one Token Save Flow prompt per service (token -> author -> host), per-service skips that never abort the loop, and `figma` / `figma_mcp` as distinct passes. The discovery summary can no longer end setup.
934
1006
  - **Per-repo Jira project keys in setup**: the Jira pass closes with an optional multi-select mapping (repo -> project key) written to `prefs.projects[{slug}].jiraProjectKeys`; per-repo keys resolve before `global.defaultJiraKey` everywhere a key is needed. Re-open with `/multi-agent:setup jira-keys`.
935
1007
  - **Figma MCP mode question**: the `figma_mcp` pass opens with Remote / Local. Remote generates the `figu_` OAuth token for the current user (Dynamic Client Registration + PKCE; `prefs.global.figmaMcp.remoteGeneratorScript` drives the flow when set); Local wires the PAT-based `@anthropic-ai/figma-mcp` server (`localGeneratorScript` supported). Prefs schema: new `global.figmaMcp` block plus `supabase_access` / `supabase_service_role` keychainMapping keys.
936
1008
 
937
1009
  ### Changed
1010
+
938
1011
  - **Help spells out the --dev pipeline**: the Pipeline section now carries the dev chain (Phases 1-2 skipped, Review never skipped, Opus dev). Stale notes claiming Test or Review are skipped in --dev were corrected in both languages.
939
1012
  - **Setup Step 5 (Repo Discovery) split to a reference**: the full contract moved to `multi-agent-refs/setup/repo-discovery.md`; the SKILL keeps the opt-in prompt and a summary.
940
1013
 
941
1014
  ### Fixed
1015
+
942
1016
  - **build-stack-plugins.mjs dead import**: unused `APPLE_ONLY` import removed (superseded by the `STACK_ONLY` routing table).
943
1017
 
944
1018
  ## [15.6.1] - 2026-08-19
945
1019
 
946
1020
  ### Changed
1021
+
947
1022
  - **`/multi-agent:update` installs from npm, not from a git clone**: the registry is the single update channel - latest published release resolved with a direct registry read (never `npm view`'s cache), downloaded via `npm pack` with the registry pinned, installed with `install.js --all`, changes rendered from the packaged CHANGELOG, smokes run from the tarball. A pipeline repo clone is now purely a maintainer workspace (synced by `/multi-agent:sync`); consumers need no git access at all, so collaborator grants on the private repo can stay read-only or be dropped.
948
1023
 
949
1024
  ### Fixed
1025
+
950
1026
  - **`node --test` runs stop pinging the live dashboard**: the tracker-entities suite calls `phase-tracker.sh init` outside run-smokes' `MULTI_AGENT_SMOKE` guard, so every test run left a phantom "probe" row on the timeline. The suite now sets the flag itself, and `usage-report.mjs` refuses to emit under it as the last line of defense for any caller.
951
1027
  - **Usage report reads the tracker as it is actually written**: `tracker-state.json` stores `phases` as an array, but the reporter iterated it with `Object.entries`, so dashboard phase ids were array indexes - every phase after a skipped one was mislabeled (Commit id "6" reported as Faz 5). Failed-phase error tags carried the same wrong ids.
952
1028
  - **Run duration and terminal timestamp resolve from the tracker**: nothing stamps `state.finishedAt`, so every run reported `du=null` and a terminal emit was stamped with the reporter's wall clock (wrong for backfills). Both now fall back to the tracker's phase span (earliest start to latest completion).
@@ -955,28 +1031,34 @@ Found by reviewing v16.0.0 after it shipped. Four defects, three of them mine fr
955
1031
  ## [15.6.0] - 2026-08-18
956
1032
 
957
1033
  ### Fixed
1034
+
958
1035
  - **The heart goes platform-blind** (platform-dynamic audit, 15 findings): Phase 3's RED run, target resolution and build verification become `case "$STACK"` arms (gradle/pytest/npm beside xcodebuild, with the Gradle build-lock decision stated); Phase 4 Gate 1 matches its own stack-generic Gates 2-3; Phase 2 dispatches the platform's architect agent; Phase 5's device-check table gains the Android MCP tools; the figma-config schema stops claiming SwiftUI as universal; wiki scope gains a `platform` value.
959
1036
 
960
1037
  ### Added
1038
+
961
1039
  - **Per-stack routing hatches**: `STACK_ONLY` lists in `_stack-routing.mjs` so a stack-only skill (ktlint, hilt-di, ...) routes with one list entry instead of a regex widening - and `--check-routing` now FAILS on unrouted skills. `lint-skills` accepts `platform: backend|frontend`.
962
1040
 
963
1041
  ## [15.5.0] - 2026-08-18
964
1042
 
965
1043
  ### Added
1044
+
966
1045
  - **`sharedUtilities` census bucket** - the bind-don't-rebuild inventory: formatter families, validation rule types + per-module facades and design-token namespaces living outside screen slices, counted with samples; Phase 3 treats a non-empty bucket as binding.
967
1046
 
968
1047
  ### Fixed
1048
+
969
1049
  - **Smoke runs never touch the live dashboard** - run-smokes exports `MULTI_AGENT_SMOKE=1` and phase-tracker's live ping returns under it (test gates were leaving phantom "running" rows on the timeline).
970
1050
  - **Routing resolves the multi-agent-plugins toolkit first** - the public toolkit family is the pipeline's standard companion; a corporate variant is the fallback, not the default.
971
1051
 
972
1052
  ## [15.4.0] - 2026-08-18
973
1053
 
974
1054
  ### Added
1055
+
975
1056
  - **Per-project `testPolicy`** (`tdd` | `tests-after` | `none`): Phase 0 resolves it after project selection for every input type, asking once via a native picker when absent (autopilot defaults to `tdd` and notes it). Phase 3 gates the TDD cycle on it; Phase 5 skips the gap scan under `none` and runs only pre-existing targets. Prefs schema + template carry the field.
976
1057
  - **Ordered stack-toolkit routing candidates**: the ios/android rows resolve the corporate `-engineering-` variant before its public derivation, and a full probe miss records every candidate tried. The conventions census learns a `CrossDomains` root; screen-creation tasks bind the routed toolkit's `create-screen` workflow.
977
1058
  - **ios-module-structure 0.3.0 / ios-coding-standard 1.2.0** (authoring source): the settled architecture becomes the standard - UnitDialect slot, unit vocabulary roles, STRUCT-18..21 with checker support, UNIT-01..03 / SAFE-03 / MOD-08; the pre-conversion spellings are named residue.
978
1059
 
979
1060
  ### Fixed
1061
+
980
1062
  - The test-policy step resolves for Jira-ID and URL inputs, not only free-text; STRUCT-18 reports an unbound contracts root as disabled coverage instead of passing silently.
981
1063
 
982
1064
  ## [15.3.2] - 2026-08-17
@@ -1054,7 +1136,7 @@ places at once.
1054
1136
  delivered a different stack than the repo asked for. Repo settings are now read
1055
1137
  first, user-global second, `--platform` last.
1056
1138
  - **Copilot's skipped-stack prune could delete user-authored skills.** The prune
1057
- was derived from catalog *names*, so a user's own `~/.copilot/skills/<name>/`
1139
+ was derived from catalog _names_, so a user's own `~/.copilot/skills/<name>/`
1058
1140
  sharing a catalog name was removed with no prompt. It is now scoped to the
1059
1141
  previous delivery manifest (or, pre-manifest, to dirs byte-identical to the
1060
1142
  catalog) - the same contract `uninstall` already used.
@@ -1155,7 +1237,6 @@ places at once.
1155
1237
  were already at (or 193 bytes past) their limit; the always-on surface itself
1156
1238
  shrank with the local copy gone.
1157
1239
 
1158
-
1159
1240
  ### Added
1160
1241
 
1161
1242
  - **`/multi-agent:complaint-analysis`** — customer-complaint / customer-reported-error
@@ -1207,7 +1288,7 @@ places at once.
1207
1288
  `gzip` preserves the source file's mtime by default, so rotating log
1208
1289
  content already older than `KEEP_DAYS` produced a `.gz` that inherited
1209
1290
  that old mtime - and the very next line's retention sweep (`find -mtime
1210
- +$KEEP_DAYS -delete`) deleted it in the same run, before it was ever read.
1291
+ +$KEEP_DAYS -delete`) deleted it in the same run, before it was ever read.
1211
1292
  The archive's mtime is now reset to rotation time, so retention counts
1212
1293
  from when it was archived, not from the age of the content inside it.
1213
1294
  - The mode banner claimed pickers stay English when they didn't - language +
@@ -1228,26 +1309,26 @@ places at once.
1228
1309
 
1229
1310
  - **`testflight-validation` merged into a new `/multi-agent:store-ready`, and the
1230
1311
  Android side brought to parity.** The iOS archive audit existed twice with
1231
- identical arguments - `ios_app_store_audit({archive_path, rules: "all"})` in the
1312
+ identical arguments - `ios_app_store_audit({archive_path, rules: "all"})` in the
1232
1313
  command's Gate 1 and again inside `sim-test.md`'s `store-ready` scenario. Two
1233
1314
  copies of one call is how the second door grew with no Gate 2, no Gate 3 and no
1234
1315
  Android equivalent. There is now one implementation.
1235
1316
  Gates are symmetric per platform, because the failure modes are: a malformed
1236
1317
  package, a package the store itself refuses, and a policy a human enforces.
1237
1318
 
1238
- | Gate | iOS | Android |
1239
- |---|---|---|
1240
- | 1 Static | `ios_app_store_audit`, 18 rules | `android_apk_audit` + `google-play-compliance`, 21 rules |
1241
- | 2 Authoritative | `altool --validate-app` | `SKIPPED` |
1242
- | 3 Policy | `app-store-review` vs source | `play-store-review` vs source |
1319
+ | Gate | iOS | Android |
1320
+ | --------------- | ------------------------------- | -------------------------------------------------------- |
1321
+ | 1 Static | `ios_app_store_audit`, 18 rules | `android_apk_audit` + `google-play-compliance`, 21 rules |
1322
+ | 2 Authoritative | `altool --validate-app` | `SKIPPED` |
1323
+ | 3 Policy | `app-store-review` vs source | `play-store-review` vs source |
1243
1324
 
1244
1325
  Gate 2's asymmetry is reported as an asymmetry rather than smoothed over: Play's
1245
1326
  authoritative check exists only server-side, through a Publishing API draft edit,
1246
1327
  and the pipeline ships no client for it. An Android run therefore clears at most
1247
1328
  2 of 3 and never prints `passed`. `bundletool validate` is Gate 1 and is not
1248
1329
  promoted to fill the hole.
1249
- The running-app sweep became Step A rather than a separate errand - a build can
1250
- be structurally perfect and still ship an unreadable screen - and it degrades to
1330
+ The running-app sweep became Step A rather than a separate errand - a build can
1331
+ be structurally perfect and still ship an unreadable screen - and it degrades to
1251
1332
  `SKIPPED (no booted device)` instead of halting the package validation.
1252
1333
 
1253
1334
  Nothing was removed. `testflight-validation` is a thin iOS-pinned alias (removing
@@ -1278,7 +1359,7 @@ places at once.
1278
1359
  - **Copilot's `purge` could not see a single worktree.** It discovered them with
1279
1360
  `find {repo}/.worktrees/ -name "agent-state.json"`, and no worktree carries that
1280
1361
  file: Phase 0 writes state to `$HOME/.claude/logs/multi-agent/{project}/{task-id}/`.
1281
- Verified on a repo with two live task worktrees - the marker scan returns zero
1362
+ Verified on a repo with two live task worktrees - the marker scan returns zero
1282
1363
  while `purge.sh`'s own directory enumeration finds both. So the skill reported
1283
1364
  "nothing to purge" as success with real worktrees on disk. The Claude Code command
1284
1365
  had already been fixed by delegating to `purge.sh`; the Copilot mirror never
@@ -1301,7 +1382,7 @@ places at once.
1301
1382
  `.worktrees/PROJ-*/agent-log.md` and `agent-state.json`; Phase 0 has written both
1302
1383
  to `$HOME/.claude/logs/multi-agent/{project}/{task-id}/` since the layout moved.
1303
1384
  Against the current tree the scan matches zero files, so the op reported "logs
1304
- cleared, deleted 0" as success while every real log stayed on disk - worse than a
1385
+ cleared, deleted 0" as success while every real log stayed on disk - worse than a
1305
1386
  missing command, because it reads as done. There was also no `clear-logs` command
1306
1387
  directory, so the `/multi-agent:clear-logs` entry in the Copilot help pointed at a
1307
1388
  slash command that was never installed.
@@ -1315,7 +1396,7 @@ places at once.
1315
1396
  reaching either fell through to the general sweep and was reported as the scenario
1316
1397
  asked for. Neither can be built symmetrically today: biometric has `ios_biometric`
1317
1398
  and no Android counterpart, launch timing has `android_launch_time` and no iOS
1318
- counterpart, and the file auto-detects platform - so each would work on one
1399
+ counterpart, and the file auto-detects platform - so each would work on one
1319
1400
  platform and silently do nothing on the other. Both rows are withdrawn with the
1320
1401
  reason recorded in place, rather than shipped as half-features or left advertised.
1321
1402
  Advertised scenarios and implemented sections now match one to one.
@@ -1412,7 +1493,7 @@ own toolkit says apply, and clean up after itself.
1412
1493
  so `bugfix` / `feature` / `refactor` / `chore` had no skill dispatch at all:
1413
1494
  whichever skills the host surfaced by description match were the ones used, and
1414
1495
  nothing recorded or required any of them. That was the dev-side half of the gap
1415
- v14.0.0 closed on the review side - review asked "was this built to the rules it
1496
+ v14.0.0 closed on the review side - review asked "was this built to the rules it
1416
1497
  was supposed to follow" while nobody had chosen any rules.
1417
1498
 
1418
1499
  The routing table is NOT copied into this repo. A second copy would drift the
@@ -1422,7 +1503,7 @@ own toolkit says apply, and clean up after itself.
1422
1503
  6-row table). Routed skills land in `telemetry.skillCalls[]` with
1423
1504
  `routedBy: "<toolkit>:index@<version>"`, so Phase 4 conformance can hold the run to
1424
1505
  what its own toolkit chose. An absent or disabled toolkit is a recorded no-op, not
1425
- a halt - a backend repo has no toolkit and must still run.
1506
+ a halt - a backend repo has no toolkit and must still run.
1426
1507
  Contract: `refs/features/stack-skill-routing.md`.
1427
1508
 
1428
1509
  - **Phase 6 removes a task's worktree once its PR is open** (`worktree-finalize.sh`,
@@ -1430,7 +1511,7 @@ own toolkit says apply, and clean up after itself.
1430
1511
  salvages `agent-state.json`, `phase-tracker.json`, `triage-output.json`,
1431
1512
  `.pipeline/`, `.build.log`, `.test.log` and `.review-diff.txt` into the log dir
1432
1513
  first, because Phase 7's triage-memory ingest, the learnings-ledger distill,
1433
- `render-work-summary.sh`, `:resume`, `:status` and `:log` all read them - and the
1514
+ `render-work-summary.sh`, `:resume`, `:status` and `:log` all read them - and the
1434
1515
  first three are `[ -f ]`-guarded, so a removal without salvage would have degraded
1435
1516
  silently rather than failing.
1436
1517
 
@@ -1470,7 +1551,7 @@ own toolkit says apply, and clean up after itself.
1470
1551
  `f.file` from the diff-risk report, which declares and emits `path`, so each
1471
1552
  finding carried `file: undefined` and read "Test file 'undefined' shrinks". That
1472
1553
  is useless to the developer and rejected by `reviewer-output.schema.json`, which
1473
- requires `file` with `minLength: 1` - on the one gate that deliberately has no
1554
+ requires `file` with `minLength: 1` - on the one gate that deliberately has no
1474
1555
  opt-out. It stayed invisible because the smoke's own fixtures used `"file"`, the
1475
1556
  key the bug read: the fixture matched the bug instead of the schema. Fixtures
1476
1557
  corrected, and a new assertion checks the finding names a real path (verified by
@@ -1482,14 +1563,13 @@ own toolkit says apply, and clean up after itself.
1482
1563
  elsewhere in those files' prose. Added check 4b: every name in an inventory must be
1483
1564
  a command that exists in the tree (verified against a planted `ghostcmd`).
1484
1565
 
1485
-
1486
1566
  ## [14.0.0] - 2026-07-29
1487
1567
 
1488
1568
  The `--dev` family reviews its own work, and review now checks the code against the
1489
1569
  criteria it was supposed to be built to instead of general good taste.
1490
1570
 
1491
1571
  Major because a command was renamed: `/multi-agent:finish` is now `/multi-agent:ship`.
1492
- No alias is kept - `installCommands()` replaces the command tree wholesale, so a clean
1572
+ No alias is kept - `installCommands()` replaces the command tree wholesale, so a clean
1493
1573
  cut leaves nothing half-migrated.
1494
1574
 
1495
1575
  ### Added
@@ -1502,7 +1582,7 @@ cut leaves nothing half-migrated.
1502
1582
  without them, while review judges work that now exists and has no substitute. Accepted
1503
1583
  blocking findings return to Phase 3 under the existing 3-iteration hard kill; the
1504
1584
  autopilot variants auto-fix without prompting and halt on the existing rework-storm
1505
- circuit breaker rather than committing. No new machinery for either - both were already
1585
+ circuit breaker rather than committing. No new machinery for either - both were already
1506
1586
  built and became reachable the moment Phase 4 entered the phase set.
1507
1587
  - **Phase 4 Step 1.78, criteria resolution** (`skill-conformance.mjs`, zero LLM). Resolves
1508
1588
  which rule registries apply to this diff, scoped to its languages and paths, and writes
@@ -1517,7 +1597,7 @@ cut leaves nothing half-migrated.
1517
1597
  - **Every registry declares its own `scope`** (`languages`, `paths`, `excludePaths`,
1518
1598
  `notCovered`), and per-rule `scope` narrows it further. Measured before this landed: only
1519
1599
  2 of the iOS registry's 99 rules carried any applicability field, and the rest wrote their
1520
- scope as English prose inside `mechanism` - so an Objective-C or UIKit diff would have
1600
+ scope as English prose inside `mechanism` - so an Objective-C or UIKit diff would have
1521
1601
  collected all 99 SwiftUI-shaped rules, manufacturing findings and burying the real ones.
1522
1602
  - `references/rules.yml` for `apple-archive-compliance` (18 rules) and
1523
1603
  `google-play-compliance` (21), converted from their existing SKILL.md tables with IDs and
@@ -1526,7 +1606,7 @@ cut leaves nothing half-migrated.
1526
1606
  - **Exception-marker audit**: expired, reason-less, expiry-less, or unknown-ID
1527
1607
  `standard:exception(...)` markers become rule-ID-bearing findings. The marker template is
1528
1608
  read from the registry, never hardcoded, so a registry with different comment syntax works.
1529
- - `state.telemetry.skillCalls[]` - Phase 3 records each skill, plugin skill and guide it
1609
+ - `state.telemetry.skillCalls[]` - Phase 3 records each skill, plugin skill and guide it
1530
1610
  consulted, with the files it applied them to.
1531
1611
  - `prefs.global.skillConformance.blockOnCoverageGap` (default **false**) and
1532
1612
  `prefs.global.ship.autoFix`, the latter referenced by the tail command's spec since it
@@ -1546,13 +1626,13 @@ cut leaves nothing half-migrated.
1546
1626
  - **Reviewers cite rule IDs.** `reviewer-output.schema.json` 1.0.0 -> 1.1.0 adds `ruleId`,
1547
1627
  `criteriaSource` and the per-rule `conformance[]` array; `triage-output.schema.json`
1548
1628
  3.2.0 -> 3.3.0 carries both through triage. `code-reviewer.md` gains a `${CRITERIA}`
1549
- injection slot - the phase doc had claimed for some time that "skills are injected into
1629
+ injection slot - the phase doc had claimed for some time that "skills are injected into
1550
1630
  reviewer prompt context" while the agent definition had no slot for them, which is exactly
1551
1631
  why review could not cite a rule.
1552
1632
  - `validate-reviewer.mjs` enforces the checklist with `--criteria`: a selected ID with no
1553
1633
  verdict, a verdict for an ID that was never selected, a `conformant` row with no file
1554
1634
  evidence, and a `violated` row with no matching finding all fail. Without this the field
1555
- would be decoration - the validator is hand-written and does not apply
1635
+ would be decoration - the validator is hand-written and does not apply
1556
1636
  `additionalProperties`, so any array at all would have passed.
1557
1637
  - `${CRITERIA}` lives in the shared cacheable prefix, identical for every reviewer.
1558
1638
  Subsetting it per reviewer would invalidate the prefix for the whole panel and re-bill the
@@ -1574,7 +1654,7 @@ cut leaves nothing half-migrated.
1574
1654
 
1575
1655
  - **`ios-coding-standard` in the repo was a version behind the installed copy** (95 rules
1576
1656
  v1.0.0 vs 99 rules v1.1.0). Because `installSkills()` copies the repo over the
1577
- destination, the next `/multi-agent:update` would have downgraded a user's registry -
1657
+ destination, the next `/multi-agent:update` would have downgraded a user's registry -
1578
1658
  harmless while nothing read it, a correctness regression the moment Phase 4 blocks on rule
1579
1659
  IDs. Resynced to 99 rules.
1580
1660
  - `modules/*.yml` overlays and `references/EXAMPLES.md` are deliberately NOT shipped: they
@@ -1600,8 +1680,8 @@ gates are named per item.
1600
1680
 
1601
1681
  - **The Tier 2 Figma PAT could not be found on any migrated install.** `migrate-prefs.mjs`
1602
1682
  consolidated `keychainMapping.figma_pat` into `.figma` and deleted the old key, but the
1603
- setup wizard kept *writing* `figma_pat` (so the next migration deleted the mapping it
1604
- had just created), both Tier 2 fetchers kept *reading* it, and the failure text told the
1683
+ setup wizard kept _writing_ `figma_pat` (so the next migration deleted the mapping it
1684
+ had just created), both Tier 2 fetchers kept _reading_ it, and the failure text told the
1605
1685
  user to map the one key guaranteed not to survive. Tier 2 reported `missing-token` while
1606
1686
  a valid PAT sat under the new name. Claude Code's MCP tier masked it; Copilot and Codex,
1607
1687
  which serve no Figma MCP tools at all, fell straight to Tier 3. The lookup now lives once
@@ -1660,7 +1740,7 @@ gates are named per item.
1660
1740
  written. Copilot now rewrites the trees it owns, and installs `rules/` as well, closing
1661
1741
  the same silent-nothing fallback Codex's installer already documented.
1662
1742
  - **`phase-tracker.sh` never reclaimed a stale lock on Linux.** It tried `stat -f %m`
1663
- before `stat -c %Y`, and on GNU coreutils `stat -f` is a *valid* flag (`--file-system`,
1743
+ before `stat -c %Y`, and on GNU coreutils `stat -f` is a _valid_ flag (`--file-system`,
1664
1744
  where `%m` is the mount point): it succeeds, returns something like `/`, the `||` never
1665
1745
  fires, and the age arithmetic runs on a path. Every tracker call then spun the full ~5s
1666
1746
  bound and fell open with a warning. Every other `stat` call site in the repo already had
@@ -1675,7 +1755,7 @@ gates are named per item.
1675
1755
  the previous generation. Now `claude-opus-5` and `claude-sonnet-5`, with the rung/ID split
1676
1756
  stated in the fallback contract. Gate: a generation guard in `smoke-model-fallback.sh`.
1677
1757
  - **`rules/pipeline-output-formatting.md` did not exist.** Two shipped skills named it as
1678
- the PR-body contract to follow. The install-layout smoke had been *reporting* paths that
1758
+ the PR-body contract to follow. The install-layout smoke had been _reporting_ paths that
1679
1759
  exist on no host as a note rather than failing on them, which is how it survived; that
1680
1760
  claim is now strict.
1681
1761
  - **A pre-migration preferences file failed validation before the migration could fix it.**
@@ -1732,7 +1812,7 @@ gates are named per item.
1732
1812
 
1733
1813
  - **Every fetcher died on a Copilot-only or Codex-only install.** Eleven runtime scripts
1734
1814
  loaded `credential-store-resolver.sh` as `. "$HOME/.claude/lib/..." || . <next> || {
1735
- error }`, all of them under `set -e`. Sourcing a file that does not exist aborts the
1815
+ error }`, all of them under `set -e`. Sourcing a file that does not exist aborts the
1736
1816
  shell outright - `||` included - so on a host without `~/.claude/lib` the chain reached
1737
1817
  neither its later candidates nor its error branch: bare exit 1, no message. Reordering
1738
1818
  does not help, because whichever candidate is absent aborts at that point. All eleven
@@ -1823,7 +1903,7 @@ gates are named per item.
1823
1903
  `--dev` skips the LLM phases, not Phase 0's pickers.
1824
1904
  - **An unreachable external source is announced instead of absorbed.** Exit code `3`
1825
1905
  used to mean "mark it failed and continue", so an expired token and a VPN-off remote
1826
- both reached the analysis phase as *no data*, indistinguishable from a ticket that
1906
+ both reached the analysis phase as _no data_, indistinguishable from a ticket that
1827
1907
  referenced nothing. The run then planned from a partial picture and reported success.
1828
1908
  `external-context-injection.md` now classifies the stderr and surfaces a decision -
1829
1909
  refresh the credential, connect the VPN and retry, supply a current URL, or continue
@@ -1845,7 +1925,7 @@ gates are named per item.
1845
1925
  `api.figma.com` returned 403 for a healthy token and would have sent the user to
1846
1926
  regenerate something that worked. It is now reported as not-probeable, with liveness
1847
1927
  left to `figma-mcp-refresh.sh`, which owns the grant.
1848
- - A failed `curl` wrote `000` through `write-out` *and* triggered the `|| echo "000"`
1928
+ - A failed `curl` wrote `000` through `write-out` _and_ triggered the `|| echo "000"`
1849
1929
  fallback, producing `000000` and a bogus verdict for what was simply a closed VPN.
1850
1930
  Any non-three-digit status now reads as `probe-error` rather than being dressed up as
1851
1931
  a service verdict.
@@ -1882,7 +1962,7 @@ gates are named per item.
1882
1962
  ### Fixed
1883
1963
 
1884
1964
  - **`build-stack-plugins.mjs` never propagated content edits.** It copied a skill only
1885
- when the skill *set* changed, so editing a routed skill in `shared/external` reached
1965
+ when the skill _set_ changed, so editing a routed skill in `shared/external` reached
1886
1966
  no plugin while the generator reported "all plugins up to date" - which read as
1887
1967
  confirmation. This falsified the single-authoring-source guarantee the whole
1888
1968
  `shared/external` design rests on. Found by converting banned punctuation in
@@ -1914,7 +1994,7 @@ The iOS coding standard reaches all three hosts, and works outside the pipeline.
1914
1994
  The 95-rule registry lived as a pipeline-local routine. Two consequences, both
1915
1995
  measured: only Claude Code could reach it, because `local-only` commands are
1916
1996
  deliberately never synced (verified absent on Copilot CLI and Codex CLI), and it
1917
- applied only when the routine was invoked explicitly - nothing pulled the rules in
1997
+ applied only when the routine was invoked explicitly - nothing pulled the rules in
1918
1998
  while Swift was being written. The plugin's own `reference/code-style` skill is 154
1919
1999
  lines of prose with zero rule IDs, so the registry was not reachable that way either.
1920
2000
 
@@ -1958,7 +2038,6 @@ lines of prose with zero rule IDs, so the registry was not reachable that way ei
1958
2038
  predates this change and `tools/validate.py` warns on it; routing `knowledge/` behind
1959
2039
  the plugin's `index` skill is the fix and is separate work.
1960
2040
 
1961
-
1962
2041
  ## [13.1.0] - 2026-07-27
1963
2042
 
1964
2043
  Six gates, from one branch that spent half its commits on rework.
@@ -1968,7 +2047,7 @@ chain, measured from the run's own artefacts: Phase 0 reported `completed` havin
1968
2047
  written only `tracker-state.json`, so `agent-state.json` and its `taskType` never
1969
2048
  existed, so Phase 3's component dispatch could not fire. The stack plugin already
1970
2049
  ships `create-screen`, `figma-validate` (7 criteria including design-token compliance
1971
- and Code Connect strategy) and `figma-review` (14-item checklist) - none of them ran.
2050
+ and Code Connect strategy) and `figma-review` (14-item checklist) - none of them ran.
1972
2051
  Padding came out 16 where the frame said `Spacing/12`. Three of six commits were fixes,
1973
2052
  the last a full sheet rebuild.
1974
2053
 
@@ -1985,7 +2064,7 @@ invokes a skill that was already written but never called.
1985
2064
  gate is testable without reproducing a run.
1986
2065
  - **Phase 4 Step 2.8, visual conformance gate.** Runs `figma-review` and
1987
2066
  `/multi-agent:design-check` for component/screen work, with the coverage gate, and
1988
- asserts Code Connect was **published** rather than merely written - a
2067
+ asserts Code Connect was **published** rather than merely written - a
1989
2068
  `*.figma.swift` on disk with "Not published" in Figma is a binding that exists for
1990
2069
  nobody. `design-check` had been a command with no phase invoking it, so the only
1991
2070
  thing standing between a build and visual drift was the user opening the app.
@@ -2000,14 +2079,14 @@ invokes a skill that was already written but never called.
2000
2079
 
2001
2080
  - **Component dispatch halts instead of degrading.** The contract used to send an
2002
2081
  incomplete-state component task down the generic TDD path while the next sentence
2003
- said "never silently skip the Figma work" - taking the generic path *is* skipping
2082
+ said "never silently skip the Figma work" - taking the generic path _is_ skipping
2004
2083
  it. That wording authorised the exact failure above.
2005
2084
  - **Dispatch routes on scope, not just platform.** A screen and a component are
2006
2085
  different jobs and the plugin ships a skill for each; routing a screen to the
2007
2086
  component skill is why one run produced entities and a mapper but left the screen
2008
2087
  half-wired. `figma-validate` now runs before the create skill.
2009
2088
  - **Phase 1 captures spacing by token name, per atom.** Phase 3 is forbidden from
2010
- calling Figma, so a pixel number - or a missing entry - is unrecoverable later. A
2089
+ calling Figma, so a pixel number - or a missing entry - is unrecoverable later. A
2011
2090
  UI frame with no spacing entries is a capture failure, not an empty frame.
2012
2091
  - **Phase 3 records that generated trees are not editable.** A mock fixture went into
2013
2092
  the generated tree; the fix moved it to the custom tree and registered the scenario
@@ -2017,7 +2096,6 @@ invokes a skill that was already written but never called.
2017
2096
  Planning by design, so there is no phase that turns a document into a plan. The doc
2018
2097
  becomes context for one pass and work lands in whatever order it was read.
2019
2098
 
2020
-
2021
2099
  ## [13.0.0] - 2026-07-27
2022
2100
 
2023
2101
  > **Why major and not minor.** The additions here are additive, but two defaults
@@ -2027,7 +2105,6 @@ invokes a skill that was already written but never called.
2027
2105
  > comparing Codex on skill directories would read a correct install as drift).
2028
2106
  > Per the versioning policy that is a changed default, not a new option.
2029
2107
 
2030
-
2031
2108
  Codex CLI becomes a third supported host, and a new pre-submission validation command.
2032
2109
 
2033
2110
  ### Added
@@ -2038,19 +2115,19 @@ Codex CLI becomes a third supported host, and a new pre-submission validation co
2038
2115
  with per-agent model and reasoning effort, hooks, MCP and a plugin marketplace,
2039
2116
  so it earns the same treatment as the other two rather than an adapter.
2040
2117
  `install/codex.mjs` writes `~/.codex/{skills/multi-agent,multi-agent-refs,
2041
- agents/*.toml,prompts/multi-agent.md,scripts,lib,schemas,rules}` plus a managed
2118
+ agents/*.toml,prompts/multi-agent.md,scripts,lib,schemas,rules}` plus a managed
2042
2119
  span in `~/.codex/AGENTS.md`, and registers the dev-toolkit MCP server through
2043
2120
  `codex mcp add` rather than hand-merging TOML that Codex owns.
2044
2121
  - **`/multi-agent:testflight-validation`** (command 43). Three gates, each seeing
2045
2122
  what the others structurally cannot: the static 18-rule archive audit, Apple's
2046
2123
  own `altool --validate-app`, and a Review-Guidelines check against repo evidence.
2047
- ITMS codes are mapped to the rule each implies. It validates only - never
2048
- `--upload-app` - so a validation run cannot ship a build by accident.
2124
+ ITMS codes are mapped to the rule each implies. It validates only - never
2125
+ `--upload-app` - so a validation run cannot ship a build by accident.
2049
2126
  - **App Store Connect credentials in `/multi-agent:setup`**, inside the Step 1
2050
2127
  discovery / Step 2 mapping / Step 3b onboarding flow alongside Jira and
2051
2128
  Bitbucket, not as a late add-on: a user who already has one in their keychain
2052
2129
  gets it mapped automatically. Tier 1 is an API key, tier 2 an Apple ID plus an
2053
- app-specific password - which matters because creating an API key needs an
2130
+ app-specific password - which matters because creating an API key needs an
2054
2131
  Admin or App Manager role many developers on a corporate team do not have.
2055
2132
 
2056
2133
  ### Changed
@@ -2066,8 +2143,8 @@ Codex CLI becomes a third supported host, and a new pre-submission validation co
2066
2143
  gpt-5.6 @ medium, triage at max) with two measured constraints written into the
2067
2144
  contract: a `spawn_agent` that sets `model` without `fork_turns: "none"`
2068
2145
  **silently inherits the parent model**, collapsing the panel onto one
2069
- perspective; and 4 concurrency slots *including the orchestrator* make three
2070
- reviewers the ceiling. Single-vendor caveat recorded - consensus among three
2146
+ perspective; and 4 concurrency slots _including the orchestrator_ make three
2147
+ reviewers the ceiling. Single-vendor caveat recorded - consensus among three
2071
2148
  OpenAI models is weaker evidence than the same consensus on a two-vendor host.
2072
2149
  - `tracker-contract.md` gains the `codex` visual channel: the native `update_plan`
2073
2150
  tool, with its no-parallel-call and no-plan-mode caveats.
@@ -2087,7 +2164,7 @@ Codex CLI becomes a third supported host, and a new pre-submission validation co
2087
2164
  - **The Phase 0 branch-collision probe read a failed probe as "no collision".**
2088
2165
  With `2>/dev/null` and an empty-output test, an auth or network failure was
2089
2166
  indistinguishable from "the ref does not exist", so the run created a branch
2090
- that already existed on the remote - surfacing as a rejected push at Phase 6,
2167
+ that already existed on the remote - surfacing as a rejected push at Phase 6,
2091
2168
  far from its cause. Exit codes are now distinguished (0 exists, 2 free, anything
2092
2169
  else unknown-and-recorded).
2093
2170
  - **`/multi-agent:update` deleted the Codex prompt on every run**, pruning
@@ -2100,14 +2177,14 @@ Codex CLI becomes a third supported host, and a new pre-submission validation co
2100
2177
  (added in v12.11.0) holds the banned characters as its own assertion list and
2101
2178
  was never allowlisted.
2102
2179
  - **Half-English pickers on Turkish runs: six shipped files contradicted the
2103
- canonical language matrix.** `rules.md` is unambiguous - `AskUserQuestion`
2180
+ canonical language matrix.** `rules.md` is unambiguous - `AskUserQuestion`
2104
2181
  `question` and `options[].description` render in `outputLanguage`, only `label`,
2105
2182
  `header` and host chrome are pinned to English. But `/multi-agent:setup` Step 0
2106
2183
  claimed `promptLanguage` governs "interactive pickers and prompts ... Picker UI is
2107
2184
  always English", `/multi-agent:language` claimed "confirmation prompts ... are
2108
2185
  authored in English. Only the assistant's free-form replies follow
2109
2186
  `outputLanguage`", and four more files said variations of the same. Those are the
2110
- two commands a user goes to *configure* this, so the model followed whichever
2187
+ two commands a user goes to _configure_ this, so the model followed whichever
2111
2188
  canonical-looking doc it read first and gate questions came out English. All six
2112
2189
  corrected to describe `promptLanguage`'s real scope: the button and chip chrome,
2113
2190
  never the question a user reads.
@@ -2117,7 +2194,7 @@ Codex CLI becomes a third supported host, and a new pre-submission validation co
2117
2194
  - New `smoke-language-matrix.sh`: asserts `rules.md` still pins the per-field
2118
2195
  matrix, then greps every shipped command / skill / ref for the seven phrasings
2119
2196
  that actually shipped and contradicted it. Two authoritative docs giving opposite
2120
- answers is a spec with two answers, not a wording nit - the gate found a sixth
2197
+ answers is a spec with two answers, not a wording nit - the gate found a sixth
2121
2198
  violation (`_input-parser.md`) that the manual sweep had missed.
2122
2199
 
2123
2200
  ### Gates
@@ -2129,7 +2206,7 @@ Codex CLI becomes a third supported host, and a new pre-submission validation co
2129
2206
  - `smoke-install-layout.sh` extended to three targets, including a check that every
2130
2207
  concrete `$HOME/.codex` reference **resolves on disk**. The earlier "no `.claude`
2131
2208
  references remain" check was blind to a wrongly-rewritten path, which is how
2132
- `commands/multi-agent.md` became `multi-agent-refs/commands.md` - well-formed,
2209
+ `commands/multi-agent.md` became `multi-agent-refs/commands.md` - well-formed,
2133
2210
  pointing at nothing. A miss is classified by whether the Claude counterpart
2134
2211
  exists, so broken-by-rewrite fails and broken-upstream is only reported.
2135
2212
  - New `test/codex-install.test.mjs` (24 assertions) locks the path-rewrite map,
@@ -2162,11 +2239,11 @@ bug that produced it.
2162
2239
 
2163
2240
  **All three encodings**, because a title can arrive in any of them:
2164
2241
 
2165
- | | ampersand | less-than | em-dash |
2166
- |---|---|---|---|
2167
- | named | `&amp;` | `&lt;` | `&mdash;` |
2168
- | decimal | `&#38;` | `&#60;` | `&#8212;` |
2169
- | hex | `&#x26;` | `&#x3C;` | `&#x2014;` |
2242
+ | | ampersand | less-than | em-dash |
2243
+ | ------- | --------- | --------- | ---------- |
2244
+ | named | `&amp;` | `&lt;` | `&mdash;` |
2245
+ | decimal | `&#38;` | `&#60;` | `&#8212;` |
2246
+ | hex | `&#x26;` | `&#x3C;` | `&#x2014;` |
2170
2247
 
2171
2248
  The first version of this fix handled only the named column, which is the same bug
2172
2249
  fixed for one spelling out of three.
@@ -2384,7 +2461,7 @@ entry, so the count would have under-reported by 100+ files.
2384
2461
  ### Four gates that were tested, green, and never ran
2385
2462
 
2386
2463
  Every gate in this repo checks its own behaviour. Nothing checked that a gate is
2387
- *reachable*. Four were not - each with a passing unit test and a passing smoke,
2464
+ _reachable_. Four were not - each with a passing unit test and a passing smoke,
2388
2465
  and no reference from `pipeline/commands/`, `multi-agent-refs/`, `agents/`,
2389
2466
  `skills/` or `install/`:
2390
2467
 
@@ -2545,7 +2622,7 @@ while fixing them.
2545
2622
  Two threads. The design-check command gains a scenario inventory, a coverage
2546
2623
  gate and MCP-currency gating, and the refactor/sync pair learns to research,
2547
2624
  audit and ship the companion dev-toolkit MCP server. Alongside that, a defect
2548
- sweep found eight gates and features that exited 0 while doing nothing - six
2625
+ sweep found eight gates and features that exited 0 while doing nothing - six
2549
2626
  of them guarded by a smoke test whose fixture had the wrong shape, so the suite
2550
2627
  stayed green the whole time.
2551
2628
 
@@ -2577,14 +2654,14 @@ Requires `@mmerterden/dev-toolkit-mcp` >= v2.9.0 for the App Store audit path.
2577
2654
  `smoke-personal-data.sh --root`, keeping one pattern list.
2578
2655
  - **shellcheck moves from `error` to `warning` severity.** `error` returns zero
2579
2656
  findings across all 175 scripts, so the gate could not catch this repo's
2580
- actual defect class - SC2046 is a warning. Noisy codes are excluded with a
2657
+ actual defect class - SC2046 is a warning. Noisy codes are excluded with a
2581
2658
  per-code rationale. Four real findings surfaced and are fixed, including two
2582
2659
  unguarded `cd` calls in `smoke-shadow-git.sh`, one preceding a relative
2583
2660
  `rm -rf`.
2584
2661
  - **`credential-store.sh` resolves logical keys through
2585
2662
  `prefs.global.keychainMapping`.** `get github` searched the backend for a
2586
2663
  credential literally named "github" and returned empty with exit 1 -
2587
- indistinguishable from "no such credential" - because the entry is named by
2664
+ indistinguishable from "no such credential" - because the entry is named by
2588
2665
  the mapping. Applied in `get`/`set`/`delete`, falling back to the logical key
2589
2666
  when no mapping exists.
2590
2667
 
@@ -2794,6 +2871,7 @@ all project specifics live in per-project `figma-config` / prefs.
2794
2871
  ## [12.2.0] - 2026-07-21
2795
2872
 
2796
2873
  ### Added
2874
+
2797
2875
  - **Graylog log-fetch integration** — when a task's ticket/issue carries a transaction id (`trx`/`trxId`/`transaction id`) and/or a conversation id (`conversationId`/`convId`/`X-conversationId`), the pipeline fetches the matching Graylog log messages and injects them as advisory diagnostic context into Phase 1 analysis. Mirrors the Crashlytics analysis-context adapter (advisory only, no Phase-4 gate).
2798
2876
  - New `lib/fetch-graylog.sh` adapter (mirrors the Fortify hosted-service shape): host from `prefs.global.hosts.graylog` (+ `GRAYLOG_HOST_OVERRIDE`), token via `prefs.global.keychainMapping.graylog` → `${USER}_Graylog_Access_Token` through the credential-store resolver. Graylog PAT auth is HTTP Basic `<token>:token` delivered only via a `curl -K` process-substitution config (never on argv). Universal/relative search by full-text OR of the ids, range + limit configurable via env.
2799
2877
  - **Non-blocking by design:** any network/VPN failure degrades to an empty normalized result and exit 0 — a log fetch never blocks a run. Exit codes: 0 ok/degraded, 2 missing-token, 3 genuine auth rejection, 4 usage, 6 host-not-configured.
@@ -2802,6 +2880,7 @@ all project specifics live in per-project `figma-config` / prefs.
2802
2880
  ## [12.1.1] - 2026-07-21
2803
2881
 
2804
2882
  ### Changed
2883
+
2805
2884
  - **Channel outputs no longer carry decorative/emotive emoji or smileys.** Jira/issue comments, Confluence & Wiki pages, and PR bodies are plain technical prose. Added an explicit no-emoji hard-rule to each channel template (`multi-agent-refs/channels/{jira,issue-comment,confluence,wiki}.md`) and an emoji-stripping pattern to the humanizer (`shared/external/humanizer`). Functional status/severity marks a fixed template defines (`✅/⏳` phase ticks, `🔴/🟡` severity labels) are unaffected.
2806
2885
 
2807
2886
  ---
@@ -2809,20 +2888,25 @@ all project specifics live in per-project `figma-config` / prefs.
2809
2888
  ## [12.1.0] - 2026-07-21
2810
2889
 
2811
2890
  ### Security
2891
+
2812
2892
  - **`agent-guard.sh` is now wired by the default installer** (was opt-in via the setup template). A plain `install` now OS-enforces the two load-bearing git gates on every `Bash` tool call: no AI/assistant attribution in commit messages, and no force-push to a protected branch (`main`/`master`/`develop`).
2813
2893
  - **Force-push protection now fails CLOSED.** A detected force-push that can't be tokenized, or a bare force-push while the current branch can't be confirmed non-protected, is blocked rather than allowed. (The general guard stays fail-open; only the data-loss path is fail-closed.)
2814
2894
  - **Multi-repo clone hardening.** `cmd_prepare` disables git's `ext::`/`fd::` remote helpers (`protocol.ext.allow=never`, `protocol.fd.allow=never`) and ends options with `--`, closing an arbitrary-command-execution / option-injection vector on repo URLs sourced from `.gitmodules` suggestions. Local/file/https/ssh clones are unaffected.
2815
2895
 
2816
2896
  ### Changed
2897
+
2817
2898
  - **Node floor raised to 20** (`.nvmrc`, `engines.node` → `>=20.0.0`). Node 18 was EOL and untested by CI; the matrix already covers 20/22.
2818
2899
 
2819
2900
  ### Refactored
2901
+
2820
2902
  - **Token-cost pricing is now a single source.** The per-Mtok cost formula, previously copy-pasted across ~6 bash/mjs scripts, lives in `cost-lib.sh` (bash) + `_cost.mjs` (Node), both reading the shared `cost-table.json`. Zero runtime dependencies preserved.
2821
2903
 
2822
2904
  ### Added
2905
+
2823
2906
  - **`smoke-source-parity.sh`** — guards feature-set drift between the two source trees (`commands/multi-agent/<name>` vs `skills/shared/core/multi-agent-<name>`); fails if a feature exists in one tree but not the other.
2824
2907
 
2825
2908
  ### Docs
2909
+
2826
2910
  - Refreshed post-v12 doc drift: `SECURITY.md` + `ROADMAP.md` support/version markers → 12.x; `docs/features.md` smoke-suite count `10` → `100+`; ADR index now lists 0008 and 0008 carries a v10.7.0 amendment (the `_adapters.mjs` module was removed); `CONTRIBUTING.md` coverage claim clarified (local-only) and the stale phase-docs path corrected to `pipeline/multi-agent-refs/phases/`.
2827
2911
 
2828
2912
  ---
@@ -2942,7 +3026,7 @@ research, bug hunt, category scoring, upstream-drift check).
2942
3026
  - **False-success family.** `multi-repo-pipeline.sh` no longer prints
2943
3027
  "committed/pushed" on failure; `repo-cache.sh` keeps the previous cache when a
2944
3028
  refresh fails instead of serving an empty file for the TTL; `review-watch.sh
2945
- --watch` survives transient gh failures and its cursor can no longer move
3029
+ --watch` survives transient gh failures and its cursor can no longer move
2946
3030
  backwards.
2947
3031
  - **`issue-fetcher.sh`** classifies bare `#316` / `316` correctly (repo picker
2948
3032
  path instead of a malformed `//issues/` URL).
@@ -2952,7 +3036,7 @@ research, bug hunt, category scoring, upstream-drift check).
2952
3036
  code, ballooning snapshots with DerivedData), and `restore --files` removes
2953
3037
  files created after the snapshot instead of keeping half the change.
2954
3038
  - **macOS bash 3.2 portability**: `${URL,,}` in `fetch-swagger.sh`, `date -r
2955
- <file>` in `audit-log-rotate.sh` (which also rotates atomically now).
3039
+ <file>` in `audit-log-rotate.sh` (which also rotates atomically now).
2956
3040
  - **Glob translation** in `triage-memory.mjs` / `test-gap-scan.mjs`: `**` no
2957
3041
  longer breaks after the `*` pass (nested-path filters match again).
2958
3042
  - **`migrate-prefs.mjs`** errors on unknown flags (a `--dry-rnu` typo used to
@@ -3048,7 +3132,7 @@ Self-audit patch (found by running `/multi-agent:refactor` on the pipeline itsel
3048
3132
  lists it in the PRESERVED summary.
3049
3133
  - **Fix (gate regression): `lint-skills` scoped to `pipeline/skills`.** The
3050
3134
  v11.3.0 command-layout migration made command files match `find pipeline -name
3051
- SKILL.md`; slash commands have no `name:` frontmatter, so the skill linter
3135
+ SKILL.md`; slash commands have no `name:` frontmatter, so the skill linter
3052
3136
  started erroring on all 37 of them (latent because CI is billing-paused and
3053
3137
  releases were published manually). The linter now only checks actual skills.
3054
3138
  - **Fix: `.skills-index.json` is idempotent** - dropped the `generatedAt`
@@ -3059,7 +3143,7 @@ Self-audit patch (found by running `/multi-agent:refactor` on the pipeline itsel
3059
3143
  - **Fix: install "dev-only excluded" count** is now the real file count (the
3060
3144
  `fixtures/` directory was under-counted as one entry).
3061
3145
  - **Security (supply-chain):** `release.yml` publishes with `npm publish
3062
- --provenance` (`id-token: write`); all GitHub Actions pinned to commit SHAs;
3146
+ --provenance` (`id-token: write`); all GitHub Actions pinned to commit SHAs;
3063
3147
  added `.github/dependabot.yml` (actions + npm dev deps); `credential-store.sh`
3064
3148
  escapes single quotes in the Windows PowerShell paths.
3065
3149
  - **New: `eval-mine-corpus.mjs`** turns recorded triage decisions
@@ -3171,7 +3255,7 @@ command is a breaking surface change).
3171
3255
  skeleton (Task/Story: Detailed Description, Scope, Acceptance Criteria, Test
3172
3256
  Scenarios; Bug: Detailed Description, Steps to Reproduce, Expected/Actual
3173
3257
  Result, Environment). Conditional sections (Design Reference, API Contract /
3174
- Swagger, Screenshots, Notes) render only when their trigger is present - no
3258
+ Swagger, Screenshots, Notes) render only when their trigger is present - no
3175
3259
  empty placeholder headings, nothing invented.
3176
3260
  - **Test Scenarios pulled from Jira convention.** Detects linked Xray/Zephyr test
3177
3261
  issues or a dominant test-scenario heading style and reuses it; falls back to an
@@ -3314,7 +3398,7 @@ Live tracker UX: per-tile cost, a real "currently doing X" line, and tracker con
3314
3398
  history (elapsed, tokens, USD) survives, Phase 5 is closed with a Result
3315
3399
  meta, the Claude Code TaskList is rebuilt from state, and one line
3316
3400
  summarizes the inherited history (`Continuing <id>: phases 0-3 finished
3317
- earlier (12m, 38.4k tok, ~$0.74)`). `add` is idempotent to make this safe.
3401
+ earlier (12m, 38.4k tok, ~$0.74)`). `add` is idempotent to make this safe.
3318
3402
  - **`/multi-agent:resume` rebuilds the phase tiles** from `tracker-state.json`
3319
3403
  (the contract documented it; the command now actually does it).
3320
3404
  - **Phase 0 + 7 token forwarding**: the clarifier call and the Phase 7 report
@@ -3615,7 +3699,7 @@ Generic SwiftUI interaction coverage for the figma-to-swiftui pipeline. Three
3615
3699
  new cross-cutting integration skills (navigation, overlays, bottom sheets) plus
3616
3700
  a reconcile-and-extend workflow, all native-SwiftUI-first with an optional
3617
3701
  per-project `ui.*` config hook so the same capabilities work on any SwiftUI
3618
- codebase - no app-specific coupling. Phase 3D dev detection and Phase 4 review
3702
+ codebase - no app-specific coupling. Phase 3D dev detection and Phase 4 review
3619
3703
  both consume them.
3620
3704
 
3621
3705
  ### Added
@@ -3633,12 +3717,12 @@ both consume them.
3633
3717
  component against current Figma (drift-heal) + additively extend it for a
3634
3718
  need, behind a mandatory human gate; distinct from `figma-to-swiftui`
3635
3719
  (build new), `figma-mend` (rebuild), and `figma-fix` (review bug).
3636
- - **`animated-gradient-border` UI pattern** (`figma-ui-patterns/patterns/`) -
3720
+ - **`animated-gradient-border` UI pattern** (`figma-ui-patterns/patterns/`) -
3637
3721
  self-contained native-SwiftUI recipe (angular-gradient stroke + Reduce-Motion
3638
3722
  gate), no external dependency.
3639
- - **`ui` block in `figma-project-config.schema.json`** - optional
3723
+ - **`ui` block in `figma-project-config.schema.json`** - optional
3640
3724
  `navigationSystem` / `overlaySystem` / `sheetSystem` (`mode: native | custom`
3641
- + type names). Absent → native SwiftUI.
3725
+ - type names). Absent → native SwiftUI.
3642
3726
 
3643
3727
  ### Changed
3644
3728
 
@@ -3674,7 +3758,7 @@ stale worktrees, and validates state before resume.
3674
3758
  Opus. This also resolves the prior CLAUDE.md-vs-phase-doc inconsistency
3675
3759
  (CLAUDE.md already documented Opus for these stages).
3676
3760
  - **Model fallback ladder is now `opus -> sonnet`** (was `fable -> opus ->
3677
- sonnet`). `modelFallback.fallbackModel` default is `sonnet`. The
3761
+ sonnet`). `modelFallback.fallbackModel` default is `sonnet`. The
3678
3762
  `premiumTierUntil` date gate is kept as a generic mechanism for any future
3679
3763
  plan-window-limited premium tier. `model-fallback.md` bumped to v10.1.0 and
3680
3764
  `smoke-model-fallback.sh` updated to assert the new ladder + Opus personas.
@@ -3752,12 +3836,12 @@ token ledger agree on what an input token is.
3752
3836
  - **Prompt-cache reuse ratio in metrics.** `aggregate-metrics.mjs` now sums
3753
3837
  `tokens_cached` per model and reports `cache_ratio = cached / (in + cached)`
3754
3838
  - the share of input tokens served from the host prompt cache - per model and
3755
- overall, in all three output modes (json / markdown / text). This is the
3756
- single number that says whether cache-friendly prompt structuring is paying
3757
- off. Backward-compatible: a phase that omits `tokens_cached` reads as 0%.
3839
+ overall, in all three output modes (json / markdown / text). This is the
3840
+ single number that says whether cache-friendly prompt structuring is paying
3841
+ off. Backward-compatible: a phase that omits `tokens_cached` reads as 0%.
3758
3842
  - **PR review iteration counter derived from the PR itself.** On a `needs_work`
3759
3843
  post, the iteration number is re-derived as `max("iteration #N" already on the
3760
- PR) + 1` instead of trusting agent-state. Standalone `/multi-agent:review`
3844
+ PR) + 1` instead of trusting agent-state. Standalone `/multi-agent:review`
3761
3845
  runs use a fresh task id each time, so the PR comments are the only reliable
3762
3846
  cross-run source of truth.
3763
3847
 
@@ -3767,7 +3851,7 @@ token ledger agree on what an input token is.
3767
3851
  `render-agent-log-cost.sh` treated `tokens_in` as cache-inclusive and
3768
3852
  subtracted the cached count (`fresh = in - min(cached, in)`), while the new
3769
3853
  `aggregate-metrics.mjs` treated `tokens_in` as cache-exclusive (`total =
3770
- in + cached`). Fed real data with high cache reuse (`cached > in`), the
3854
+ in + cached`). Fed real data with high cache reuse (`cached > in`), the
3771
3855
  renderer collapsed `fresh` to 0 and underpriced the row. Standardized on the
3772
3856
  **cache-exclusive** convention that matches the host usage report
3773
3857
  (`input_tokens` and `cache_read_input_tokens` are disjoint): the renderer no
@@ -3810,7 +3894,7 @@ reproduced locally by putting GNU coreutils' gnubin ahead of PATH.
3810
3894
  `search-logs.sh` (since-filter, scoring, JSON/TSV output) and
3811
3895
  `repo-cache.sh` TTLs. Probe order flipped to GNU-first (`stat -c` first;
3812
3896
  BSD rejects `-c`, so the fallback chain is safe both ways). `date -r
3813
- <epoch>` display calls gained a GNU `date -d @` fallback.
3897
+ <epoch>` display calls gained a GNU `date -d @` fallback.
3814
3898
  - **Hardcoded maintainer layout `$HOME/multi-agent-pipeline` removed** from
3815
3899
  `smoke-schema-validation.sh` (preferences-template path) and
3816
3900
  `smoke-pat-audit.sh` (.gitignore audit-log check) - both now derive the
@@ -4162,10 +4246,12 @@ Disk hygiene + correctness hardening, all gate-backed.
4162
4246
  Toward proven (not just designed): measure the features instead of asserting them, and ship the evidence-collection harness.
4163
4247
 
4164
4248
  ### Added
4249
+
4165
4250
  - **Measured intent-guard accuracy** (`eval-intent.mjs` + `pipeline/eval/intent-cases.json`). 26 labeled EN+TR cases run through `classify-intent.sh`; the gate uses operationally-safe accuracy (the only dangerous errors are a task read as a question -> work skipped, or a question read as a task -> a spurious worktree; `ambiguous` proceeds as a task so it is safe for task cases). Currently 100% safe / 96.2% exact. Wired into `npm test`. Turns the heuristic into a number with a regression set.
4166
4251
  - **Per-run outcome metrics** (`run-metrics.mjs` + fixture + `smoke-run-metrics.sh`). Parses an `agent-state.json` into the numbers that answer "did this run go well": review iterations (rework loops), first-pass-clean, reviewer signal-to-noise (accepted / raw findings), consensus verdict, build outcome. Phase 7 emits it; accumulating the output across real runs is the real-world validation corpus that golden tasks + benchmarks only approximate.
4167
4252
 
4168
4253
  ### Notes
4254
+
4169
4255
  - These address the honest "measure, don't assume" gap from the self-review: the intent guard and review signal are now quantified, and the harness exists to turn real runs into evidence. The remaining step (running real tasks + a public benchmark) is the user's, and cannot be fabricated.
4170
4256
  - CI auto-run stays disabled in `test.yml` (the maintainer paused it for GitHub Actions billing); re-enabling the push/PR triggers is a billing decision, not changed here.
4171
4257
 
@@ -4174,6 +4260,7 @@ Toward proven (not just designed): measure the features instead of asserting the
4174
4260
  Closes the structural gaps the adversarial review surfaced: the deterministic gates now actually RUN on the three adapter platforms, and the multi-model review is restored there using each platform's real model lineup.
4175
4261
 
4176
4262
  ### Added
4263
+
4177
4264
  - **Shared runtime so gates execute on Cursor / Antigravity / VS Code Copilot Chat.** The gate scripts + lib + schemas are installed once to `~/.multi-agent/` (dev-only / PII files excluded) and the emitted agents/commands/workflow reference them by absolute path (`installSharedRuntime` / `rewriteScriptRefs` in `_base.mjs`). Previously the emitted agents referenced `pipeline/scripts/...` which did not exist in the consumer project, so the deterministic gates could not run there at all. Uninstall removes the runtime. Enforced by `smoke-shared-runtime.sh`.
4178
4265
  - **Cross-vendor 2-model review on the adapter platforms.** A second reviewer agent (`ma-code-reviewer-x`) is emitted pinned to a different vendor, using each platform's actual model lineup (researched mid-2026, centralized in `_base.mjs#REVIEWER_MODELS`): Cursor `inherit` + `gpt-5.5`; VS Code Copilot Chat `Claude Opus 4.8` + `GPT-5.5`; Antigravity documents a `Gemini 3 Pro` + `Claude Opus 4.6` pair (its models are dropdown-selected, not file-pinned). Restores the cross-model diversity that was Claude-Code / Copilot-CLI-only.
4179
4266
  - **Recommended PreToolUse hooks template** (`install/templates/claude-hooks.json`) wiring the secret scan as a HARD pre-commit gate on Claude Code; `multi-agent:setup` Step 8 offers to merge it. The secret scan is the one gate that is OS-hookable (no run-specific args); the others are phase-invoked by contract. Enforced by `smoke-gate-hooks.sh`.
@@ -4181,9 +4268,11 @@ Closes the structural gaps the adversarial review surfaced: the deterministic ga
4181
4268
  - **Reviewer-count contract checks** in `smoke-cross-cli-behavior.sh`: locks Claude=2 / Copilot=3 + the documented adapter-platform reviewer set against drift.
4182
4269
 
4183
4270
  ### Fixed
4271
+
4184
4272
  - VS Code Copilot Chat agents emitted `model: inherit`, which is not a valid Copilot model (there is no `inherit` keyword; omitting `model` inherits the picker). Normal personas now omit `model`; the two reviewers pin a picker label.
4185
4273
 
4186
4274
  ### Notes (stated honestly)
4275
+
4187
4276
  - The adapter platforms still have no `PreToolUse` equivalent, so their gates are workflow-enforced (run as steps) rather than OS-blocked.
4188
4277
  - Pinned adapter models depend on the user's subscription; swap them in `REVIEWER_MODELS` / the Antigravity dropdown if a model is unavailable.
4189
4278
  - Old PII-bearing versions (9.3.0-9.3.3) remain in the private registry: GitHub Packages does not support `npm deprecate` (E400), and deleting versions / rewriting git history are irreversible and were left to an explicit owner decision. The package + repo are private, so this is not a public exposure.
@@ -4193,12 +4282,14 @@ Closes the structural gaps the adversarial review surfaced: the deterministic ga
4193
4282
  Second round of review-driven fixes - the lower-severity findings left open in 9.3.3, plus an honesty correction on the multi-platform claim.
4194
4283
 
4195
4284
  ### Fixed
4285
+
4196
4286
  - **Arg parsers dropped values starting with `--`.** `learnings-ledger.mjs` and `evidence-gate.mjs` now accept the `--key=value` form, so a statement / pattern that begins with `--` (e.g. `--statement="-- prefer let"`) is preserved instead of silently failing.
4197
4287
  - **Secret scan skipped filenames with spaces.** `pre-commit-check.sh` now iterates staged files NUL-delimited (`git diff --name-only -z`), closing a false-negative where a secret in `my file.txt` went unscanned.
4198
4288
  - **`learnings-ledger forget` rewrite is now atomic** (temp file + rename) so a crash or concurrent reader never sees a half-written ledger.
4199
4289
  - **`from-triage` scope for a top-level file** is now the filename itself, not a stray `./*` glob.
4200
4290
 
4201
4291
  ### Changed
4292
+
4202
4293
  - **Honest multi-platform claim.** The README and the adapter-emitted orchestration commands now state plainly that Claude Code + Copilot CLI run the pipeline natively (gate scripts installed), while Cursor / Antigravity / VS Code Copilot Chat receive the workflow + subagents + MCP but run the deterministic gates as ADVISORY (the gate scripts are not copied into those projects). Making those gates execute on the three adapter platforms is tracked work (needs a uniform script-path resolution + per-platform testing), not yet shipped. Stale `cursor.mjs` header ("26 commands can't run there") corrected.
4203
4294
 
4204
4295
  ## [9.3.3] - 2026-05-30
@@ -4206,68 +4297,82 @@ Second round of review-driven fixes - the lower-severity findings left open in 9
4206
4297
  A 4-agent adversarial review of the v9.3.x work surfaced real defects in the features just shipped; this release fixes them.
4207
4298
 
4208
4299
  ### Fixed
4300
+
4209
4301
  - **evidence-gate was bypassable.** A failing build log that also contained the word "SUCCESS" (cached-step note, banner) passed because success and failure were weighed equally. Failure markers are now DECISIVE (a definitive failure marker fails the claim regardless of success text), success markers were narrowed (dropped the generic `\bSUCCESS\b`), and caller-supplied `--success/--failure-pattern` are length-capped + compiled in a try/catch so a bad pattern is a clean usage error, not a crash. (`evidence-gate.mjs`, `smoke-evidence-gate.sh`)
4210
4302
  - **intent-guard misclassified questions as tasks.** "does it support offline mode", "should we enable caching" were read as tasks (the imperative check beat the interrogative) and would spin up a worktree. A strong question signal (interrogative lead / trailing `?` / TR particle) now wins over a bare imperative verb; an explicit polite request ("can you split this file") stays a task. (`classify-intent.sh`, `smoke-intent-guard.sh`)
4211
4303
  - **consensus block was decorative.** `validate-triage.mjs` validated the v3.1.0 consensus block structurally but never cross-checked it: `unanimous-block` + `approved:true`, `unanimous-pass` + an accepted blocker, and a single-reviewer "unanimous" verdict now all fail validation. (`smoke-phase4-triage.sh`)
4212
4304
  - **A malformed `tokens_cached` poisoned the whole tokens call.** `log-metric.sh` now sanitizes a non-integer cached count to 0 before forwarding, so the valid in/out counts still land. (`smoke-agent-log-cost.sh`)
4213
4305
 
4214
4306
  ### Security
4307
+
4215
4308
  - **Stopped real maintainer/employer identifiers from shipping in the npm tarball.** The dev-only figma substitution map (a scrub table that by design holds real upstream values), the two personal-data scanners, and two internal planning docs were excluded from the package via negated `files` entries; stray corporate hosts / repo names / a private Jira key in CHANGELOG + docs examples were genericized. The leak gate now scans the published npm tarball (not just the install tree), closing the hole that let these ship in 9.3.0-9.3.2. The repo/package are private, so this was not a public exposure. (`smoke-install-leak-gate.sh`, `.npmignore`, `package.json` files)
4216
4309
 
4217
4310
  ### Changed
4311
+
4218
4312
  - README "What's new" refreshed to v9.3.3; em-dashes removed from README and the `MANDATORY` keyword removed from `install/templates/copilot-instructions.md` (project style rules).
4219
4313
 
4220
4314
  ## [9.3.2] - 2026-05-30
4221
4315
 
4222
4316
  ### Fixed
4317
+
4223
4318
  - **Cost-ledger cache pricing is now wired end-to-end.** v9.3.0 added `cacheReadPerMtok` pricing + a cache-reads line to `render-agent-log-cost.sh`, but nothing fed `tokens_cached` to the tracker, so the feature was dormant. `phase-tracker.sh tokens` now accepts an optional 4th `cached` arg (defaults to 0, fully back-compatible), `log-metric.sh` forwards `tokens_cached=` into it, and the Phase 4 telemetry doc documents passing the host's `cache_read_input_tokens`. Verified end-to-end in `smoke-agent-log-cost.sh`.
4224
4319
  - `evidence-gate.mjs` made executable to match its sibling `.mjs` scripts.
4225
4320
 
4226
4321
  ### Changed
4322
+
4227
4323
  - README "What's new" refreshed to v9.3.x (was stale at v8.8.1).
4228
4324
 
4229
4325
  ## [9.3.1] - 2026-05-30
4230
4326
 
4231
4327
  ### Fixed
4328
+
4232
4329
  - **Learnings ledger no longer auto-suppresses rejected BLOCKING findings.** `learnings-ledger.mjs from-triage` previously distilled every rejected finding into a durable "do not re-flag" preference regardless of severity; a single wrong rejection of a blocking issue could permanently silence that class on future runs. Blocking-severity rejections are now skipped (reported as `skippedBlocking`); only lower-severity rejections become durable preferences, and they are recorded at `low` confidence.
4233
4330
 
4234
4331
  ### Added
4332
+
4235
4333
  - **`learnings-ledger.mjs forget`** subcommand to remove a bad or stale ledger entry by statement substring and/or kind (the one non-append operation), so a wrong learning can be cleared instead of persisting forever. Enforced by `smoke-learnings-ledger.sh`.
4236
4334
 
4237
4335
  ## [9.3.0] - 2026-05-30
4238
4336
 
4239
4337
  ### Added
4338
+
4240
4339
  - **Review consensus surfacing (anti-correlation).** Phase 4 triage now records an optional `consensus` block (triage-output schema v3.1.0): `reviewerCount`, a `verdict` (`unanimous-pass` / `unanimous-block` / `split` / `unverified`), and `disagreements[]`. Unanimous agreement among same-base-model reviewers on a judgment-heavy surface (security, auth, concurrency, money, migration) is marked `unverified` and surfaced to the user instead of being trusted as a pass. Disagreements are shown at the Step 4 checkpoint and written to the agent-log "Review Consensus" section. Validated by `validate-triage.mjs` + new fixtures in `smoke-phase4-triage.sh`.
4241
4340
  - **Persistent learnings ledger** (`pipeline/scripts/learnings-ledger.mjs`, schema `learnings-ledger.schema.json`). A per-repo, append-only store of durable architectural facts, conventions, and explicitly rejected review preferences, stored next to the triage corpus. A compact `<repo-learnings>` brief is injected into Phase 1 analysis and Phase 4 triage so agents stop re-discovering structure and reviewers stop re-flagging rejected feedback (the most-cited cold-boot-amnesia complaint). Phase 7 distills each run's rejected findings into the ledger. On by default via `prefs.global.learningsLedger`; per-repo isolated. Enforced by `smoke-learnings-ledger.sh`.
4242
4341
  - **Default-FAIL evidence gate** (`pipeline/scripts/evidence-gate.mjs`). A build/test/review "passed" claim is only trusted when a substantiating log artifact exists and shows success; the gate fails CLOSED on missing, empty, or contradicting evidence. Wired into Phase 3 (build), Phase 4 Stage 1 gates (build + test), and Phase 6 (commit). Enforced by `smoke-evidence-gate.sh`.
4243
4342
  - **Conceptual-vs-edit intent guard** (`pipeline/lib/classify-intent.sh`). A deterministic, language-aware (EN + TR) classifier runs on free-text input at Phase 0; a question is answered in place instead of spinning up a branch/worktree. On by default via `prefs.global.intentGuard`. Enforced by `smoke-intent-guard.sh`.
4244
4343
 
4245
4344
  ### Changed
4345
+
4246
4346
  - **Secret pre-commit gate** (`pre-commit-check.sh`) extended beyond pattern matching: high-signal provider-token prefixes (GitHub PAT, Slack, Google API key, Stripe, npm, GitLab), JWT detection, and a Shannon-entropy scan that catches custom/unknown secrets while exempting lockfiles, integrity hashes, source maps, and snapshots.
4247
4347
  - **Per-phase cost ledger** (`render-agent-log-cost.sh`) now prices prompt-cache reads at the discounted `cacheReadPerMtok` rate (cost-table schema 1.1.0; backward-compatible, defaults to 0 cached), appends a "Top cost driver" line so the report shows where spend went, and surfaces a cache-reads line when the tracker recorded cache hits.
4248
4348
  - Uninstall header and package description refreshed to the current 5-platform set (Cursor / Antigravity / VS Code Copilot Chat), replacing stale Windsurf/Cline references.
4249
4349
 
4250
4350
  ### Fixed
4351
+
4251
4352
  - Cursor uninstall left an empty `.cursor/commands/` directory behind, and the `.cursor` parent-empty cleanup ran before orchestration teardown so the parent was never reclaimed. Both now clean up after the orchestration uninstall.
4252
4353
 
4253
4354
  ## [9.2.0] - 2026-05-30
4254
4355
 
4255
4356
  ### Added
4357
+
4256
4358
  - **Full-pipeline orchestration on three more platforms** (previously knowledge-layer only). Cursor (`.cursor/agents/ma-*.md` subagents + `.cursor/commands/multi-agent.md` + `.cursor/mcp.json`), Antigravity (`.agent/workflows/multi-agent.md` + `.agent/rules/` + `AGENTS.md` + `.agent/mcp_config.json`), and VS Code Copilot Chat (`.github/agents/ma-*.agent.md` + `.github/prompts/multi-agent.prompt.md` + `.vscode/mcp.json`). Each adapter transforms the pipeline personas into the platform's subagent/agent format and registers the dev-toolkit MCP server. Install with `--cursor` / `--antigravity` / `--copilot-chat` (or `--all-tools`).
4257
4359
  - **Picker contract** (`refs/picker-contract.md`) + `pipeline/lib/ask-choice.sh`: a cross-platform single-choice abstraction so confirmations degrade gracefully where there is no native `AskUserQuestion` (numbered-menu fallback; `ASK_CHOICE_DEFAULT` for autopilot/CI).
4258
4360
  - **Proactive token-budget cap** (`prefs.global.costBudget` + `cost-budget-check.mjs`): prices the phase-tracker accumulators live and warns/halts before spend runs away.
4259
4361
  - **Eval harness** expanded from 2 to 7 golden tasks across all stacks and every triage bucket.
4260
4362
 
4261
4363
  ### Changed
4364
+
4262
4365
  - **Confirmations are now native pickers** instead of typed keywords (`AskUserQuestion` on Claude Code, degrading per the picker contract elsewhere). Removed the typed `y/N` / `onayla`/`iptal` prompts.
4263
4366
  - **Command/skill instruction files are English** throughout (token efficiency + model comprehension); `outputLanguage` still governs all runtime user-facing text.
4264
4367
  - Phase 5 (User Test) now runs only in interactive worktree-backed modes (`dev`, `full`); every autopilot/local variant skips it.
4265
4368
  - Analysis->plan contract field names aligned across schema, validator, and phase docs; the "no MCP outside analysis" gate made enforceable (telemetry recorded + checked).
4266
4369
 
4267
4370
  ### Fixed
4371
+
4268
4372
  - Command-injection vectors in `diff-explain.mjs` and `figma-screenshot.sh`; `review-watch` cursor loss (re-reviewed PRs forever); `diff-risk` / `classify-plan-safety` / `match-skills` logic defects; `write-state` stale-lock deadlock; several pre-existing test failures (mode-dispatch drift, README/install-layout counts, token budgets).
4269
4373
 
4270
4374
  ### Removed
4375
+
4271
4376
  - Dead `--windsurf` / `--cline` / `--continue` / `--zed` install flags (the adapters were dropped in 8.5.4; only the advertising lingered).
4272
4377
 
4273
4378
  ## [9.1.1] - 2026-05-16
@@ -4281,7 +4386,7 @@ A 4-agent adversarial review of the v9.3.x work surfaced real defects in the fea
4281
4386
  - `pipeline/commands/multi-agent/channels.md`: 103 chars
4282
4387
  - `pipeline/skills/shared/core/multi-agent/SKILL.md`: 97 chars
4283
4388
  - `pipeline/commands/multi-agent/setup.md`: 79 chars
4284
- - Mapping: `U+2014` -> ` - `, `U+2013` -> `-`, `U+2026` -> `...`, `U+201C/D` -> `"`, `U+2018/9` -> `'`, `U+00A7` -> `section`.
4389
+ - Mapping: `U+2014` -> `-`, `U+2013` -> `-`, `U+2026` -> `...`, `U+201C/D` -> `"`, `U+2018/9` -> `'`, `U+00A7` -> `section`.
4285
4390
 
4286
4391
  ### Excluded from sweep
4287
4392
 
@@ -4325,13 +4430,13 @@ A 4-agent adversarial review of the v9.3.x work surfaced real defects in the fea
4325
4430
 
4326
4431
  Six measurable improvements on the hot path:
4327
4432
 
4328
- | Fix | Before | After | Speedup |
4329
- |---|---|---|---|
4330
- | `smoke-personal-data.sh` pattern alternation (single grep vs 22 invocations) | 3.348s | 1.545s | 2.2x |
4331
- | `smoke-no-token-prompt.sh` multi-`-e` (single grep vs 7x7=49 invocations) | 0.485s | 0.014s | 35x |
4332
- | `phase-tracker.sh` render batched jq (U+001F separator preserves empty fields) | ~7 jq calls per phase + ~3 per sub | 1 batch + 1 per active sub/meta | ~120 subprocess azalma per render |
4333
- | `issue-fetcher.sh` python3 batch (single inline vs 7 separate calls per fetch) | 0.806s / 3 iter | 0.136s / 3 iter | 5.9x |
4334
- | `md2confluence-v3.py` HTTP retry wrapper (3-attempt exponential backoff on 5xx / 429) + `ThreadPoolExecutor(max_workers=4)` paralel attachment | sequential N x ~1.5s | parallel ~max(individual) | up to 4x on multi-screenshot pages, plus transient-error resilience |
4433
+ | Fix | Before | After | Speedup |
4434
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- |
4435
+ | `smoke-personal-data.sh` pattern alternation (single grep vs 22 invocations) | 3.348s | 1.545s | 2.2x |
4436
+ | `smoke-no-token-prompt.sh` multi-`-e` (single grep vs 7x7=49 invocations) | 0.485s | 0.014s | 35x |
4437
+ | `phase-tracker.sh` render batched jq (U+001F separator preserves empty fields) | ~7 jq calls per phase + ~3 per sub | 1 batch + 1 per active sub/meta | ~120 subprocess azalma per render |
4438
+ | `issue-fetcher.sh` python3 batch (single inline vs 7 separate calls per fetch) | 0.806s / 3 iter | 0.136s / 3 iter | 5.9x |
4439
+ | `md2confluence-v3.py` HTTP retry wrapper (3-attempt exponential backoff on 5xx / 429) + `ThreadPoolExecutor(max_workers=4)` paralel attachment | sequential N x ~1.5s | parallel ~max(individual) | up to 4x on multi-screenshot pages, plus transient-error resilience |
4335
4440
  | `extract-conventions.sh` env override `EXTRACT_CONV_EXTRA_ROOTS` + auto-add `.gitmodules` paths, bucket timeout 30s -> 10s, `xargs basename` -> `awk -F/ '{print $NF}'` (8 callsites) | scan roots too narrow on monorepos with submodules; per-bucket 30s budget | submodule paths auto-detected, faster fail | resolves "confidence: none" on submodule-heavy repos; ~100-300ms saved per bucket |
4336
4441
 
4337
4442
  ### Verified