@mmerterden/multi-agent-pipeline 20.24.0 → 20.25.1

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 (68) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/docs/facts.json +3 -3
  3. package/docs/features.md +9 -4
  4. package/install/catalog-history.json +1 -1
  5. package/install/templates/claude-hooks.json +1 -1
  6. package/install/templates/codex-hooks.json +18 -0
  7. package/manifest.json +71 -68
  8. package/package.json +1 -1
  9. package/pipeline/agents/task-clarifier.md +1 -1
  10. package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
  11. package/pipeline/commands/multi-agent/postmortem/SKILL.md +18 -6
  12. package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
  13. package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
  15. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
  16. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  17. package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
  18. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  19. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  20. package/pipeline/contract/fixtures/runs-running.json +1 -1
  21. package/pipeline/lib/credential-inventory.sh +5 -5
  22. package/pipeline/lib/fetch-crashlytics.sh +6 -8
  23. package/pipeline/lib/model-rung.sh +1 -1
  24. package/pipeline/lib/plan-todos.sh +21 -4
  25. package/pipeline/lib/post-pr-review.sh +4 -6
  26. package/pipeline/lib/route-state.sh +1 -1
  27. package/pipeline/lib/shadow-git.sh +4 -5
  28. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -5
  29. package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
  30. package/pipeline/multi-agent-refs/features/model-fallback.md +17 -12
  31. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  32. package/pipeline/multi-agent-refs/phases/phase-3-review.md +5 -5
  33. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  34. package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
  35. package/pipeline/schemas/plan-todos.schema.json +4 -4
  36. package/pipeline/schemas/prefs.schema.json +4 -4
  37. package/pipeline/scripts/_code-graph.mjs +2 -2
  38. package/pipeline/scripts/_transcript-hosts.mjs +406 -0
  39. package/pipeline/scripts/build-stack-plugins.mjs +3 -3
  40. package/pipeline/scripts/capture-flush.sh +7 -7
  41. package/pipeline/scripts/code-graph-rules/android.json +1 -1
  42. package/pipeline/scripts/cost-analyze.mjs +26 -19
  43. package/pipeline/scripts/cost-table.json +35 -19
  44. package/pipeline/scripts/edit-syntax-gate.mjs +151 -29
  45. package/pipeline/scripts/edit-syntax-gate.sh +9 -6
  46. package/pipeline/scripts/gc-abandoned.sh +2 -2
  47. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
  48. package/pipeline/scripts/git-path.mjs +1 -2
  49. package/pipeline/scripts/host-hooks.mjs +265 -0
  50. package/pipeline/scripts/jira-attach.sh +1 -1
  51. package/pipeline/scripts/keychain.py +3 -3
  52. package/pipeline/scripts/output-quality-check.sh +1 -1
  53. package/pipeline/scripts/phase-tracker.sh +5 -6
  54. package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
  55. package/pipeline/scripts/pre-push-check.sh +3 -6
  56. package/pipeline/scripts/transcript-index.mjs +423 -242
  57. package/pipeline/scripts/uninstall.mjs +90 -4
  58. package/pipeline/scripts/update-issue-progress.sh +4 -6
  59. package/pipeline/scripts/usage-report.mjs +2 -2
  60. package/pipeline/skills/.skill-manifest.json +8 -8
  61. package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
  62. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
  63. package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +10 -7
  64. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
  65. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
  66. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
  67. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
  68. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
@@ -429,14 +429,16 @@ To set up multi-agent on a new machine:
429
429
 
430
430
  All tokens are optional in the sense that every service can be answered with Skip - but the ASKING is not optional: the Step 3 sequential loop still walks every missing service one by one (token → author → host). Phase 0 re-asks at runtime only for tokens the user skipped here.
431
431
 
432
- ### Step 8 - Enforcement hooks (optional, Claude Code)
432
+ ### Step 8 - Enforcement hooks (optional)
433
433
 
434
- Offer to merge `$HOME/.claude/templates/claude-hooks.json`: three `PreToolUse` gates that block on a non-zero exit (secret scan, agent-guard, read-size) plus four hooks that block nothing (`PreCompact`, `SessionEnd`, `SessionStart`, `PostToolUse`). What each does: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
434
+ On Claude Code, offer to merge `$HOME/.claude/templates/claude-hooks.json`: three blocking `PreToolUse` gates (secret scan, agent-guard, read-size) plus four non-blocking hooks (`PreCompact`, `SessionEnd`, `SessionStart`, `PostToolUse`). What each does: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
435
435
 
436
436
  - Ask (picker): "Install the pipeline's hooks into `~/.claude/settings.json`?" Default Yes.
437
- - On Yes, deep-merge EVERY event in the template's `hooks` object, not `PreToolUse` alone - merging one event silently drops the capture hooks, and a run killed before Phase 5 then loses its findings exactly as it did before they existed. Preserve existing hooks; never duplicate a matcher already calling the same script.
438
- - Say what the merge does NOT cover: only the three gates need no run-specific arguments, so only they are hookable; the rest are phase-enforced.
439
- - Say what it does not turn on: the read-size gate is inert until `prefs.global.bulkRead.mode` is set. Recommend `observe` first. Why, and the Phase 3 exemption: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
437
+ - On Yes, deep-merge EVERY event in the template's `hooks` object (merging `PreToolUse` alone drops the capture hooks); keep existing hooks, never duplicate a matcher calling the same script.
438
+ - Say only the three gates are hookable (no run-specific arguments); the rest are phase-enforced.
439
+ - Say the read-size gate stays inert until `prefs.global.bulkRead.mode` is set; recommend `observe` first (why, and the Phase 3 exemption: `picker-contract.md`).
440
+
441
+ Codex: offer only the syntax gate, then run `node "$HOME/.codex/scripts/host-hooks.mjs" install --host codex`. Copilot CLI: offer no hook (it discards `postToolUse` output as of 1.0.45).
440
442
 
441
443
  ### Step 9 - Default stack plugin enablement
442
444
 
@@ -11,10 +11,9 @@ allowed-tools: Agent, Bash, Read, Write, Edit, Glob, Grep, TaskCreate, TaskUpdat
11
11
 
12
12
  # /multi-agent:testflight-validation - iOS alias for store-ready
13
13
 
14
- This command is an alias. The implementation it used to carry was merged into
14
+ This command is an alias. The implementation lives in
15
15
  `/multi-agent:store-ready`, which runs the same three gates on iOS and adds the
16
- Android side, so there is one flow to maintain instead of two that had already
17
- started to drift.
16
+ Android side, so there is one flow to maintain instead of two that drift.
18
17
 
19
18
  The name is kept because it is the one people reach for when the target is
20
19
  TestFlight, and removing a command is a breaking change to the slash-command
@@ -85,7 +85,7 @@
85
85
  "out": 1020,
86
86
  "cached": 2700
87
87
  },
88
- "estUsd": 0.0449,
88
+ "estUsd": 0.0353,
89
89
  "unlockedWrites": 0,
90
90
  "runType": "development",
91
91
  "title": null,
@@ -63,7 +63,7 @@
63
63
  "out": 680,
64
64
  "cached": 1800
65
65
  },
66
- "estUsd": 0.0299,
66
+ "estUsd": 0.0236,
67
67
  "unlockedWrites": 0
68
68
  }
69
69
  ]
@@ -106,7 +106,7 @@
106
106
  "out": 2040,
107
107
  "cached": 5400
108
108
  },
109
- "estUsd": 0.0897,
109
+ "estUsd": 0.0707,
110
110
  "unlockedWrites": 0,
111
111
  "runType": "development",
112
112
  "title": null,
@@ -111,7 +111,7 @@
111
111
  "out": 2040,
112
112
  "cached": 5400
113
113
  },
114
- "estUsd": 0.0897,
114
+ "estUsd": 0.0707,
115
115
  "unlockedWrites": 0,
116
116
  "runType": "development",
117
117
  "title": null,
@@ -96,7 +96,7 @@
96
96
  "out": 680,
97
97
  "cached": 1800
98
98
  },
99
- "estUsd": 0.0299,
99
+ "estUsd": 0.0236,
100
100
  "unlockedWrites": 0,
101
101
  "runType": "autopilot",
102
102
  "title": "Fix the crash on the login screen",
@@ -3,11 +3,11 @@
3
3
  #
4
4
  # WHY THIS EXISTS
5
5
  #
6
- # A run asked the user to paste a Crashlytics stack trace by hand while a Firebase
7
- # service-account JSON sat in the Keychain, mapped as `firebase`, fully valid. Nothing
8
- # was broken - the pipeline simply never asked itself "do I already hold a credential
9
- # that answers this?" before asking the user. From the user's side that is worse than a
10
- # failure: they had supplied the key precisely so this would not happen.
6
+ # A run that asks the user to paste a Crashlytics stack trace by hand while a valid
7
+ # Firebase service-account JSON sits in the Keychain, mapped as `firebase`, has failed
8
+ # even though nothing is broken. From the user's side that is worse than a failure: they
9
+ # supplied the key precisely so this would not happen. So the pipeline asks itself "do I
10
+ # already hold a credential that answers this?" before it asks the user.
11
11
  #
12
12
  # One command, so there is no excuse for asking blind. Run it before any question that
13
13
  # requests data an external system holds, and let the answer shape the question:
@@ -214,15 +214,13 @@ if [ -z "$SA_JSON_RAW" ]; then
214
214
  fi
215
215
 
216
216
  # The credential store returns the JSON as stored - encoding is its internal
217
- # business, not this script's. The base64 attempt that used to live here was
218
- # guesswork that never fired: what actually broke the fetcher was the store
219
- # returning bare hex for any multi-line value, which is neither base64-of-JSON
220
- # nor JSON, so both branches failed and the failure surfaced three layers later
221
- # as a token-exchange error.
217
+ # business, not this script's, so nothing here guesses at a decoding. A guess
218
+ # that misfires (bare hex read as base64, say) fails three layers later as a
219
+ # token-exchange error instead of here.
222
220
  #
223
- # With no decode step there is nothing to stage on disk, so the temp file and its
224
- # cleanup traps went with it: the service-account JSON now only ever exists in a
225
- # shell variable, which is one fewer place a GCP private key can be left behind.
221
+ # With no decode step there is nothing to stage on disk, so there is no temp file
222
+ # and no cleanup trap: the service-account JSON only ever exists in a shell
223
+ # variable, which is one fewer place a GCP private key can be left behind.
226
224
  SA_JSON="$SA_JSON_RAW"
227
225
 
228
226
  # Verify project_id from SA JSON matches the URL - protects against pasting a
@@ -69,7 +69,7 @@ host_note() {
69
69
  echo "Claude Code: the fable rung is Fable 5. This switch is live here."
70
70
  ;;
71
71
  copilot)
72
- echo "Copilot CLI: Fable 5 is not offered; its personas never sat on this rung."
72
+ echo "Copilot CLI: Fable is not offered; its personas never sat on this rung."
73
73
  echo "This setting is recorded but changes nothing on this host."
74
74
  ;;
75
75
  codex)
@@ -13,6 +13,7 @@
13
13
  # Subcommands:
14
14
  # init <task-id> [title] Initialize with empty todos[] (Phase 2 usually pipes the JSON instead)
15
15
  # set <task-id> <plan-json> Replace the whole plan from a JSON blob on stdin or arg
16
+ # (exit 2 when a dep names no todo id exactly)
16
17
  # start <task-id> <todo-id> Mark in_progress, set startedAt; refuses if deps unsatisfied
17
18
  # or the todo awaits re-plan confirmation
18
19
  # complete <task-id> <todo-id> [notes] [--no-tests <reason>]
@@ -84,10 +85,9 @@ resolve_state() {
84
85
  local task_id="$1"
85
86
  local found=""
86
87
  # multi-repo-pipeline.sh writes agent-state.json under a per-project
87
- # directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/) - not the
88
- # ~/.claude/projects/*/state/ path this used to glob, which no writer in
89
- # the pipeline ever populates.
90
- # This globbed ONLY the nested layout; flat-layout runs resolved to nothing.
88
+ # directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/), other
89
+ # writers write it flat; no writer uses ~/.claude/projects/*/state/.
90
+ # ma_resolve_run_file checks both layouts.
91
91
  found="$(ma_resolve_run_file "$task_id" agent-state.json 2>/dev/null || true)"
92
92
  if [ -z "$found" ]; then
93
93
  echo "plan-todos: agent-state.json not found for task=$task_id" >&2
@@ -155,6 +155,23 @@ do_set() {
155
155
  echo "plan-todos: every todo must be an object with string .id and .task fields" >&2
156
156
  exit 2
157
157
  fi
158
+ # Every dependency must name a todo exactly. Ids are compared as written, so
159
+ # `t1` against a todo `T1` would leave the dependent blocked for good; the
160
+ # error names the id that differs only by case.
161
+ local bad_deps
162
+ bad_deps=$(jq -r '
163
+ [.todos[].id] as $ids
164
+ | [.todos[] | .id as $owner | (.deps // [])[]
165
+ | select((type != "string") or (. as $d | $ids | index($d) | not))
166
+ | . as $d
167
+ | ($ids | map(select(($d | type) == "string" and ascii_downcase == ($d | ascii_downcase)))) as $near
168
+ | "\($owner) depends on \($d | tojson), which is no todo id"
169
+ + (if ($near | length) > 0 then " (did you mean \($near | map(tojson) | join(", "))?)" else "" end)]
170
+ | .[]' <<<"$plan_blob" 2>/dev/null)
171
+ if [ -n "$bad_deps" ]; then
172
+ printf 'plan-todos: %s\n' "$bad_deps" >&2
173
+ exit 2
174
+ fi
158
175
  local state_path; state_path=$(resolve_state "$task_id")
159
176
  local now; now=$(iso_now)
160
177
  local new
@@ -107,12 +107,10 @@ if [ -z "$TASK_ID" ]; then
107
107
  fi
108
108
 
109
109
  # Resolve agent-state.json. multi-repo-pipeline.sh writes it under a
110
- # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
111
- # - not the ~/.claude/projects/*/state/ path this used to glob, which no
112
- # writer in the pipeline ever populates.
113
- # This globbed ONLY the nested layout. phase-tracker.sh and most Phase 0 paths
114
- # write flat, so on a real machine the majority of runs resolved to nothing and
115
- # this script reported "no agent-state.json" for a run whose state existed.
110
+ # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/),
111
+ # while phase-tracker.sh and most Phase 0 paths write it flat; no writer uses
112
+ # ~/.claude/projects/*/state/. ma_resolve_run_file checks both layouts, so a
113
+ # run whose state exists is always found.
116
114
  AGENT_STATE="$(ma_resolve_run_file "$TASK_ID" agent-state.json 2>/dev/null || true)"
117
115
 
118
116
  if [ -z "$AGENT_STATE" ]; then
@@ -77,7 +77,7 @@ free_savings() {
77
77
  since=$(( $(date +%s) - 7 * 86400 ))
78
78
  jq -rs --argjson since "$since" --slurpfile ct "$COST_TABLE" '
79
79
  map(select((.ts | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601? // 0) >= $since))
80
- | ($ct[0].prices.haiku // {inPerMtok: 1, outPerMtok: 5}) as $h
80
+ | ($ct[0].prices.haiku // {inPerMtok: 0.5, outPerMtok: 2.5}) as $h
81
81
  | {
82
82
  n: length,
83
83
  ok: (map(select(.ok and (.cached | not))) | length),
@@ -227,11 +227,10 @@ do_prune() {
227
227
  printf 'shadow-git: pruned shadow repo for %s\n' "$task_id" >&2
228
228
  else
229
229
  [ ! -d "$sd/.git" ] && { err "no shadow for $task_id"; exit 1; }
230
- # Retention never touches a real work-tree: this used to run
231
- # `reset --hard` with --work-tree=$(pwd), silently rewriting whatever
232
- # directory prune happened to be invoked from, and it kept the OLDEST
233
- # commit inside the window while discarding every newer one - backwards
234
- # for a "keep the last N days" policy. Every git call below is
230
+ # Retention never touches a real work-tree: a `reset --hard` with
231
+ # --work-tree=$(pwd) would silently rewrite whatever directory prune is
232
+ # invoked from. It keeps every commit inside the window and drops the
233
+ # older ones, as a "keep the last N days" policy requires. Every git call below is
235
234
  # git-dir-only plumbing (log/rev-parse/commit-tree/update-ref/gc - no
236
235
  # checkout, no reset, no clean); the one --work-tree `sg` still wants
237
236
  # points at $sd itself, which prune already owns.
@@ -262,8 +262,8 @@ that quietly no-ops is worse than one that says it cannot act.
262
262
 
263
263
  | Host | What `model on|off` does | Why |
264
264
  |---|---|---|
265
- | Claude Code | live switch: flips `modelFallback.fableEnabled` and realigns `costBudget.pricingModel` | the only host where the `fable` rung is Fable 5 |
266
- | Copilot CLI | writes the preference, changes no dispatch | Fable 5 is not offered there; its personas never sat on this rung |
265
+ | Claude Code | live switch: flips `modelFallback.fableEnabled` and realigns `costBudget.pricingModel` | the only host where the `fable` rung is Fable 5.1 |
266
+ | Copilot CLI | writes the preference, changes no dispatch | Fable is not offered there; its personas never sat on this rung |
267
267
  | Codex CLI | writes the preference, changes no dispatch, **deliberately** | the `fable` rung on Codex means `gpt-5.6 @ xhigh` - a different model on a different account. A knob named after an Anthropic model must not silently retune a Codex run |
268
268
 
269
269
  `model-rung.sh` prints which of the three it is on every invocation. The rule it
@@ -440,9 +440,8 @@ what keeps a secret off argv and what writes the audit entry.
440
440
  something that is not a timestamp.
441
441
  - `sed -E`, never `-r`; `sed -i ''` when editing in place.
442
442
 
443
- Clipboard callers may use `pbpaste` / `pbcopy` directly; the three-way
444
- `command -v` guard they used to carry was for hosts this package no longer
445
- installs on.
443
+ Clipboard callers may use `pbpaste` / `pbcopy` directly; a `command -v` guard
444
+ for other clipboard tools would serve hosts this package does not install on.
446
445
 
447
446
  ---
448
447
 
@@ -46,6 +46,10 @@ discovered later:
46
46
  - A host with no such transcripts (Copilot, Codex) reports `UNMEASURED`, never
47
47
  zero. Zero would read as "you spent nothing" when it means "I could not see".
48
48
 
49
+ A record is priced by its exact wire ID: a rung's `modelId`, a Copilot CLI
50
+ `copilotModelId` at its own model's rate from the table's `models` map, or a
51
+ previous-generation ID in the table's `legacy` map at that ID's own rate. A
52
+ dated snapshot (`<id>-<date>`) prices as the longest known ID it extends.
49
53
  Records whose model is not in `cost-table.json` are COUNTED as unpriced and left
50
54
  out of the total. Folding them in at zero would make an unknown model look free,
51
55
  which is the direction that hurts.
@@ -11,17 +11,17 @@
11
11
  - [Codex CLI](#codex-cli)
12
12
  <!-- /toc -->
13
13
 
14
- > Contract last revised in **v10.6.0** (Fable 5 restored as top tier). The version tag here tracks the last substantive change to this contract, not the pipeline release.
14
+ > Contract last revised in **v20.25.0** (current Claude generation; Copilot CLI model ids per rung). The version tag here tracks the last substantive change to this contract, not the pipeline release.
15
15
 
16
16
  Personas route to the top available intelligence tier they declare in
17
17
  `preferredModel`. That tier can be quota-limited or temporarily unavailable.
18
18
  This contract defines when and how the orchestrator falls back, deterministically,
19
19
  without ever editing persona files at runtime.
20
20
 
21
- > **Fable 5 available again (2026-07).** `claude-fable-5` is the top tier once
22
- > more. Architect (`ios/android/backend-architect`), Reviewer-1 (`code-reviewer`),
21
+ > **Fable is the top tier.** The `fable` rung resolves to `claude-fable-5-1`.
22
+ > Architect (`ios/android/backend-architect`), Reviewer-1 (`code-reviewer`),
23
23
  > and triage personas declare `preferredModel: fable` - the deepest-reasoning
24
- > roles. **If Fable 5 is unavailable, the first fallback is opus (`claude-opus-5`).**
24
+ > roles. **If Fable is unavailable, the first fallback is opus (`claude-opus-5-5`).**
25
25
  > Security and other `preferredModel: opus` personas keep opus as their top tier.
26
26
  > The `premiumTierUntil` date gate stays in the contract as a generic mechanism
27
27
  > for any plan-window-limited premium tier.
@@ -35,7 +35,7 @@ fable -> opus -> sonnet -> haiku
35
35
  The orchestrator walks **one step down per trigger** from whichever tier a
36
36
  persona prefers: a `fable` persona degrades `fable -> opus` first, then
37
37
  `opus -> sonnet`, then `sonnet -> haiku`; an `opus` persona starts at
38
- `opus -> sonnet`. So a Fable 5 outage transparently promotes Opus 5 into the
38
+ `opus -> sonnet`. So a Fable outage transparently promotes Opus into the
39
39
  architect/reviewer roles with no file edits.
40
40
 
41
41
  ### Rung names are the contract; model IDs are not
@@ -46,11 +46,16 @@ model ID each rung resolves to lives in exactly two places: `scripts/cost-table.
46
46
  `phases/phase-3-review.md`. A generation move edits those; it never renames a rung and
47
47
  never touches a persona file.
48
48
 
49
- Current resolution: `fable` -> `claude-fable-5`, `opus` -> `claude-opus-5`,
50
- `sonnet` -> `claude-sonnet-5`, `haiku` -> `claude-haiku-4-5`.
49
+ Current resolution: `fable` -> `claude-fable-5-1`, `opus` -> `claude-opus-5-5`,
50
+ `sonnet` -> `claude-sonnet-5-5`, `haiku` -> `claude-haiku-5-5`. These are the Claude Code
51
+ wire IDs. Copilot CLI dispatches only the models `copilot help config` lists, with dotted
52
+ names, so its column in the reviewer table names those (`claude-opus-4.7`,
53
+ `claude-sonnet-4.6`), recorded per rung as `copilotModelId` in the cost table (haiku:
54
+ `claude-haiku-4.5`). The fable rung has none and is not offered on Copilot CLI; a persona
55
+ that prefers it dispatches the opus rung there.
51
56
 
52
- **Rate limits do not follow the rung.** Claude Opus 5 draws on a pool separate from
53
- the combined Opus 4.x pool, so a generation move does not inherit the previous
57
+ **Rate limits do not follow the rung.** A new generation can draw on a rate-limit pool
58
+ separate from the previous one, so a generation move does not inherit the previous
54
59
  generation's headroom. Budget-ceiling downgrades below are unaffected - they read the
55
60
  cost ledger, not the provider's quota - but a 429 on the opus rung after a generation
56
61
  move is a limits question, not a fallback-contract question.
@@ -119,8 +124,8 @@ Scope is Claude Code only, and the reason differs per host:
119
124
 
120
125
  | Host | Effect of `fableEnabled: false` | Why |
121
126
  |---|---|---|
122
- | Claude Code | Every `preferredModel: fable` persona (`ios/android/backend-architect`, `code-reviewer`, triage) starts on `opus` | This is the only host where the rung means Fable 5 |
123
- | Copilot CLI | none | Fable 5 is not offered there; its personas never sat on this rung |
127
+ | Claude Code | Every `preferredModel: fable` persona (`ios/android/backend-architect`, `code-reviewer`, triage) starts on `opus` | This is the only host where the rung means Fable 5.1 |
128
+ | Copilot CLI | none | Fable is not offered there; its personas never sat on this rung |
124
129
  | Codex CLI | none, deliberately | The `fable` rung there means `gpt-5.6 @ xhigh`, a different model on a different account. Switching it off from a knob named after an Anthropic model would surprise a Codex user, so it does not |
125
130
 
126
131
  **Phase 4 loses a reviewer, and says so.** Reviewer 1 lands on `opus`, which
@@ -250,7 +255,7 @@ better internals.
250
255
  - No edits to `pipeline/agents/*.md` at runtime; frontmatter is install-time
251
256
  configuration only.
252
257
  - Copilot CLI reviewer set is out of scope: Copilot CLI pins its own three
253
- reviewer models (GPT-5.4 + Opus + Sonnet - Fable 5 is not offered there) and
258
+ reviewer models (GPT-5.4 + Opus + Sonnet - Fable is not offered there) and
254
259
  does not use this persona ladder. Only Claude Code dispatches Reviewer-1 on
255
260
  Fable.
256
261
 
@@ -652,7 +652,7 @@ Log: `Phase 0 Step 7.7: testDepth = {unit|unit+ui|unit+mcp} (source {user|autopi
652
652
 
653
653
  5. Phase 1 Analysis reads `state.clarification.userAnswers` (when present) as additional context - fold answers into the Explore prompt so downstream phases inherit the resolution.
654
654
 
655
- **Reference:** `$HOME/.claude/agents/task-clarifier.md` - scoring rubric, question-quality rules and the ~$0.0025-per-call cost note.
655
+ **Reference:** `$HOME/.claude/agents/task-clarifier.md` - scoring rubric, question-quality rules and the ~$0.0004-per-call cost note.
656
656
 
657
657
  #### Telemetry
658
658
 
@@ -237,10 +237,10 @@ Launch Agent instances **in parallel** using the shared `code-reviewer` subagent
237
237
 
238
238
  | Reviewer | subagent_type | Claude Code | Copilot CLI | Codex CLI | Focus | Skills Referenced |
239
239
  | ---------- | --------------- | --- | --- | --- | --- | --- |
240
- | Reviewer 1 | `code-reviewer` | `claude-fable-5` | `claude-opus-5` | `gpt-5.6` @ `xhigh` | Deep security + architecture | `api-security-best-practices`, `architecture` |
241
- | Reviewer 2 | `code-reviewer` | `claude-opus-5` | `gpt-5.4` | `gpt-5.4` @ `high` | Edge cases, different perspective | cross-model diversity |
242
- | Reviewer 3 | `code-reviewer` | `claude-sonnet-5` | `claude-sonnet-5` | `gpt-5.6` @ `medium` | Quality + correctness + naming | `ai-backend-toolkit:clean-code`, stack-specific skill |
243
- | Triage | triage persona | `claude-fable-5` | `claude-opus-5` | `gpt-5.6` @ `max` | Filter false positives + out-of-scope | - |
240
+ | Reviewer 1 | `code-reviewer` | `claude-fable-5-1` | `claude-opus-4.7` | `gpt-5.6` @ `xhigh` | Deep security + architecture | `api-security-best-practices`, `architecture` |
241
+ | Reviewer 2 | `code-reviewer` | `claude-opus-5-5` | `gpt-5.4` | `gpt-5.4` @ `high` | Edge cases, different perspective | cross-model diversity |
242
+ | Reviewer 3 | `code-reviewer` | `claude-sonnet-5-5` | `claude-sonnet-4.6` | `gpt-5.6` @ `medium` | Quality + correctness + naming | `ai-backend-toolkit:clean-code`, stack-specific skill |
243
+ | Triage | triage persona | `claude-fable-5-1` | `claude-opus-4.7` | `gpt-5.6` @ `max` | Filter false positives + out-of-scope | - |
244
244
 
245
245
  Reviewer count per host: **Claude Code 3, Copilot CLI 3, Codex CLI 3** - **2** on Claude Code with the fable rung off, per `$HOME/.claude/multi-agent-refs/features/model-fallback.md`.
246
246
 
@@ -265,7 +265,7 @@ borderline finding. Where the diversity budget goes instead:
265
265
 
266
266
  Each reviewer inherits the `code-reviewer` agent's focus areas (Security, Architecture, Quality, Performance) and output contract. The orchestrator overrides only the model and the stack-specific skill per-reviewer - no prompt duplication.
267
267
 
268
- **Model override wiring:** `code-reviewer.md` declares `preferredModel: fable`, so Reviewer 1 uses the persona default (Fable 5). Reviewer 2 (`claude-opus-5` on Claude Code, `gpt-5.4` elsewhere) and Reviewer 3 (`claude-sonnet-5`) set `PHASE_MODEL_OVERRIDE=<model>` before dispatch - the orchestrator exports `CLAUDE_CODE_SUBAGENT_MODEL` on Claude Code, or passes `--model` on Copilot CLI. Full precedence rule: `skills/shared/core/multi-agent/SKILL.md#agent-dispatch--per-persona-model-routing`. Fable dispatches are subject to the fallback contract (`$HOME/.claude/multi-agent-refs/features/model-fallback.md`): dispatch-error retry walks `fable -> opus -> sonnet` and budget-ceiling downgrade.
268
+ **Model override wiring:** `code-reviewer.md` declares `preferredModel: fable`, so Reviewer 1 uses the persona default (Fable 5.1). Reviewer 2 (`claude-opus-5-5` on Claude Code, `gpt-5.4` elsewhere) and Reviewer 3 (sonnet rung, per host above) set `PHASE_MODEL_OVERRIDE=<model>` before dispatch - the orchestrator exports `CLAUDE_CODE_SUBAGENT_MODEL` on Claude Code, or passes `--model` on Copilot CLI. Full precedence rule: `skills/shared/core/multi-agent/SKILL.md#agent-dispatch--per-persona-model-routing`. Fable dispatches are subject to the fallback contract (`$HOME/.claude/multi-agent-refs/features/model-fallback.md`): dispatch-error retry walks `fable -> opus -> sonnet` and budget-ceiling downgrade.
269
269
 
270
270
  **Stack-specific skills loaded per reviewer** (from Phase 1 `detectedStack`). All three columns are used on every host; Reviewer 2 reads them as Opus on Claude Code and as GPT-5.4 elsewhere.
271
271
 
@@ -190,6 +190,6 @@ park it writes `agent-state.json` beside its tracker with `taskId`,
190
190
 
191
191
  ## Deterministic gates note
192
192
 
193
- Claude Code's `PreToolUse` exit-2 hooks are the HARD blocking gates. Three ship, none needing run-specific arguments so they are naturally hookable: (1) `pre-commit-check.sh` scans the staged diff on every `git commit` and blocks on a detected secret; (2) `agent-guard.sh` runs on `git commit` + `git push` and blocks AI/assistant attribution in a commit message and force-push to a protected branch (main/master/develop); (3) `check-read-size.sh` runs on `Read` and on the shell commands that read a file whole, and routes an oversized read to a cheap worker (`bulk-read.sh`) instead of the caller's own rung. The first two inspect what a run WRITES; the third inspects what it pays to READ, and it is inert until `bulkRead.mode` is set to `observe` or `enforce`, so merging the block changes nothing until the user opts in. Its `observe` mode blocks nothing and only logs, which is how the baseline is measured before anything is routed. All three are self-contained and never execute the inspected command. On an internal error they let the call through in an attended session; `agent-guard.sh` blocks instead under `MULTI_AGENT_UNATTENDED=1`, where nobody is watching to notice a gap. Three capture hooks ship in the same block and block nothing: `SessionEnd` runs `capture-flush.sh --if-stale` (writing a killed run's findings into the per-repo stores, so they do not depend on the run reaching Phase 5) plus `note-session.sh` (the mechanical shape of a non-pipeline session: tools used, commands that failed, calls the user refused - never an argument, never any output), `PreCompact` runs `capture-flush.sh` without `--if-stale` (a compaction summarizes a long phase mid-flight, so it is the moment unflushed findings are at risk; the staleness test exists only so a session exit does not re-flush a finished run, and both store writes are idempotent), and `SessionStart` runs `capture-resume.sh`, at most two lines about an unfinished run and a stale observation queue. None of them calls a model; all exit 0 on every path. One `PostToolUse` hook ships in the same block and blocks nothing either: `edit-syntax-gate.sh` on `Edit|Write|MultiEdit` parses the file the edit just wrote (`swiftc -parse`, `node --check`, `py_compile` into a temp dir, `JSON.parse`, `bash -n`; Kotlin is skipped, no parser answers inside its 2 s deadline) and hands a syntax error to the model as `additionalContext`; it is silent when a parser is missing or slow, and inert until `editSyntaxGate.enabled` is true. Every registration here is Claude Code's: Codex has a `PostToolUse` event and Copilot CLI a `postToolUse` one, but the installer registers none of the pipeline's hooks on either host, so this gate is Claude Code only for now. The recommended hook block ships at `install/templates/claude-hooks.json`; `multi-agent:setup` offers to merge it into `~/.claude/settings.json`. The other deterministic gates (evidence, consensus, intent, learnings) are invoked by the pipeline phases with per-run arguments (a build-log path, the triage JSON, the free-text input), so they are phase-enforced by contract, not OS-hookable.
193
+ Claude Code's `PreToolUse` exit-2 hooks are the HARD blocking gates. Three ship, none needing run-specific arguments so they are naturally hookable: (1) `pre-commit-check.sh` scans the staged diff on every `git commit` and blocks on a detected secret; (2) `agent-guard.sh` runs on `git commit` + `git push` and blocks AI/assistant attribution in a commit message and force-push to a protected branch (main/master/develop); (3) `check-read-size.sh` runs on `Read` and on the shell commands that read a file whole, and routes an oversized read to a cheap worker (`bulk-read.sh`) instead of the caller's own rung. The first two inspect what a run WRITES; the third inspects what it pays to READ, and it is inert until `bulkRead.mode` is set to `observe` or `enforce`, so merging the block changes nothing until the user opts in. Its `observe` mode blocks nothing and only logs, which is how the baseline is measured before anything is routed. All three are self-contained and never execute the inspected command. On an internal error they let the call through in an attended session; `agent-guard.sh` blocks instead under `MULTI_AGENT_UNATTENDED=1`, where nobody is watching to notice a gap. Three capture hooks ship in the same block and block nothing: `SessionEnd` runs `capture-flush.sh --if-stale` (writing a killed run's findings into the per-repo stores, so they do not depend on the run reaching Phase 5) plus `note-session.sh` (the mechanical shape of a non-pipeline session: tools used, commands that failed, calls the user refused - never an argument, never any output), `PreCompact` runs `capture-flush.sh` without `--if-stale` (a compaction summarizes a long phase mid-flight, so it is the moment unflushed findings are at risk; the staleness test exists only so a session exit does not re-flush a finished run, and both store writes are idempotent), and `SessionStart` runs `capture-resume.sh`, at most two lines about an unfinished run and a stale observation queue. None of them calls a model; all exit 0 on every path. One `PostToolUse` hook ships in the same block and blocks nothing either: `edit-syntax-gate.sh` on `Edit|Write|MultiEdit` parses the file the edit just wrote (`swiftc -parse`, `node --check`, `py_compile` into a temp dir, `JSON.parse`, `bash -n`; Kotlin is skipped, no parser answers inside its 2 s deadline) and hands a syntax error to the model as `additionalContext`; it is silent when a parser is missing or slow, and inert until `editSyntaxGate.enabled` is true. The syntax gate also runs on Codex, on `PostToolUse` for `apply_patch` (every file the patch adds, updates or moves to). `multi-agent:setup` Step 8 asks once and registers it there with `host-hooks.mjs install --host codex` from `install/templates/codex-hooks.json`: idempotent, leaving every other hook alone, `status` reports it and uninstall removes it. Codex runs it only after the user trusts it in `/hooks`, which the helper says. Copilot CLI gets no registration: as of 1.0.45 it discards a `postToolUse` hook's output and applies no matcher, so the gate would cost a spawn per tool call and say nothing. Every other registration here is Claude Code's. The recommended hook block ships at `install/templates/claude-hooks.json`; `multi-agent:setup` offers to merge it into `~/.claude/settings.json`. The other deterministic gates (evidence, consensus, intent, learnings) are invoked by the pipeline phases with per-run arguments (a build-log path, the triage JSON, the free-text input), so they are phase-enforced by contract, not OS-hookable.
194
194
 
195
195
  Copilot CLI has no `PreToolUse` equivalent, so the secret scan there is workflow-enforced (run as a phase step, not OS-blocked) plus a CI smoke-gate step.
@@ -146,11 +146,11 @@ acted on, not read.
146
146
  #### Accounting is a gate, not a request
147
147
 
148
148
  `update <N> completed` exits **3** for phases 1-4 when nothing was recorded via
149
- `tokens`. The alternative is what shipped for months: the docs asked for the
150
- call, `smoke-tracker-tokens-invocation.sh` linted that the docs *mention* the
151
- call, and no run made it - every phase priced at `-`, every completion line
152
- printed without numbers, and every gate green. A lint over prose cannot observe
153
- a runtime omission; the tool that would have received the call can.
149
+ `tokens`. A doc that asks for the call, and a lint
150
+ (`smoke-tracker-tokens-invocation.sh`) that checks the doc *mentions* it, cannot
151
+ see a run that skips it: every phase would price at `-` and every completion line
152
+ would print without numbers while every gate passes. A lint over prose cannot
153
+ observe a runtime omission; the tool that would have received the call can.
154
154
 
155
155
  The refusal is recoverable and loses no work:
156
156
 
@@ -160,7 +160,7 @@ phase-tracker.sh tokens <N> <in> <out> [cached]
160
160
  phase-tracker.sh update <N> completed
161
161
  ```
162
162
 
163
- `model` resolves a full model id to its cost-table family (`claude-opus-5` →
163
+ `model` resolves a full model id to its cost-table family (`claude-opus-5-5` →
164
164
  `opus`) and says so out loud when it cannot, because an unpriced name is the
165
165
  other way a phase silently costs nothing. A phase that truly ran no LLM call
166
166
  completes with `--no-llm`, which is recorded and keeps it out of the run
@@ -27,8 +27,8 @@
27
27
  "properties": {
28
28
  "id": {
29
29
  "type": "string",
30
- "pattern": "^t[0-9]+$",
31
- "description": "Stable identifier (t1, t2, ...). Used in dependency edges + progress lines."
30
+ "pattern": "^[tT][0-9]+$",
31
+ "description": "Stable identifier: t1, t2 when written directly, T1, T2 when converted from a planning-output document. Used in dependency edges and progress lines."
32
32
  },
33
33
  "task": {
34
34
  "type": "string",
@@ -43,7 +43,7 @@
43
43
  "type": "array",
44
44
  "default": [],
45
45
  "description": "Array of todo IDs that must reach `completed` (or `skipped`) before this one can move to `in_progress`. Empty for parallelizable steps.",
46
- "items": { "type": "string", "pattern": "^t[0-9]+$" }
46
+ "items": { "type": "string", "pattern": "^[tT][0-9]+$" }
47
47
  },
48
48
  "startedAt": { "type": "string", "format": "date-time" },
49
49
  "completedAt": { "type": "string", "format": "date-time" },
@@ -148,7 +148,7 @@
148
148
  "properties": {
149
149
  "because": {
150
150
  "type": "string",
151
- "pattern": "^t[0-9]+$",
151
+ "pattern": "^[tT][0-9]+$",
152
152
  "description": "The step that drifted."
153
153
  },
154
154
  "at": { "type": "string", "format": "date-time" },
@@ -568,7 +568,7 @@
568
568
  "fableEnabled": {
569
569
  "type": "boolean",
570
570
  "default": false,
571
- "description": "Whether the fable rung is available at all on Claude Code. Ships OFF: the top rung is the most expensive thing a run can reach, and a cost control that is on by default is not a control. false makes every persona that declares preferredModel: fable dispatch on opus from the first call, with no dispatch attempt on fable and no error to recover from - a cost control, not a fallback. Claude Code only: Copilot CLI does not offer Fable 5, and on Codex CLI the fable rung means gpt-5.6 @ xhigh, which this switch deliberately does not touch. Turning it off also collapses the Phase 3 Claude Code reviewer panel from three to two (Reviewer 1 lands on opus, which Reviewer 2 already holds), and consensus.reviewerCount records 2."
571
+ "description": "Whether the fable rung is available at all on Claude Code. Ships OFF: the top rung is the most expensive thing a run can reach, and a cost control that is on by default is not a control. false makes every persona that declares preferredModel: fable dispatch on opus from the first call, with no dispatch attempt on fable and no error to recover from - a cost control, not a fallback. Claude Code only: Copilot CLI does not offer Fable, and on Codex CLI the fable rung means gpt-5.6 @ xhigh, which this switch deliberately does not touch. Turning it off also collapses the Phase 3 Claude Code reviewer panel from three to two (Reviewer 1 lands on opus, which Reviewer 2 already holds), and consensus.reviewerCount records 2."
572
572
  },
573
573
  "onDispatchError": {
574
574
  "type": "boolean",
@@ -1211,7 +1211,7 @@
1211
1211
  "clarifyAmbiguous": {
1212
1212
  "type": "object",
1213
1213
  "additionalProperties": false,
1214
- "description": "v8.6+ - Phase 0 Step 9 clarifying-question loop. Before Phase 1 starts, a cheap Haiku classifier scores task ambiguity (0-10) and emits up to N questions if score < threshold. Cost: ~$0.0025 per Haiku call. Off by default - flip on for teams burned by ambiguity-driven rework or when working on cross-team issues where the spec lives in someone else's head.",
1214
+ "description": "v8.6+ - Phase 0 Step 9 clarifying-question loop. Before Phase 1 starts, a cheap Haiku classifier scores task ambiguity (0-10) and emits up to N questions if score < threshold. Cost: ~$0.0004 per Haiku call. Off by default - flip on for teams burned by ambiguity-driven rework or when working on cross-team issues where the spec lives in someone else's head.",
1215
1215
  "properties": {
1216
1216
  "enabled": {
1217
1217
  "type": "boolean",
@@ -1841,7 +1841,7 @@
1841
1841
  "editSyntaxGate": {
1842
1842
  "type": "object",
1843
1843
  "additionalProperties": false,
1844
- "description": "Parse the file an Edit / Write / MultiEdit just wrote and tell the model when it no longer parses: swiftc -parse, node --check, python3 py_compile (bytecode to a temp dir), JSON.parse, bash -n, chosen by extension, each under a 2 s deadline. Kotlin and every other extension are skipped. The hook is pipeline/scripts/edit-syntax-gate.sh (PostToolUse, Claude Code, installed from install/templates/claude-hooks.json). It never blocks: the message arrives as PostToolUse context and the hook exits 0. A parser that is not installed or runs past the deadline means no message.",
1844
+ "description": "Parse the file an edit just wrote and tell the model when it no longer parses: swiftc -parse, node --check, python3 py_compile (bytecode to a temp dir), JSON.parse, bash -n, chosen by extension, each under a 2 s deadline. Kotlin and every other extension are skipped. The hook is pipeline/scripts/edit-syntax-gate.sh: PostToolUse on Claude Code (install/templates/claude-hooks.json) and on Codex apply_patch (codex-hooks.json, registered by host-hooks.mjs). Copilot CLI is not registered: as of 1.0.45 it discards postToolUse output. It never blocks: the message arrives as PostToolUse context and the hook exits 0. A parser that is not installed or runs past the deadline means no message.",
1845
1845
  "properties": {
1846
1846
  "enabled": {
1847
1847
  "type": "boolean",
@@ -2006,7 +2006,7 @@
2006
2006
  },
2007
2007
  "localRepo": {
2008
2008
  "type": "string",
2009
- "description": "Repo that localPath is relative to, e.g. $HOME/multi-agent-plugins. Without it localPath reads as relative to the current repo, which is how both entries came to point at the UPSTREAM tree and stayed wrong through a rename: nothing in the config said which checkout held the derived copies."
2009
+ "description": "Repo that localPath is relative to, e.g. $HOME/multi-agent-plugins. Without it localPath reads as relative to the current repo, so an entry can silently point at the UPSTREAM tree: nothing else in the config says which checkout holds the derived copies."
2010
2010
  },
2011
2011
  "upstreamMarketplace": {
2012
2012
  "type": "string",
@@ -212,8 +212,8 @@ const IMPORT_LINE_RE = /^[ \t]*import[ \t].*$/gm;
212
212
  *
213
213
  * Swift's submodule form - `import class DesignKit.PrimaryButton` - otherwise
214
214
  * feeds the `class` definition pattern, which registers the MODULE name as a
215
- * declared type. On a large Swift app that alone lifted two module names into
216
- * the top ten god-nodes, which is how the bug surfaced. Imports are extracted
215
+ * declared type. On a large Swift app that alone lifts module names into the
216
+ * top ten god-nodes. Imports are extracted
217
217
  * before this runs, so nothing is lost.
218
218
  *
219
219
  * @param {string} stripped - output of stripCode