@mmerterden/multi-agent-pipeline 20.23.2 → 20.25.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 (123) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +8 -6
  3. package/README.tr.md +7 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -6
  7. package/docs/features.md +9 -4
  8. package/install/catalog-history.json +1 -1
  9. package/install/templates/claude-hooks.json +14 -1
  10. package/install/templates/codex-hooks.json +18 -0
  11. package/manifest.json +126 -98
  12. package/package.json +1 -1
  13. package/pipeline/agents/task-clarifier.md +1 -1
  14. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +4 -2
  15. package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
  16. package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
  17. package/pipeline/commands/multi-agent/postmortem/SKILL.md +90 -0
  18. package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
  19. package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
  20. package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
  21. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
  22. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
  23. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  24. package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
  25. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  26. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  27. package/pipeline/contract/fixtures/runs-running.json +1 -1
  28. package/pipeline/lib/analysis-jira-write.sh +70 -0
  29. package/pipeline/lib/credential-inventory.sh +5 -5
  30. package/pipeline/lib/decisions.mjs +130 -0
  31. package/pipeline/lib/fetch-crashlytics.sh +6 -8
  32. package/pipeline/lib/model-rung.sh +1 -1
  33. package/pipeline/lib/plan-todos.sh +177 -37
  34. package/pipeline/lib/post-pr-review.sh +4 -6
  35. package/pipeline/lib/route-state.sh +1 -1
  36. package/pipeline/lib/shadow-git.sh +4 -5
  37. package/pipeline/lib/task-graph.mjs +446 -0
  38. package/pipeline/lib/tdd-evidence.mjs +579 -0
  39. package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
  40. package/pipeline/multi-agent-refs/analysis-template.md +4 -2
  41. package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -11
  42. package/pipeline/multi-agent-refs/features/analysis-jira.md +42 -0
  43. package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
  44. package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
  45. package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
  46. package/pipeline/multi-agent-refs/features/model-fallback.md +21 -14
  47. package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
  48. package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
  49. package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
  50. package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
  51. package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
  52. package/pipeline/multi-agent-refs/help/en.md +2 -0
  53. package/pipeline/multi-agent-refs/help/tr.md +2 -0
  54. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  55. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
  56. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -7
  57. package/pipeline/multi-agent-refs/phases/phase-3-review.md +18 -16
  58. package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
  59. package/pipeline/multi-agent-refs/picker-contract.md +3 -2
  60. package/pipeline/multi-agent-refs/response-shape.md +65 -0
  61. package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
  62. package/pipeline/rules/response-shape.md +18 -0
  63. package/pipeline/schemas/agent-state.schema.json +36 -1
  64. package/pipeline/schemas/phases.json +4 -0
  65. package/pipeline/schemas/plan-todos.schema.json +104 -3
  66. package/pipeline/schemas/planning-output.schema.json +46 -0
  67. package/pipeline/schemas/prefs.schema.json +122 -3
  68. package/pipeline/scripts/_code-graph.mjs +2 -2
  69. package/pipeline/scripts/_jsonl.mjs +151 -0
  70. package/pipeline/scripts/_plan-overlap.mjs +231 -0
  71. package/pipeline/scripts/_redact.mjs +124 -10
  72. package/pipeline/scripts/_transcript-hosts.mjs +406 -0
  73. package/pipeline/scripts/analysis-story-tree.mjs +153 -8
  74. package/pipeline/scripts/autopilot-publish.mjs +12 -1
  75. package/pipeline/scripts/build-stack-plugins.mjs +3 -3
  76. package/pipeline/scripts/capture-flush.sh +7 -7
  77. package/pipeline/scripts/code-graph-rules/android.json +1 -1
  78. package/pipeline/scripts/cost-analyze.mjs +32 -43
  79. package/pipeline/scripts/cost-table.json +35 -19
  80. package/pipeline/scripts/decisions-log.mjs +118 -0
  81. package/pipeline/scripts/dep-bump.mjs +1556 -0
  82. package/pipeline/scripts/edit-syntax-gate.mjs +362 -0
  83. package/pipeline/scripts/edit-syntax-gate.sh +32 -0
  84. package/pipeline/scripts/find-polluter.mjs +281 -0
  85. package/pipeline/scripts/gate-ledger.mjs +46 -13
  86. package/pipeline/scripts/gc-abandoned.sh +2 -2
  87. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
  88. package/pipeline/scripts/git-path.mjs +1 -2
  89. package/pipeline/scripts/host-hooks.mjs +265 -0
  90. package/pipeline/scripts/jira-attach.sh +1 -1
  91. package/pipeline/scripts/keychain.py +3 -3
  92. package/pipeline/scripts/mutation-probe.mjs +484 -0
  93. package/pipeline/scripts/output-quality-check.sh +1 -1
  94. package/pipeline/scripts/phase-tracker.sh +5 -6
  95. package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
  96. package/pipeline/scripts/plan-complexity.mjs +259 -0
  97. package/pipeline/scripts/pre-push-check.sh +3 -6
  98. package/pipeline/scripts/review-decision-gate.mjs +33 -1
  99. package/pipeline/scripts/rework-escalation.mjs +264 -0
  100. package/pipeline/scripts/test-strength.mjs +5 -3
  101. package/pipeline/scripts/transcript-index.mjs +1131 -0
  102. package/pipeline/scripts/uninstall.mjs +90 -4
  103. package/pipeline/scripts/update-issue-progress.sh +4 -6
  104. package/pipeline/scripts/usage-report.mjs +2 -2
  105. package/pipeline/scripts/validate-planning.mjs +48 -6
  106. package/pipeline/skills/.skill-manifest.json +20 -12
  107. package/pipeline/skills/.skills-index.json +23 -1
  108. package/pipeline/skills/shared/README.md +5 -3
  109. package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
  110. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +4 -2
  111. package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
  112. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
  113. package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +69 -0
  114. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
  115. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
  116. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
  117. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
  118. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
  119. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
  120. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
  121. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
  122. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  123. package/pipeline/skills/skills-index.md +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.23.2",
3
+ "version": "20.25.0",
4
4
  "description": "6-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -116,7 +116,7 @@ pipeline/scripts/log-metric.sh "$TASK_ID" 0 clarify.call \
116
116
  tokens_in=$TI tokens_out=$TO
117
117
  ```
118
118
 
119
- Cost expectation on Haiku (input ~1.5k tokens issue body, output ~400 tokens JSON): **~$0.0025 per call**. Off by default; flip on for teams burned by ambiguity-driven rework.
119
+ Cost expectation on Haiku (input ~1.5k tokens issue body, output ~400 tokens JSON): **~$0.0004 per call**. Off by default; flip on for teams burned by ambiguity-driven rework.
120
120
 
121
121
  ## Pattern citation
122
122
 
@@ -29,6 +29,7 @@ Contract, severities and reasoning:
29
29
  - An open `EKLENECEK`/`TBD` marker or an open Section 20 row stops planning with exit 4. Resolve them with `/multi-agent:analysis-resolve` first.
30
30
  - The epic must already exist: stories are linked to it, never an epic created. On Server/Data Center the Epic Link field is discovered from the site; when none is found the stories are created unlinked and the writer says so.
31
31
  - Channel clones (`issueTree.cloneByChannel`) carry their own labels and are searched like stories, so a re-run never doubles them or their links.
32
+ - A requirement row with `Depends on: <ids>` links the stories, prerequisite first, as `issueTree.dependencyLinkType` (default Blocks). A cycle, or a prerequisite no story covers, makes the plan `incomplete`; fix the document, not the links.
32
33
  - Story bodies hold only document text. A body warning (no user story, fewer than 2 criteria, no negative criterion) is a gap in the document; fix the document, not the ticket.
33
34
 
34
35
  ## Phase 1 - Plan, offline
@@ -64,6 +65,7 @@ what makes a preview meaningful:
64
65
  - the coverage verdict **on its own line**, and the consistency verdict below it
65
66
  - every field beside the pref key it came from, so a wrong setting shows here
66
67
  rather than in Jira afterwards
68
+ - the dependency edges between stories, prerequisite first
67
69
  - the write count
68
70
  - every story's full description, as it will be written, with its body
69
71
  warnings; `--brief` gives the one-line-per-node view
@@ -106,7 +108,7 @@ A run that could not be checked is allowed. One that looks checked when it was
106
108
  not is the defect, so the approval has to name it and a plain "Approve" must not
107
109
  be reachable.
108
110
 
109
- `incomplete` prints its uncovered and invented ids before the question. An
111
+ `incomplete` prints its uncovered and invented ids, and any dependency cycle or uncovered prerequisite, before the question. An
110
112
  invented id means the plan cites something the document does not define - treat
111
113
  that as a defect in the plan, not a warning to click past.
112
114
 
@@ -156,7 +158,7 @@ missing-attachment warning.
156
158
 
157
159
  ## Phase 5 - Report
158
160
 
159
- Keys created, keys skipped, the epic linked (or none), the coverage verdict, and the ledger path. If the
161
+ Keys created, keys skipped, the epic linked (or none), the dependency links written, the coverage verdict, and the ledger path. If the
160
162
  verdict was `unverifiable`, say that in the report too - not only at approval
161
163
  time, because the report is what gets pasted elsewhere.
162
164
 
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: "Bump one dependency, fix only what the bump broke, review, open a PR; rolls back past the fix round cap. SwiftPM, Gradle, npm, pip. Use when a library needs updating."
3
+ description-tr: "Tek bağımlılığı yükseltir, yalnızca yükseltmenin kırdığını düzeltir, review edip PR açar; düzeltme turu sınırında geri alır. SwiftPM, Gradle, npm, pip. Bir kütüphaneyi güncellemek için kullanılır."
4
+ argument-hint: "[package[@version]] [--major] - no package opens a picker over the outdated candidates; --major allows a breaking bump"
5
+ parameters: [{"name":"package","kind":"text"},{"name":"--major","kind":"flag"}]
6
+ gui: form
7
+ destructive: false
8
+ confirm: none
9
+ ---
10
+
11
+ # multi-agent deps - bump one dependency and fix what it broke
12
+
13
+ > **Pickers follow** `$HOME/.claude/multi-agent-refs/picker-contract.md`: never a one-option call, and branch on the option selected, not on its text.
14
+
15
+ **Input**: $ARGUMENTS
16
+
17
+ > **Language (read FIRST)**: read `prefs.global.outputLanguage` and render every conversational line in it. `AskUserQuestion` renders `question`, option `label`s and `description`s in `outputLanguage`; only `header` stays English (<=12-char chip). Commit messages and branch names stay English. Full contract: `$HOME/.claude/multi-agent-refs/rules.md` "Language Application".
18
+
19
+ The normal pipeline with a fixed task. Read `$HOME/.claude/multi-agent-refs/features/dep-bump.md` first: it is the contract for every step below (run shape, scope rule, rollback). `dep-bump.mjs` does every deterministic step; never edit a version string or a lockfile by hand.
20
+
21
+ ## Steps
22
+
23
+ 1. **Repo and settings.** Resolve the repo as `/multi-agent` does for a free-text task (`_account-picker.md`, `_repo-picker.md`). Then:
24
+ ```bash
25
+ node "$HOME/.claude/scripts/dep-bump.mjs" config # depBump.maxFixRounds, depBump.queryRegistry
26
+ node "$HOME/.claude/scripts/dep-bump.mjs" detect --dir "$REPO"
27
+ ```
28
+ No ecosystem detected: say so and stop.
29
+
30
+ 2. **Candidates.**
31
+ ```bash
32
+ node "$HOME/.claude/scripts/dep-bump.mjs" list --dir "$REPO" [--registry] # --registry when queryRegistry is true
33
+ ```
34
+ Show `registry.note` and every per-package `note`: an offline listing is reported as offline, never as "nothing outdated".
35
+ - `package@version` given: use it. `package` alone: its `latestInMajor`, else `latest`. Not in the list: stop with the manifests that were read.
36
+ - No package: a single-select picker over the candidates with a newer version, non-breaking first, each option `name current -> target (risk)`. Two or more options per the picker contract; one candidate is confirmed with a yes/no, none means report "nothing newer found" with the registry note.
37
+
38
+ 3. **Major gate.** Risk `major` or `unknown`, or a prerelease target (`prerelease: true`), needs `--major` in the input or an explicit confirmation naming the versions (`Bump anyway` / `Take {latestInMajor} instead` when one exists / `Cancel`). An autopilot run without `--major` takes `latestInMajor` or stops.
39
+
40
+ 4. **Run the pipeline** with `state.mode = "deps"`, `state.taskType = "chore"`, the free-text task `chore(deps): bump {name} from {current} to {target}` and the branch `chore/deps-{name}-{target}` (kebab rules from Phase 0 Step 4). Phase 0 intake, worktree and tracker run as in `/multi-agent`. Then:
41
+ - **Phase 1**: the fixed plan from the feature doc, with the changelog excerpt as evidence:
42
+ ```bash
43
+ node "$HOME/.claude/scripts/dep-bump.mjs" notes --dir "$WORKTREE" --ecosystem E --name N --from A --to B
44
+ ```
45
+ - **Phase 2**: apply first, then the exit gate as written:
46
+ ```bash
47
+ node "$HOME/.claude/scripts/dep-bump.mjs" apply --dir "$WORKTREE" --ecosystem E --name N --to B [--allow-major]
48
+ ```
49
+ Exit 1: report `error` and `lock.output` (or `dirty`: the worktree had uncommitted changes), stop with nothing to commit. Persist the output to `state.depBump`. Green: no dev agent, on to Phase 3. Red: fix rounds, at most `depBump.maxFixRounds`, each dispatched with the failing log tail, `surface` output, the changelog excerpt and the scope rule (feature doc section 4); after each round:
50
+ ```bash
51
+ node "$HOME/.claude/scripts/dep-bump.mjs" scope --dir "$WORKTREE" --log "$GATE_LOG" # the round's build/test log
52
+ ```
53
+ Only the surface recorded by `apply` is in scope. A file listed in `newImporters` with `cited: null` started importing the dependency without an error line behind it: revert it before the next round.
54
+ - **Cap reached and still red** (or still out of scope): `dep-bump.mjs rollback --dir "$WORKTREE"` (restores the manifest, lockfile and fix files; anything under `kept` is reported, not discarded), set `state.depBump.rolledBack = true`, and end with the report from feature doc section 5. No commit, no push, no PR.
55
+ - **Phase 3**: standard review. Each `outOfScope` file without a cited error line, and each `newImporters` entry with `cited: null`, is a blocking finding.
56
+ - **Phase 4**: commit with `commitMessage` from `apply` (plus ` [{jiraId}]` when present). PR body per the feature doc; `Ref:` or `Related:`, never `Closes`, `Fixes` or `Resolves`, and never close an issue.
57
+ - **Phase 5**: channels as in every mode.
58
+
59
+ ## Report
60
+
61
+ In `outputLanguage`, at most 10 lines: the dependency, from and to, risk, lockfile command and result, rounds taken against the cap, files changed (in scope / justified), review verdict, and the PR URL or the rollback reason.
@@ -52,8 +52,8 @@ because it does not mean the same thing on all three:
52
52
 
53
53
  | Host | What `off` does | Why |
54
54
  |---|---|---|
55
- | Claude Code | every `preferredModel: fable` persona (architects, reviewer 1, triage) starts on `opus` | the only host where the rung is Fable 5 |
56
- | Copilot CLI | nothing | Fable 5 is not offered there; its personas never sat on this rung |
55
+ | Claude Code | every `preferredModel: fable` persona (architects, reviewer 1, triage) starts on `opus` | the only host where the rung is Fable 5.1 |
56
+ | Copilot CLI | nothing | Fable is not offered there; its personas never sat on this rung |
57
57
  | Codex CLI | nothing, deliberately | the `fable` rung there means `gpt-5.6 @ xhigh`, a different model on a different account - switching it from a knob named after an Anthropic model would surprise a Codex user |
58
58
 
59
59
  So on two of the three hosts this command is a **status report**, not a switch,
@@ -0,0 +1,90 @@
1
+ ---
2
+ description: "Post-mortem of a run from its session transcripts: loops, stalls, root-cause hypotheses on file:line, fixes. Read-only, redacted. Use when a run went wrong or ran far too long."
3
+ description-tr: "Bir koşunun oturum transkriptlerinden post-mortem'i: ne oldu, nerede döngüye girdi veya takıldı, file:line'a dayalı kök-neden hipotezleri, önerilen düzeltmeler. Salt-okunur; gösterilmeden önce redakte edilir."
4
+ argument-hint: "[#id | transcript.jsonl] [--audit] - a run (task id or #N) or a session file; no argument means the most recent run. --audit re-checks the draft against the index"
5
+ parameters: [{"name":"target","kind":"text"},{"name":"--audit","kind":"flag"}]
6
+ gui: button
7
+ destructive: false
8
+ confirm: none
9
+ ---
10
+
11
+ # multi-agent postmortem - Run Post-Mortem
12
+
13
+ **Input**: $ARGUMENTS
14
+
15
+ Explains what a run actually did, read from the session transcripts it ran in,
16
+ not from what the run log says it decided. The evidence comes from a
17
+ deterministic indexer; the model only interprets it.
18
+
19
+ The indexer reads all three hosts and detects the format per file:
20
+
21
+ | Host | Session files |
22
+ |------|---------------|
23
+ | Claude Code | `~/.claude/projects/<dir>/<session>.jsonl` plus its subagent files |
24
+ | Copilot CLI | `~/.copilot/session-state/<session>/events.jsonl` (cited as `<first 8 of session>/events.jsonl`) |
25
+ | Codex | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` (cited as `rollout-<first 8 of session>.jsonl`) |
26
+
27
+ Copilot CLI reports tokens per session, so its turns carry output tokens only
28
+ and the totals come from the session's shutdown record; Codex reports them per
29
+ request.
30
+
31
+ **Read-only.** Nothing is sent anywhere: no issue comment, no PR, no channel.
32
+ The one write is the report itself, saved under the run's log directory.
33
+
34
+ ## Steps
35
+
36
+ 1. **Resolve the target.**
37
+ - A path ending in `.jsonl` (any of the three hosts): index that file
38
+ (Claude Code subagent files beside it are included). There is no run directory, so nothing is saved unless the user
39
+ names one.
40
+ - A task id or `#N`: locate the run and its transcripts:
41
+ ```bash
42
+ node "$HOME/.claude/scripts/transcript-index.mjs" --run "<id>" --locate
43
+ ```
44
+ `matchedBy: "session"` means the run recorded its session id and the match
45
+ is exact. `"time-window"` means it did not (attended runs record none), so
46
+ the files are the sessions, on any host, that ran in the run's worktree or
47
+ checkout while it was active; say that this match is a heuristic. Exit 3 with a `reason`:
48
+ report the reason, and offer to take a transcript path instead.
49
+ - No argument: take the most recent run from
50
+ `node "$HOME/.claude/scripts/runs-index.mjs" --json` (latest `startedAt`)
51
+ and continue as above.
52
+
53
+ 2. **Index.**
54
+ ```bash
55
+ node "$HOME/.claude/scripts/transcript-index.mjs" --run "<id>" # or: <file.jsonl>
56
+ ```
57
+ The output lists turns, loops (identical tool calls repeated), stalls (gaps
58
+ while a tool or the model was working), compactions, subagents with their
59
+ results, errors and denials, skill and command invocations, the slowest tool
60
+ calls and a timeline. Every item carries `file:line`, the line of the JSONL
61
+ it came from. Exit 4 means the redaction guard found a credential left in the
62
+ output and printed nothing: report that and stop; never fall back to reading
63
+ the transcript directly.
64
+
65
+ Read only the index. Do not open the transcript files: the index is the
66
+ redacted view, the files are not.
67
+
68
+ 3. **Write the post-mortem** in `prefs.global.outputLanguage`, short:
69
+ - **What happened**: the run in five lines or fewer, from the turns and
70
+ timeline.
71
+ - **Where it looped or stalled**: each loop and stall that matters, with its
72
+ citations.
73
+ - **Root-cause hypotheses**: each one a hypothesis, not a verdict, and each
74
+ anchored to at least one `file:line` from the index. A hypothesis with no
75
+ citation is left out.
76
+ - **Suggested fixes**: concrete and tied to a hypothesis (a prompt rule, a
77
+ guard, a timeout, a tool to use instead).
78
+ - **Evidence limits**: the matching method (`session` / `time-window`),
79
+ malformed or truncated lines, and the redaction count the index reported.
80
+
81
+ Quote only excerpts the index already shows; they are redacted.
82
+
83
+ 4. **Audit pass** (`--audit`): re-read the draft against the index. Every cited
84
+ `file:line` must exist in the index and say what the draft claims; every
85
+ count must match the index's counts. Correct or drop what does not hold, and
86
+ add one line: `audit: N claims checked, M corrected`.
87
+
88
+ 5. **Save.** With a run directory, write the report to
89
+ `<runDir>/postmortem.md` (`runDir` from step 1, `~` expanded) and give the
90
+ path. That file is the only write this command makes.
@@ -247,14 +247,14 @@ off: Agent 1 is dropped, the panel is Agent 2 + Agent 3, triage runs on
247
247
  Never dispatch a Fable reviewer or a Fable triage on the model list below alone.
248
248
 
249
249
  **Claude Code (3 in parallel while the fable rung is on, 2 while it is off):**
250
- - Agent 1: `claude-fable-5` → security + architecture (only when `REVIEW_RUNG` is `fable`)
251
- - Agent 2: `claude-opus-5` → edge cases, alternate perspective
252
- - Agent 3: `claude-sonnet-5` → general quality
250
+ - Agent 1: `claude-fable-5-1` → security + architecture (only when `REVIEW_RUNG` is `fable`)
251
+ - Agent 2: `claude-opus-5-5` → edge cases, alternate perspective
252
+ - Agent 3: `claude-sonnet-5-5` → general quality
253
253
 
254
254
  **Copilot CLI (3 in parallel):**
255
- - Agent 1: `claude-opus-5` → security + architecture (Fable 5 is not offered on Copilot CLI)
255
+ - Agent 1: `claude-opus-4.7` → security + architecture (Fable is not offered on Copilot CLI)
256
256
  - Agent 2: `gpt-5.4` → edge cases, alternate perspective
257
- - Agent 3: `claude-sonnet-5` → general quality
257
+ - Agent 3: `claude-sonnet-4.6` → general quality
258
258
 
259
259
  **Codex CLI (3 in parallel, single vendor):**
260
260
  - Agent 1: `gpt-5.6` @ `xhigh` → security + architecture
@@ -30,7 +30,7 @@ honoured only at `bulk-read` and `delegate` and only while `--free` is on.
30
30
 
31
31
  **Rung names are the contract; model ids are not.** `opus` is a rung here and a
32
32
  model id in `cost-table.json`, and the second can change without anyone editing
33
- a rule. Writing `claude-opus-5` into a rule pins a decision to a string that will
33
+ a rule. Writing `claude-opus-5-5` into a rule pins a decision to a string that will
34
34
  go stale.
35
35
 
36
36
  Set rules with:
@@ -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 three capture hooks that block nothing (`PreCompact`, `SessionEnd`, `SessionStart`). 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
 
@@ -73,7 +73,7 @@ Run every step automatically:
73
73
  ```
74
74
  Step 0: DOCTOR doctor.mjs - exit 2 or 4 stops the sync
75
75
  Step 1.5: DETECT Compare timestamps, find stale targets
76
- Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 64 sub-command skills)
76
+ Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 66 sub-command skills)
77
77
  Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + sub-command specs as refs + agent TOML)
78
78
  Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
79
79
  Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
@@ -241,14 +241,14 @@ same specs as reference files rather than as peer skills, via Step 2b - see
241
241
  |-------------|-------------|
242
242
  | `~/.claude/commands/multi-agent/{cmd}/SKILL.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
243
243
 
244
- **64 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
244
+ **66 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
245
245
 
246
246
  ```
247
247
  analysis, analysis-jira, analysis-resolve, autopilot, autopilot-off, autopilot-on,
248
248
  autopilot-status, bug-bash, build-optimize, channels, complaint-analysis, create-jira,
249
- design-check, diff-explain, doctor, estimate, feedback, forget, garbage-collect, graph, help,
249
+ deps, design-check, diff-explain, doctor, estimate, feedback, forget, garbage-collect, graph, help,
250
250
  ios-coding-standard, issue, jira, kill, language, log,
251
- manual-test, model, prune-logs, prune-prompts, purge, refactor, research, resume, review, review-analysis, review-issue, review-jira, route-off, route-on, route-status,
251
+ manual-test, model, postmortem, prune-logs, prune-prompts, purge, refactor, research, resume, review, review-analysis, review-issue, review-jira, route-off, route-on, route-status,
252
252
  routines, save, scaffold, scan, scenario-audit, search, security-review, serve, setup, stack, status, steer, store-ready, sync, test,
253
253
  test-accessibility, test-dark-mode, test-dynamic-type, test-screenshots,
254
254
  testflight-validation, uninstall, update
@@ -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",
@@ -34,6 +34,13 @@
34
34
  # Jira has no backup path for fields other than description. Rewriting a body an
35
35
  # engineer has since edited would lose that edit with no way back.
36
36
  #
37
+ # A STORY DEPENDENCY IS A LINK, WRITTEN LAST
38
+ #
39
+ # A plan node's `dependsOn` names the stories it waits for. Once every story has
40
+ # a key, each edge becomes an issue link of `prefsUsed.dependencyLinkType`
41
+ # (default Blocks) from the prerequisite to the dependent, checked before it is
42
+ # sent so a re-run adds none.
43
+ #
37
44
  # Usage:
38
45
  # analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE] [--epic KEY]
39
46
  # analysis-jira-write.sh --subtask-types --project KEY the site's sub-task type names, one per line
@@ -202,6 +209,40 @@ link_issues() { # link_issues <type> <fromKey> <toKey> <cloneLabel>
202
209
  echo " linked $from <-> $to"
203
210
  }
204
211
 
212
+ # A dependency link: the prerequisite is the link's source (the POST's
213
+ # inwardIssue), so the site reads "<prerequisite> blocks <dependent>" for the
214
+ # default Blocks type, and lists the link on the prerequisite under outwardIssue.
215
+ # Only a link of this type in this direction counts as already there: a Relates
216
+ # link a person added between the same two stories is not the dependency.
217
+ link_dependency() { # link_dependency <type> <prerequisiteKey> <dependentKey> <dependentLabel>
218
+ local ltype="$1" pre="$2" dep="$3" label="$4" linked
219
+ linked="$(jira_api GET "/rest/api/2/issue/${pre}?fields=issuelinks" 2>/dev/null \
220
+ | jq -r --arg t "$ltype" --arg d "$dep" \
221
+ '[.fields.issuelinks[]? | select(.type.name == $t and .outwardIssue.key == $d)] | length > 0' \
222
+ 2>/dev/null || echo false)"
223
+ if [ "$linked" = "true" ]; then
224
+ echo " linked $pre -> $dep ($ltype)"
225
+ return 0
226
+ fi
227
+ if ! printf '%s' "$(jq -cn --arg t "$ltype" --arg a "$pre" --arg b "$dep" \
228
+ '{type: {name: $t}, inwardIssue: {key: $a}, outwardIssue: {key: $b}}')" \
229
+ | jira_api POST "/rest/api/2/issueLink" --data @- >/dev/null; then
230
+ echo "ERR: could not link $pre to $dep" >&2
231
+ return 5
232
+ fi
233
+ ledger dependency "$label" "$pre"
234
+ echo " linked $pre -> $dep ($ltype)"
235
+ }
236
+
237
+ declare -a STORY_KEYS=()
238
+ story_key() { # story_key <label> -> prints the key this run created or found, or empty
239
+ local i
240
+ for i in "${STORY_KEYS[@]:-}"; do
241
+ case "$i" in "$1="*) printf '%s' "${i#*=}"; return 0 ;; esac
242
+ done
243
+ printf ''
244
+ }
245
+
205
246
  CREATED_KEY=""
206
247
  create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description> [extraFieldsJson]
207
248
  local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" extra="${6:-}" body key existing
@@ -290,6 +331,7 @@ while IFS= read -r node; do
290
331
  continue
291
332
  fi
292
333
  skey="$CREATED_KEY"
334
+ [ -n "$skey" ] && STORY_KEYS+=("$s_label=$skey")
293
335
  LINK_TYPE="$(jq -r '.prefsUsed.cloneLinkType // "Relates"' "$PLAN")"
294
336
  while IFS= read -r clone; do
295
337
  [ -n "$clone" ] || continue
@@ -319,4 +361,32 @@ while IFS= read -r node; do
319
361
  done < <(printf '%s' "$node" | jq -c '.subtasks[]?')
320
362
  done <<< "$NODES"
321
363
 
364
+ # Dependencies, once every story has a key. An edge inside a strongly connected
365
+ # component (`onCycle`, set by the planner from task-graph.mjs) is not linked:
366
+ # a cycle has no order to record, and the plan already reported it.
367
+ DEP_TYPE="$(jq -r '.prefsUsed.dependencyLinkType // "Blocks"' "$PLAN")"
368
+ EDGES="$(jq -r '(.nodes | map({(.label): .title}) | add // {}) as $t
369
+ | .dependencies.edges[]? | .prerequisite as $p | .dependent as $d
370
+ | [$p, $d, $t[$p], $t[$d], (if .onCycle == true then "cycle" else "" end)]
371
+ | map(. // "" | tostring | gsub("[\n\r\u001f]"; " "))
372
+ | join("\u001f")' "$PLAN")"
373
+ # \x1f, unlike a tab, is not IFS whitespace, so an empty field stays a field.
374
+ while IFS=$'\x1f' read -r p_label d_label p_title d_title on_cycle; do
375
+ [ -n "$p_label" ] || continue
376
+ if [ "$on_cycle" = "cycle" ]; then
377
+ echo " skipped link $p_label ($p_title) -> $d_label ($d_title): the two stories are on a dependency cycle"
378
+ continue
379
+ fi
380
+ if [ "$DRY" -eq 1 ]; then
381
+ echo " link $DEP_TYPE $p_label ($p_title) -> $d_label ($d_title) (prerequisite -> dependent)"
382
+ continue
383
+ fi
384
+ p_key="$(story_key "$p_label")"; d_key="$(story_key "$d_label")"
385
+ if [ -z "$p_key" ] || [ -z "$d_key" ]; then
386
+ echo " skipped link $p_label ($p_title) -> $d_label ($d_title): one of the stories was not created" >&2
387
+ continue
388
+ fi
389
+ link_dependency "$DEP_TYPE" "$p_key" "$d_key" "$d_label" || RC=5
390
+ done <<< "$EDGES"
391
+
322
392
  exit "$RC"
@@ -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:
@@ -0,0 +1,130 @@
1
+ /**
2
+ * decisions.mjs - the record of what an autopilot run decided in place of a
3
+ * person, and its rendering.
4
+ *
5
+ * An attended run asks: a picker, a park, a waived finding, a model change are
6
+ * each a question with an answer someone gave. An autopilot run answers them
7
+ * itself. `state.decisions[]` holds one entry per such answer, so the person
8
+ * who reads the PR or the Phase 5 report sees every place the run chose for
9
+ * them, why, and what it costs if the choice was wrong:
10
+ *
11
+ * { what, why, costIfWrong, at, phase }
12
+ *
13
+ * The entries are written from text the run composed (gate reasons, finding
14
+ * locations, picker labels), so every field is passed through redact.mjs
15
+ * before it is capped, and the rendered section again before it reaches any
16
+ * outward text. The caller still runs the
17
+ * outbound gate over the whole body it publishes; redaction here only keeps a
18
+ * decision's wording from being the reason a publish is refused.
19
+ *
20
+ * Pure functions; the writers that hold the state lock are gate-ledger.mjs
21
+ * (park) and scripts/decisions-log.mjs (everything else).
22
+ *
23
+ * @module pipeline/lib/decisions
24
+ */
25
+
26
+ import { redact } from "./redact.mjs";
27
+
28
+ export const DECISIONS_HEADING = "## Decisions made on your behalf";
29
+
30
+ const FIELD_MAX = { what: 200, why: 500, costIfWrong: 300 };
31
+ const MIN_PHASE = 0;
32
+ const MAX_PHASE = 5;
33
+ // Room for the heading's blank line and one "... N more" line.
34
+ const MIN_ROOM = 48;
35
+
36
+ /**
37
+ * `value` on one line, redacted, then capped at `max`. Redaction comes first:
38
+ * a token cut at the boundary is too short for its rule to recognise, and its
39
+ * prefix would survive.
40
+ */
41
+ function oneLine(value, max) {
42
+ const text = redact(
43
+ String(value ?? "")
44
+ .replace(/\s+/g, " ")
45
+ .trim(),
46
+ );
47
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
48
+ }
49
+
50
+ /**
51
+ * A validated entry. Throws when a field is missing: an entry that cannot say
52
+ * what was decided, why, or what being wrong costs is not worth showing.
53
+ *
54
+ * @param {{what: string, why: string, costIfWrong: string, phase: number, at?: string}} input
55
+ * @returns {{what: string, why: string, costIfWrong: string, at: string, phase: number}}
56
+ */
57
+ export function decisionEntry({ what, why, costIfWrong, phase, at } = {}) {
58
+ const out = {
59
+ what: oneLine(what, FIELD_MAX.what),
60
+ why: oneLine(why, FIELD_MAX.why),
61
+ costIfWrong: oneLine(costIfWrong, FIELD_MAX.costIfWrong),
62
+ };
63
+ for (const [k, v] of Object.entries(out)) {
64
+ if (!v) throw new Error(`a decision needs a non-empty ${k}`);
65
+ }
66
+ const n = Number(phase);
67
+ if (!Number.isInteger(n) || n < MIN_PHASE || n > MAX_PHASE) {
68
+ throw new Error(`a decision needs a phase between ${MIN_PHASE} and ${MAX_PHASE}`);
69
+ }
70
+ const stamp = typeof at === "string" && !Number.isNaN(Date.parse(at)) ? at : null;
71
+ return { ...out, at: stamp || new Date().toISOString(), phase: n };
72
+ }
73
+
74
+ /** The state with `entry` appended to `decisions[]`; `state` is not changed. */
75
+ export function withDecision(state, entry) {
76
+ const list = Array.isArray(state?.decisions) ? state.decisions : [];
77
+ return { ...state, decisions: [...list, entry] };
78
+ }
79
+
80
+ function line(d) {
81
+ const phase = Number.isInteger(d?.phase) ? `Phase ${d.phase}` : "Phase ?";
82
+ return redact(
83
+ `- **${phase}** - ${oneLine(d?.what, FIELD_MAX.what)}. Why: ${oneLine(d?.why, FIELD_MAX.why)}. If wrong: ${oneLine(d?.costIfWrong, FIELD_MAX.costIfWrong)}.`,
84
+ );
85
+ }
86
+
87
+ /**
88
+ * The markdown section, or "" for an empty list: a run that decided nothing
89
+ * for anyone adds nothing to the report.
90
+ *
91
+ * @param {object[]|undefined} decisions
92
+ * @param {{maxChars?: number}} [opts] trim to this many characters, oldest first kept
93
+ */
94
+ export function renderDecisions(decisions, { maxChars = Infinity } = {}) {
95
+ const list = Array.isArray(decisions) ? decisions.filter((d) => d && typeof d === "object") : [];
96
+ if (!list.length) return "";
97
+ const head = `${DECISIONS_HEADING}\n\n`;
98
+ const lines = [];
99
+ let size = head.length;
100
+ for (let i = 0; i < list.length; i++) {
101
+ const l = `${line(list[i])}\n`;
102
+ const rest = list.length - i - 1;
103
+ const tail = rest ? `- ... ${rest + 1} more in the run report\n`.length : 0;
104
+ if (size + l.length + tail > maxChars) {
105
+ lines.push(`- ... ${list.length - i} more in the run report\n`);
106
+ break;
107
+ }
108
+ lines.push(l);
109
+ size += l.length;
110
+ }
111
+ return head + lines.join("");
112
+ }
113
+
114
+ /**
115
+ * `body` with the decisions section appended, once. A body that already holds
116
+ * the heading is returned unchanged; `maxChars` bounds the whole result.
117
+ *
118
+ * @param {string} body
119
+ * @param {object[]|undefined} decisions
120
+ * @param {{maxChars?: number}} [opts]
121
+ */
122
+ export function appendDecisionsSection(body, decisions, { maxChars = Infinity } = {}) {
123
+ const text = typeof body === "string" ? body : "";
124
+ if (text.includes(DECISIONS_HEADING)) return text;
125
+ const sep = text.endsWith("\n") ? "\n" : "\n\n";
126
+ const room = maxChars - text.length - sep.length;
127
+ const section = renderDecisions(decisions, { maxChars: room });
128
+ if (!section || room < DECISIONS_HEADING.length + MIN_ROOM) return text;
129
+ return `${text}${sep}${section}`;
130
+ }
@@ -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