@mmerterden/multi-agent-pipeline 20.23.1 → 20.24.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 (74) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +7 -6
  3. package/README.tr.md +6 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +3 -3
  7. package/install/catalog-history.json +1 -1
  8. package/install/templates/claude-hooks.json +14 -1
  9. package/manifest.json +77 -56
  10. package/package.json +1 -1
  11. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +11 -2
  12. package/pipeline/commands/multi-agent/postmortem/SKILL.md +78 -0
  13. package/pipeline/commands/multi-agent/setup/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  15. package/pipeline/lib/analysis-jira-write.sh +105 -22
  16. package/pipeline/lib/decisions.mjs +130 -0
  17. package/pipeline/lib/plan-todos.sh +156 -33
  18. package/pipeline/lib/task-graph.mjs +446 -0
  19. package/pipeline/lib/tdd-evidence.mjs +579 -0
  20. package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
  21. package/pipeline/multi-agent-refs/analysis-template.md +4 -2
  22. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
  23. package/pipeline/multi-agent-refs/features/analysis-jira.md +51 -3
  24. package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
  25. package/pipeline/multi-agent-refs/features/model-fallback.md +4 -2
  26. package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
  27. package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
  28. package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
  29. package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
  30. package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
  31. package/pipeline/multi-agent-refs/generate-issue.md +1 -1
  32. package/pipeline/multi-agent-refs/help/en.md +1 -0
  33. package/pipeline/multi-agent-refs/help/tr.md +1 -0
  34. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
  35. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +5 -7
  36. package/pipeline/multi-agent-refs/phases/phase-3-review.md +13 -11
  37. package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
  38. package/pipeline/multi-agent-refs/picker-contract.md +3 -2
  39. package/pipeline/multi-agent-refs/response-shape.md +65 -0
  40. package/pipeline/rules/response-shape.md +18 -0
  41. package/pipeline/schemas/agent-state.schema.json +31 -1
  42. package/pipeline/schemas/plan-todos.schema.json +101 -0
  43. package/pipeline/schemas/planning-output.schema.json +46 -0
  44. package/pipeline/schemas/prefs.schema.json +117 -1
  45. package/pipeline/scripts/_jsonl.mjs +151 -0
  46. package/pipeline/scripts/_plan-overlap.mjs +231 -0
  47. package/pipeline/scripts/_redact.mjs +124 -10
  48. package/pipeline/scripts/analysis-story-body.mjs +26 -10
  49. package/pipeline/scripts/analysis-story-tree.mjs +210 -9
  50. package/pipeline/scripts/autopilot-publish.mjs +12 -1
  51. package/pipeline/scripts/cost-analyze.mjs +6 -24
  52. package/pipeline/scripts/decisions-log.mjs +118 -0
  53. package/pipeline/scripts/edit-syntax-gate.mjs +240 -0
  54. package/pipeline/scripts/edit-syntax-gate.sh +29 -0
  55. package/pipeline/scripts/find-polluter.mjs +281 -0
  56. package/pipeline/scripts/gate-ledger.mjs +46 -13
  57. package/pipeline/scripts/mutation-probe.mjs +484 -0
  58. package/pipeline/scripts/plan-complexity.mjs +259 -0
  59. package/pipeline/scripts/review-decision-gate.mjs +33 -1
  60. package/pipeline/scripts/rework-escalation.mjs +264 -0
  61. package/pipeline/scripts/test-strength.mjs +5 -3
  62. package/pipeline/scripts/transcript-index.mjs +950 -0
  63. package/pipeline/scripts/validate-planning.mjs +48 -6
  64. package/pipeline/skills/.skill-manifest.json +11 -7
  65. package/pipeline/skills/.skills-index.json +12 -1
  66. package/pipeline/skills/shared/README.md +4 -3
  67. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +11 -2
  68. package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +66 -0
  69. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
  70. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  71. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
  72. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
  73. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
  74. package/pipeline/skills/skills-index.md +2 -1
@@ -10,6 +10,7 @@
10
10
  - [The story body is document text, escaped](#the-story-body-is-document-text-escaped)
11
11
  - [An existing epic, linked the way the site links](#an-existing-epic-linked-the-way-the-site-links)
12
12
  - [Channel clones are opt-in](#channel-clones-are-opt-in)
13
+ - [Story dependencies come from the document](#story-dependencies-come-from-the-document)
13
14
  - [The document lists its tickets](#the-document-lists-its-tickets)
14
15
  - [Every site-specific name is a VALUE, never a schema key](#every-site-specific-name-is-a-value-never-a-schema-key)
15
16
  - [Auth](#auth)
@@ -122,6 +123,9 @@ Each part sits in a coloured wiki panel with its `h3.` heading first: the user
122
123
  story in `{panel:borderColor=#d4d39e}`, the acceptance criteria in
123
124
  `{panel:borderColor=#d4a19e}`, current vs target and links in
124
125
  `{panel:borderColor=#9eb6d4}`. The `Sources:` line stays outside the panels.
126
+ `issueTree.storyPanels` replaces any of the three colours (`story`, `criteria`,
127
+ `other`), or turns the panels off with `false`; `/multi-agent:create-jira`
128
+ reads the same setting.
125
129
 
126
130
  At most 8 criteria. What is missing stays missing and becomes a body warning
127
131
  (no user story, fewer than 2 criteria, no negative criterion); nothing is filled
@@ -149,6 +153,46 @@ gets one clone per channel: its own label (the story's identity plus
149
153
  the label search, and a link is checked before it is sent, so a re-run doubles
150
154
  neither.
151
155
 
156
+ ## Story dependencies come from the document
157
+
158
+ A requirement row may carry `Depends on: <ids>` (an extra trailing column, or
159
+ appended to any cell of that row): `| BR-pay-01 | ... | Depends on: BR-cart-01 |`.
160
+ The row's own id is the dependent, the ids after the clause its prerequisites.
161
+ The clause is machine syntax, so it stays English in a Turkish document, as
162
+ `THE SYSTEM SHALL` and Given/When/Then do. Only clauses inside a table row are
163
+ read, and a clause naming no requirement id is left as prose.
164
+
165
+ The planner reads the clauses first and removes them before it looks for ids,
166
+ because an id is defined by where it appears: a reference left in place would
167
+ define the id it points at, and a dependency on a requirement nobody wrote would
168
+ plan a story for it instead of being reported.
169
+
170
+ | Clause | Plan |
171
+ |---|---|
172
+ | both ids in one story | no edge |
173
+ | ids in two stories | the dependent story's `dependsOn` gets the prerequisite's label, and `dependencies.edges[]` names the pair of ids behind it |
174
+ | prerequisite id in no story | `dependencies.missing[]`, coverage `incomplete` |
175
+ | stories that wait for each other | `dependencies.cycles[]`, one per strongly connected component (from `lib/task-graph.mjs`); every edge inside one gets `onCycle: true`; coverage `incomplete` |
176
+
177
+ The preview prints each edge, its requirement pair and each cycle
178
+ prerequisite first, and the problems under the
179
+ coverage line, as `coverage.dependencyProblems`; an incomplete verdict they
180
+ cause carries them in `coverage.reason`. Its heading counts issues and links
181
+ apart (`N issue(s) and M link(s)`); `writeCount` is their sum, without the
182
+ edges on a cycle. A document with no clause plans
183
+ exactly as it did before: no `dependsOn`, no `dependencies`, and
184
+ `prefsUsed.dependencyLinkType` only when the site set it.
185
+
186
+ The writer links the stories after all of them have keys, one link per edge, of
187
+ `issueTree.dependencyLinkType` (default `Blocks`). The prerequisite is the
188
+ link's source - the POST's `inwardIssue` - so the site reads "prerequisite
189
+ blocks dependent" and lists the link on the prerequisite under `outwardIssue`.
190
+ That entry, same type and same direction, is what the writer checks for before
191
+ sending, so a re-run adds nothing; a link of another type between the same two
192
+ stories does not count. A written link is a `dependency` line in the ledger
193
+ (the dependent's label, the prerequisite's key). An edge with `onCycle` is not
194
+ linked, and the writer says so.
195
+
152
196
  ## The document lists its tickets
153
197
 
154
198
  After the write, `analysis-tickets-writeback.mjs` puts a `## Tickets` table at
@@ -169,7 +213,11 @@ are values the site fills in:
169
213
  channel, the value is the site's own name for it
170
214
  - `subtaskRoles` ships **empty**. An empty list is an instruction to look at how
171
215
  this board actually splits work; a ready-made list would be the guess most
172
- worth avoiding
216
+ worth avoiding. An attended run asks once, offering the site's own sub-task
217
+ types (`analysis-jira-write.sh --subtask-types`), and saves the answer
218
+ (`analysis-story-tree.mjs --save-subtasks`): each chosen type becomes a role
219
+ mapped to itself, and "no sub-tasks" sets `subtaskRolesDeclined`
220
+ - `cloneLinkType` and `dependencyLinkType` are the site's own link type names
173
221
  - `epicLinkFieldId`, when set, is the site's own field id; otherwise it is
174
222
  discovered
175
223
  - `subtaskIssueType: null` means discover it - `createmeta` returns whichever
@@ -178,8 +226,8 @@ are values the site fills in:
178
226
  Center 9 answers, then the classic form). Same rule as
179
227
  `features/jira-context.md`: the type travels as it comes from Jira and is
180
228
  never a matching criterion in code
181
- - a site with several sub-task types (Analysis Task, Development Task, Test
182
- Task) names them per role: `subtaskIssueType` as a map from a `subtaskRoles`
229
+ - a site with several sub-task types (research, build and QA sub-tasks)
230
+ names them per role: `subtaskIssueType` as a map from a `subtaskRoles`
183
231
  entry to the type, `"*"` for any other role. Without one the first discovered
184
232
  type is used and the run prints the types it found
185
233
 
@@ -0,0 +1,34 @@
1
+ # Feature: Decisions made on your behalf (autopilot)
2
+
3
+ **Pattern**: an attended run asks; an autopilot run answers its own questions. Every such answer is one entry in `state.decisions[]`, so whoever reads the PR or the Phase 5 report sees each place the run chose for them, why, and what it costs if the choice was wrong:
4
+
5
+ ```json
6
+ { "what": "Took the default base branch develop", "why": "no remembered branch inside the TTL",
7
+ "costIfWrong": "the PR targets the wrong base and is retargeted", "at": "<ISO-8601>", "phase": 0 }
8
+ ```
9
+
10
+ Written only while the quality gates are active (`lib/unattended.mjs` `gatesActive`: autopilot, or `MULTI_AGENT_UNATTENDED=1`). In an attended run the person answered, so nothing is recorded. Shape and rendering: `lib/decisions.mjs`; schema: `agent-state.schema.json` `decisions`.
11
+
12
+ ## Writers
13
+
14
+ | Decision | Written by |
15
+ |---|---|
16
+ | Park (`awaiting-answer`, `verification-failed`) | `gate-ledger.mjs park`, in the same locked write as the park |
17
+ | Blocker lowered to important (waived finding) | `review-decision-gate.mjs`, one entry per downgrade |
18
+ | Rework dispatched one rung up | `rework-escalation.mjs --state` (`features/rework-escalation.md`) |
19
+ | Picker resolved without asking (`remembered` / `default` / `derived`) | the picker step, through `decisions-log.mjs add` (`picker-contract.md`) |
20
+ | Split / unverified consensus taken on the triage verdict | Phase 3 Step 4, through `decisions-log.mjs add` |
21
+ | Date-gate model fallback taken without the question | Phase 0 Step 0, through `decisions-log.mjs add` (`features/model-fallback.md`) |
22
+
23
+ ```bash
24
+ node $HOME/.claude/scripts/decisions-log.mjs add --state "$STATE_FILE" --phase <0-5> \
25
+ --what "<what was chosen>" --why "<the rule or evidence that chose it>" \
26
+ --cost-if-wrong "<what a person has to undo if it was the wrong call>"
27
+ ```
28
+
29
+ Exit 0 written or skipped (attended), 2 a missing field, 3 no state. A failed add never stops the run; log `decisions.add_failed`.
30
+
31
+ ## Where it is shown
32
+
33
+ - **Phase 5 report**: `decisions-log.mjs render --state "$STATE_FILE"` inlined verbatim in `agent-log.md`. It prints its own `## Decisions made on your behalf` heading, and nothing at all for an empty list.
34
+ - **Autopilot PR body**: `autopilot-publish.mjs` appends the same section to the run's PR body before the outbound gate checks the title and body. The entries are passed through `lib/redact.mjs` when rendered, so a credential that reached a gate reason is replaced rather than published; the outbound gate still refuses a body that carries one anywhere else. A body that already holds the heading is not given a second one, and the section is trimmed to the 65536-character body limit with a pointer to the run report.
@@ -174,7 +174,8 @@ prints it once instead:
174
174
  `WARN: premium tier plan window ended <date>; preferredModel personas will dispatch on <fallbackModel>. Set prefs.global.modelFallback.premiumTierUntil to null to keep the top tier on usage credits.`
175
175
  Then export the override for every `preferredModel` persona this run. Interactive modes
176
176
  may instead ask once (`Keep top tier (usage credits) / Fall back to <model>`);
177
- autopilot never asks - it falls back and logs.
177
+ autopilot never asks - it falls back, logs, and lists the fallback in `state.decisions[]`
178
+ (`features/autopilot-decisions.md`).
178
179
  2. **Dispatch error (per dispatch).** On a model-unavailable / quota error from a
179
180
  top-tier dispatch and `onDispatchError: true`: retry the same persona once with
180
181
  `PHASE_MODEL_OVERRIDE=<fallbackModel>`. If that retry also fails with a dispatch
@@ -244,7 +245,8 @@ better internals.
244
245
  ## Non-goals
245
246
 
246
247
  - No automatic re-upgrade mid-run (a run that fell back stays fallen back -
247
- deterministic over clever).
248
+ deterministic over clever). Rework escalation (`features/rework-escalation.md`)
249
+ moves one rework dispatch up a rung and refuses to on a run that fell back.
248
250
  - No edits to `pipeline/agents/*.md` at runtime; frontmatter is install-time
249
251
  configuration only.
250
252
  - Copilot CLI reviewer set is out of scope: Copilot CLI pins its own three
@@ -0,0 +1,58 @@
1
+ # Mutation probe (Phase 3 Step 1.763, opt-in)
2
+
3
+ A passing suite says the changed code runs, not that a test checks what it
4
+ decides. `mutation-probe.mjs` inverts a few changed lines one at a time and
5
+ reruns the tests; a mutant that still passes marks a line no test pins down.
6
+
7
+ Runs only when `prefs.global.mutationProbe.enabled` is true, and only when the
8
+ stack adapter names a test command.
9
+
10
+ ```bash
11
+ MP_FILE="$WORKTREE/.pipeline/mutation-probe-$ITERATION.json"
12
+ STACK=$(node $HOME/.claude/scripts/_stack-adapter.mjs resolve --dir "$WORKTREE" | cut -d' ' -f1)
13
+ TEST_CMD=$(node $HOME/.claude/scripts/_stack-adapter.mjs command --dir "$WORKTREE" --stack "$STACK" --kind test)
14
+ [ -n "$TEST_CMD" ] && node $HOME/.claude/scripts/mutation-probe.mjs --repo "$WORKTREE" \
15
+ --base "origin/$BASE_BRANCH" --head worktree --test-cmd "$TEST_CMD" --out "$MP_FILE"
16
+ ```
17
+
18
+ `--max-mutants` and `--timeout-sec` default to `mutationProbe.maxMutants` (5)
19
+ and `mutationProbe.timeoutSec` (120 per test run). Each mutant costs one full
20
+ test run, so the bound is the cost bound.
21
+
22
+ ## Mutations
23
+
24
+ Textual, single-site, and only on lines the diff added or changed (test files
25
+ excluded). The operator needs whitespace on both sides, so generics, arrows and
26
+ unary signs are left alone; comment lines and string contents are skipped.
27
+
28
+ | Kind | Change |
29
+ |---|---|
30
+ | comparison | `==` `!=` `===` `!==` `<` `<=` `>` `>=` flipped to the opposite |
31
+ | boolean | `true`/`false`, `True`/`False` negated |
32
+ | arithmetic | binary `+` and `-` swapped |
33
+ | return | `return <number>`: 0 becomes 1, anything else 0 |
34
+
35
+ Mutants are spread across lines: one per changed line in diff order before any
36
+ line gets a second.
37
+
38
+ ## Isolation
39
+
40
+ `--head worktree` snapshots the uncommitted tree through a private index (the
41
+ caller's index, HEAD and refs are untouched), and every mutant is applied in a
42
+ detached worktree under the system temp directory, removed and pruned on exit,
43
+ SIGINT/SIGTERM included. The checkout is never written.
44
+
45
+ ## Output and merge
46
+
47
+ | Exit | Meaning | `$MP_FILE` |
48
+ |---|---|---|
49
+ | 0 | every mutant killed or timed out | `findings: []` |
50
+ | 1 | at least one survivor | one finding per survivor |
51
+ | 4 | nothing judged: no mutable changed line, or the tests fail before mutation | `findings: []`, `reason` set |
52
+ | 2 | usage or setup error | not written; the step is skipped |
53
+
54
+ A finding is reviewer-shaped (`severity: important`, `file`, `line`, `issue`,
55
+ `fix`) plus `mutation`, `testCommand` and `tag: mutation_probe`. Step 3.0
56
+ appends `$MP_FILE` with the other deterministic findings; triage keeps a
57
+ survivor unless the line is genuinely untestable here (logging, a default no
58
+ caller observes) and says so.
@@ -0,0 +1,67 @@
1
+ # Feature: Plan checks (Phase 1, after the plan is written)
2
+
3
+ Two deterministic checks run on `$WORKTREE/.pipeline/plan.json` before the approval gate: the overlap check inside `validate-planning.mjs`, and the complexity score from `plan-complexity.mjs`.
4
+
5
+ ## Overlap check
6
+
7
+ **Pattern**: a plan contradicts itself at the seams between tasks, not inside one. Two tasks that both create the same file, a rename in one task while another still calls the old name, a consumer that can start before its producer: each task reads fine alone. The check is deterministic and runs inside `validate-planning.mjs`, so it fires on every plan with no extra step.
8
+
9
+ ### What a task may state
10
+
11
+ Optional per-task lists in `planning-output.schema.json`, next to `files[]`:
12
+
13
+ | Field | Holds | Role it gives the task |
14
+ |-------|-------|------------------------|
15
+ | `creates` | files the task brings into existence | `creates` |
16
+ | `removes` | files or symbols deleted, or the old name of a rename (a file: a `/`, a known extension, a well-known name such as `Makefile`, or a dotfile; else a symbol, `UserService.fetch` included) | `removes` |
17
+ | `produces` | interfaces / symbols introduced or reshaped | `produces` |
18
+ | `consumes` | interfaces / symbols used unchanged | `consumes` |
19
+
20
+ A file listed only in `files[]` gives the role `edits`. Fill the lists when Step 8 decided the approach (New file / Modify / Refactor): a New-file task lists its file under `creates`, a rename lists the old name under `removes` and the new one under `produces`, a task that calls an interface another task adds lists it under `consumes`. A plan without them still gets the shared-file rows below. `plan-todos.sh set` carries `creates` onto each todo next to `files[]`, and the per-step drift check (`features/plan-todos.md`, Drift propagation) counts both as planned, along with the test file named after a planned file (`a.test.ts` for `a.ts`, `FooTests.swift` for `Foo.swift`).
21
+
22
+ ### Output
23
+
24
+ `validate-planning.mjs` adds two fields to its result: `overlaps[]`, one anchored row per pair of tasks naming the same file or symbol, and `warnings[]`, one sentence per suspicious row.
25
+
26
+ ```json
27
+ { "a": "T1", "b": "T3", "kind": "file", "name": "Sources/FareService.swift",
28
+ "aRole": "removes", "bRole": "edits", "order": "unordered", "finding": "removes-used" }
29
+ ```
30
+
31
+ `order` comes from `dependsOn`, transitively: `a-first`, `b-first` or `unordered`.
32
+
33
+ | Finding | When | Effect |
34
+ |---------|------|--------|
35
+ | `double-create` | both tasks create the same file (paths compared normalised, `./a` is `a`) | error, exit 1; unordered: make one depend on the other; ordered: drop it from one task's `creates` |
36
+ | `removes-used` | one task removes what the other edits, consumes, creates or produces | warning |
37
+ | `consumer-not-after-producer` | the user of a created file or produced symbol is not ordered after its producer | warning |
38
+ | `unordered-edit` | both tasks edit one file with no `dependsOn` between them | warning |
39
+
40
+ An error goes through the validator gate's single rework like any other. Warnings never change the exit code: render them in the Step 13 approval view under `⚠️ Plan overlaps` (autopilot: log `plan.overlaps={n} warnings={m}`), and fix the plan when the warning is real, usually by adding the missing `dependsOn` edge or merging the two tasks.
41
+
42
+ ## Complexity score
43
+
44
+ **Pattern**: "is this task too big" answered from what the task touches, not from its title. The score is a sum of points over counted factors, so the same plan and graph always give the same score and each point traces to a number.
45
+
46
+ | Factor | Counted as | Points |
47
+ |--------|-----------|--------|
48
+ | `files` | files in `files[]` | 1 → 0, 2-3 → 1, 4-5 → 2, 6-8 → 3, 9+ → 4 |
49
+ | `fanIn` | non-test files depending on the task's declarations, one ring out in the code graph, the task's own files excluded | 0 → 0, 1-3 → 1, 4-10 → 2, 11+ → 3 |
50
+ | `modules` | modules the task's non-test files span | one point per module past the first, at most 2 |
51
+ | `crossModuleEdges` | fan-in files outside every module the task touches | 0 → 0, 1-3 → 1, 4+ → 2 |
52
+ | `testSurface` | test files listed plus test files depending on the task's files | 0-2 → 0, 3-5 → 1, 6+ → 2 |
53
+
54
+ `score = 1 + points`, capped at 10. A module is the SPM target (`Sources/<Target>/`, `Tests/<Target>Tests/`), else the directory above the first `src` / `test` / `tests` / `lib` segment (the Gradle module), else the first directory. Without a graph, `fanIn`, `crossModuleEdges` and `notIndexed` are `null` and score nothing, and the output says `graph: absent`. `notIndexed` counts listed files the graph does not know, usually the ones the task creates.
55
+
56
+ `recommendSplit` is `score > prefs.global.planComplexity.splitAbove` (default 7; `--split-above N` overrides it). Splitting rules, applied before Step 13:
57
+
58
+ - Split along the factor that scored: files by module first, then producer before consumer, tests with the code they test.
59
+ - Traceability: every requirement id on the original appears on at least one child, and no child carries an id the original did not.
60
+ - Ordering: a task that depended on the original depends on the child that completes the original's output; children chain through `dependsOn` where one consumes another.
61
+ - One round. Re-run the validator and the scorer; a task still above the line is shown at Step 13 with its factors, not split again.
62
+
63
+ Output: `{splitAbove, graph, split: [ids], tasks: [{id, score, recommendSplit, factors, points}]}`. `--write` stores `{score, recommendSplit, factors}` as each task's `complexity` in the plan file. Autopilot log: `plan.complexity split={ids} graph={used|absent}`.
64
+
65
+ ## Reference
66
+
67
+ Overlap module: `$HOME/.claude/scripts/_plan-overlap.mjs` (`planOverlaps(tasks)`). Validator: `$HOME/.claude/scripts/validate-planning.mjs`. Unit: `test/plan-overlap.test.mjs`, fixtures under `test/fixtures/plan-overlap/`. Scorer: `$HOME/.claude/scripts/plan-complexity.mjs` (`planComplexity(tasks, {graph, splitAbove})`), reusing `graph-affected.mjs`'s reverse walk. Unit: `test/plan-complexity.test.mjs`, golden fixtures under `test/fixtures/plan-complexity/`.
@@ -7,9 +7,14 @@ while next=$(bash "$HOME/.claude/lib/plan-todos.sh" next "$TASK_ID"); [ -n "$nex
7
7
  id=$(printf '%s' "$next" | cut -f1)
8
8
  task=$(printf '%s' "$next" | cut -f2)
9
9
  bash "$HOME/.claude/lib/plan-todos.sh" start "$TASK_ID" "$id"
10
- # ... TDD cycle for "$task" ...
10
+ # ... RED: write the failing test, run it, keep the JUnit XML / .xcresult / log ...
11
+ bash "$HOME/.claude/lib/plan-todos.sh" tdd-red "$TASK_ID" "$id" "$RED_RESULTS" --tests "$TEST_NAME"
12
+ # ... GREEN: implement, run the suite ...
13
+ bash "$HOME/.claude/lib/plan-todos.sh" tdd-green "$TASK_ID" "$id" "$GREEN_RESULTS"
11
14
  if build_passed; then
12
15
  bash "$HOME/.claude/lib/plan-todos.sh" complete "$TASK_ID" "$id" "${COMMIT_HASH} · tests:${TEST_COUNT}"
16
+ git -C "$WORKTREE" diff --name-only HEAD~1 HEAD \
17
+ | bash "$HOME/.claude/lib/plan-todos.sh" drift "$TASK_ID" "$id" --files - --not-done "$NOT_DONE"
13
18
  else
14
19
  bash "$HOME/.claude/lib/plan-todos.sh" fail "$TASK_ID" "$id" "build failed after 3 retries"
15
20
  break
@@ -18,3 +23,37 @@ done
18
23
  ```
19
24
 
20
25
  When disabled, the loop still walks `tasks[]` from `planning-output` - `plan-todos.sh` adds visibility (status, notes, deps-respecting `next`) but the underlying TDD contract is unchanged.
26
+
27
+ ## Drift propagation
28
+
29
+ A step that ends off-plan invalidates the plan of every step built on it. `drift` takes the same two signals the scope self-check records for the whole run (`features/scope-check.md`), per step: files the step changed that its `files[]` does not list, and planned items it did not do (`--not-done N`, the entries that go to `scope-check.json` `notDone[]`). `--reason <text>` records drift the two counts do not show, such as an interface whose shape changed.
30
+
31
+ On drift the step gets `drift: {outside, notDone, at}` and every **pending** step that depends on it, directly or transitively, gets `needsReplan: {because, at, reason}`; steps that already started or finished keep their state, and steps on other branches of the graph are untouched. Output: `{drifted, outside, notDone, flagged}`.
32
+
33
+ A flagged step:
34
+
35
+ - is reported by `next` as waiting (`task-graph.mjs` `reason: "needs-replan"`), and named on stderr when nothing else is ready;
36
+ - is refused by `start` and `complete` (exit 2), which name the drifted step;
37
+ - starts again after `plan-todos.sh confirm-replan "$TASK_ID" <todo-id> [note]`, run once its plan has been re-read against what the drifted step actually did (interactive: ask; autopilot: confirm when the drifted files are not in the flagged step's `files[]` and none of its `files[]` were deleted, otherwise stop the loop and log `plan.replan_needed={ids}`).
38
+
39
+ A later drift replaces the flag and its confirmation. `files[]` and `creates[]` come from the planning-output task through `set`, and both count as planned; paths are normalised on both sides (`./a`, `a` and `a//b/..` compare equal). A changed test file is planned when its name covers the stem of a planned file: `a.test.ts`, `a.spec.ts`, `a_test.go`, `ATests.swift`, `ATest.kt` and `test_a.py` cover `a.ts` / `A.swift` / `a.py` in any directory. A step with neither list is judged on `--not-done` and `--reason` only.
40
+
41
+ ## TDD evidence
42
+
43
+ `tdd-red` and `tdd-green` read the runner's own results (`lib/tdd-evidence.mjs`): JUnit XML file or directory (Gradle, Surefire, Jest/Vitest junit, `pytest --junitxml`), an `.xcresult` bundle via `xcrun xcresulttool`, that summary exported to JSON, or the xcodebuild / `swift test` log. They write `plan.todos[].tdd`.
44
+
45
+ | Call | Holds when | Refused (exit 2) when |
46
+ |------|-----------|------------------------|
47
+ | `tdd-red <task> <todo> <results> [--tests a,b]` | at least one test fails; with `--tests`, one of the failing tests is a named one | nothing failed, nothing ran, or every failure is in another test; under `enforce`, `--tests` on results that name no failing test (use JUnit XML); `warn` records that run with `red.testsUnverifiable` and a warning |
48
+ | `tdd-red ... --already-passing <reason>` | the run has no failure: the behaviour already exists; the run stands for RED and GREEN, the reason is kept | any test fails |
49
+ | `tdd-green <task> <todo> <results>` | RED is recorded (not needed under test policy `tests-after`), the run has no failure, and it is the RED suite: RED's named tests pass in it, or, without names on either side, it ran at least as many tests | a test still fails, nothing ran, no RED, a RED test is missing, fewer tests than RED |
50
+
51
+ Both refuse results written before the todo's `startedAt`.
52
+
53
+ `complete` then reads `prefs.global.tddEvidence.mode`:
54
+
55
+ - `off`: no check.
56
+ - `warn` (default): a todo without both runs completes with a warning and `tdd.outcome: "unverified"`; Phase 3 notes it.
57
+ - `enforce`: `complete` refuses (exit 2) and the todo stays open.
58
+
59
+ Never blocking, always recorded with `tdd.outcome: "exempt"` and a reason: a `docs` or `chore` todo (type carried from `planning-output`), a `component` task (the component skill short-circuits the TDD path), project test policy `none`, and `complete ... --no-tests "<reason>"` for a repo with no test target. The last one is the todo's own claim, so it is stored with `tdd.selfDeclared: true` and Phase 3 notes it next to the unverified todos.
@@ -0,0 +1,49 @@
1
+ # Feature: Rework model escalation (Phase 2 re-entry, Phase 3 Step 4)
2
+
3
+ **Pattern**: a rework round that dispatches the same model with the same findings tends to produce the same fix. Two signals say the current rung is not enough: the round is the **last** one the rework cap allows, or `review-delta.mjs` reported findings **still present** after the previous round's fix. On either, the fresh dev agent for the next round runs one rung up the model-fallback ladder (`fable -> opus -> sonnet -> haiku`, `features/model-fallback.md`), and only one. The decision is a pure function in `scripts/rework-escalation.mjs`, never a model's judgement.
4
+
5
+ ## Prefs
6
+
7
+ `prefs.global.reworkEscalation`:
8
+
9
+ | Field | Default | Meaning |
10
+ |---|---|---|
11
+ | `reworkEscalation.enabled` | `true` | Master switch. `false` keeps every rework round on the rung the dev dispatch resolved to. |
12
+ | `reworkEscalation.maxTier` | `opus` | Highest rung an escalation may reach. `fable` additionally needs `modelFallback.fableEnabled: true`. |
13
+
14
+ On by default because it fires at most on the rounds where a failure costs the most (the last one, or one that repeats the previous attempt), moves one rung, and stops at `opus`. Every existing cost control outranks it.
15
+
16
+ ## The call (before each rework dispatch)
17
+
18
+ ```bash
19
+ node $HOME/.claude/scripts/cost-budget-check.mjs --task-id "$TASK_ID" --prefs "$PREFS_FILE" >/dev/null; BUDGET_RC=$?
20
+ STILL=$(jq '.reviewIterations[-1].delta.counts.stillPresent // 0' "$STATE_FILE")
21
+ ESC=$(node $HOME/.claude/scripts/rework-escalation.mjs --iteration "$ITERATION" --max-iterations 3 \
22
+ --still-present "$STILL" --current-tier "$DEV_TIER" --budget-exit "$BUDGET_RC" \
23
+ --prefs "$PREFS_FILE" --state "$STATE_FILE" ${FELL_BACK:+--fallen-back})
24
+ ```
25
+
26
+ `ITERATION` is the rework round about to run (1-based, `retryCount + 1`); `DEV_TIER` is the rung the Phase 2 dev dispatch resolves to without this override (persona and routing, `skills/shared/core/multi-agent/SKILL.md`), so escalations never compound; `FELL_BACK` is set when this run emitted a `model_fallback` metric. Pass `BUDGET_RC` on every call: without `--budget-exit` the budget counts as disabled only when `costBudget.enabled` is not `true`, otherwise the rung holds with `budget-unchecked`. Prefs come from `--prefs`, then `$MULTI_AGENT_PREFS`, then `~/.claude/multi-agent-preferences.json`. `--state` records the result as `reviewIterations[-1].reworkEscalation`, including a refusal and its reason, and in an autopilot run lists an escalation in `decisions[]` (`features/autopilot-decisions.md`). Exit 2 or 3: log `rework.escalation_skipped`, dispatch on `DEV_TIER`; it never blocks the rework.
27
+
28
+ | `reason` | Meaning |
29
+ |---|---|
30
+ | `escalated` | Dispatch on `toTier` |
31
+ | `not-triggered` | Neither the last round nor a still-present finding |
32
+ | `disabled` / `past-cap` | Switch off, or the cap has already stopped Phase 2 |
33
+ | `run-fell-back` | The run degraded; it is never re-upgraded mid-run |
34
+ | `budget-warn` / `budget-exceeded` / `budget-unknown` | `cost-budget-check.mjs` exit 10 / 11 / other |
35
+ | `budget-unchecked` | No `--budget-exit` while `prefs.global.costBudget.enabled` is `true`: an unrun check is no evidence the budget has room |
36
+ | `unknown-tier` / `top-of-ladder` | Not an Anthropic rung, or nothing above it |
37
+ | `fable-off` / `at-ceiling` | The next rung is fable with the rung off, or above `maxTier` |
38
+
39
+ Telemetry: `log-metric.sh "$TASK_ID" 2 rework.escalation escalate= from= to= reason=`.
40
+
41
+ ## The escalated dispatch
42
+
43
+ `escalate: true` -> `PHASE_MODEL_OVERRIDE=<toTier>` for that one dev dispatch (the override outranks routing, so the fable and ceiling checks above are the only ones that apply). The agent is **fresh**: no forked history, no conversation. It gets files:
44
+
45
+ - `$WORKTREE/.pipeline/triage-round-$ITERATION.json`: the persisted findings of the review that sent this round back (accepted, with fingerprints)
46
+ - `$WORKTREE/.pipeline/rework-attempt-$((ITERATION-1)).diff`: the prior attempt's change, written by the orchestrator before the dispatch as `git diff <round start>..HEAD` (round 0 starts at the run's base), so the escalated agent sees what was tried without the conversation that tried it
47
+ - `delta.stillPresent[]` quoted first, as Phase 2 re-entry item 7 already requires
48
+
49
+ An escalation is per dispatch: the next round asks again from `DEV_TIER`, so the rework never sits more than one rung above the dev dispatch.
@@ -33,6 +33,7 @@ Output `{ok, justified, unjustified[], unlisted[], notDone}`. Exit 1 lists `unju
33
33
 
34
34
  ## Consumers
35
35
 
36
+ - With plan todos on, Phase 2 applies the same two signals per step as it goes: `plan-todos.sh drift` compares each step's changed files with its planned `files[]` and counts its not-done items, and flags the steps built on a drifted one for re-plan (`features/plan-todos.md`).
36
37
  - Phase 3 Step 2.2 renders file reasons, unjustified files and `notDone[]` into the shared reviewer prefix (`features/review-delta.md`).
37
38
  - Phase 4 Step 3 builds the PR `## Changes` bullets from `files[].reason` and lists `notDone[]` under `## Related` as "Follow-ups not done in this PR", merged with the final triage `deferred[]` (`channels/pr.md`).
38
39
 
@@ -66,7 +66,7 @@ Each type has a standard section skeleton. `A` = always present. `C` = condition
66
66
  | Acceptance Criteria | `{panel:borderColor=#d4a19e}` |
67
67
  | Every other section | `{panel:borderColor=#9eb6d4}` |
68
68
 
69
- Each panel closes with `{panel}` on its own line. Task and Bug keep plain `h3.` sections.
69
+ Each panel closes with `{panel}` on its own line. Task and Bug keep plain `h3.` sections. The colours are defaults: `prefs.global.issueTree.storyPanels` (`{story, criteria, other}`, `#rrggbb` each) replaces any of them, and `false` writes the Story with plain `h3.` sections. A value that is not a `#rrggbb` colour keeps the default. The `/multi-agent:analysis-jira` stories read the same setting.
70
70
 
71
71
  Section headings render in `outputLanguage` (e.g. tr: "Detaylı Açıklama", "Kapsam", "Kabul Kriterleri", "Test Senaryoları", "Adımlar", "Beklenen Sonuç", "Gerçekleşen Sonuç", "Ortam", "Ekran Görüntüleri", "API Contract", "Design Reference", "Notlar").
72
72
 
@@ -103,6 +103,7 @@ Post-Hoc & Side-Channel:
103
103
  /multi-agent:build-optimize iOS-only Xcode build perf wrapper → benchmark + analyze + recommend-first .build-benchmark/optimization-plan.md
104
104
  /multi-agent:create-jira ["desc"] [figma-url] [swagger-url] Create a Jira Task/Bug/Story matching team conventions (type, sprint, preview + approval)
105
105
  /multi-agent:diff-explain Map a Phase 3 triage finding back to specific diff lines
106
+ /multi-agent:postmortem [#id|file.jsonl] Post-mortem of a run from its transcripts: loops, stalls, root-cause hypotheses on file:line (read-only, redacted)
106
107
  /multi-agent:estimate <doc|text> Effort range from similar closed work: P25-P75 per role, with the tickets behind it
107
108
  /multi-agent:scenario-audit <csv> Test scenarios vs the code: PRESENT / PARTIAL / ABSENT / UNCLEAR, each PRESENT on a resolving file:line
108
109
  /multi-agent:graph Build and query the repo code graph (symbols, imports, references), LLM-free
@@ -102,6 +102,7 @@ Post-Hoc & Side-Channel:
102
102
  /multi-agent:build-optimize iOS-only Xcode build performance wrapper → benchmark + analiz + recommend-first .build-benchmark/optimization-plan.md
103
103
  /multi-agent:create-jira ["açıklama"] [figma-url] [swagger-url] Takım standartlarına uygun Jira Task/Bug/Story oluştur (tip, sprint, önizleme + onay)
104
104
  /multi-agent:diff-explain Phase 3 triage bulgusunu diff satırlarına eşle
105
+ /multi-agent:postmortem [#id|dosya.jsonl] Koşunun transkriptlerinden post-mortem: döngüler, takılmalar, dosya:satır dayanaklı kök-neden hipotezleri (salt-okunur, redakte)
105
106
  /multi-agent:estimate <doc|metin> Benzer kapanmış işlerden efor aralığı: rol başına P25-P75, dayandığı ticket'larla
106
107
  /multi-agent:scenario-audit <csv> Test senaryolarını koda karşı denetle: VAR / KISMEN / YOK / BELİRSİZ, her VAR çözülen bir dosya:satır ile
107
108
  /multi-agent:graph Repo kod grafiğini kur ve sorgula (semboller, import'lar, referanslar), LLM'siz
@@ -402,6 +402,8 @@ Progress line: ` → checking validator validate-planning`
402
402
 
403
403
  Non-zero exit fails CLOSED: emit the validator stderr + `errors[]` verbatim, attempt ONE self-correction rework (re-invoke the planner with the errors quoted, overwrite `$PLAN_FILE`), re-run the validator. If it fails again -> HALT the phase (never enter Phase 2 with an invalid plan) with recovery hint: `ERR: plan output failed validate-planning.mjs twice. Inspect $PLAN_FILE against $HOME/.claude/schemas/planning-output.schema.json, then resume with /multi-agent:resume #N.` Record `agent-state.phases["1"].validator` (`pass` | `pass-after-rework` | `halted`).
404
404
 
405
+ Then `plan-complexity.mjs "$PLAN_FILE" --graph "$GRAPH_PATH" --write` (`$GRAPH_PATH` as in `features/code-graph.md`; unset, the default graph path); split `split[]` tasks (`features/plan-checks.md`).
406
+
405
407
  Log: "Phase 1: Plan - {N} tasks created, {M} with architecture review, validator:pass"
406
408
 
407
409
  #### Step 10 - Put the plan on the widget (required)
@@ -423,9 +425,6 @@ and stored, only invisible.
423
425
  printf '%s' "$PLAN_JSON" | bash "$HOME/.claude/lib/plan-todos.sh" set "$TASK_ID" -
424
426
  ```
425
427
 
426
- `set` converts a planning-output document itself, so the `tasks[]`-to-`todos[]`
427
- mapping has one definition, the tested one.
428
-
429
428
  Phase 2 (Dev) then iterates with `plan-todos.sh next "$TASK_ID"` until empty, calling `start` before each step and `complete` (with notes) or `fail`/`skip` after. Phase 3 (Review) reads the Todo list to verify all `completed` items map to diff hunks. Phase 5 (Report) renders `list` into the agent-log + PR body.
430
429
 
431
430
  **Why opt-in:** `planning-output.schema.json` already drives Phase 2 dependency order; `plan.todos[]` adds notes, durations and status transitions at the cost of a state write per step. Flip on for visibility into long features.
@@ -47,13 +47,13 @@ The analysis document is the SOLE design source in Phase 2. Variant choices, pad
47
47
 
48
48
  <!-- progress-contract: applied -->
49
49
 
50
- **Progress (per `$HOME/.claude/multi-agent-refs/progress-contract.md`):** every non-trivial step emits one `→ <verb> <object>` line at immediate flush. Canonical set for standard TDD: `→ reading <file>`, `→ writing test <name>`, `→ running xcodebuild RED`, `→ writing code <file>`, `→ running xcodebuild GREEN`, `→ committing WIP <summary>`. Component-task dispatch emits the line `→ dispatching create-component <componentName>` immediately before the Skill-tool call and then relays the plugin skill's progress lines verbatim under indent ` →`.
50
+ **Progress (per `$HOME/.claude/multi-agent-refs/progress-contract.md`):** every non-trivial step emits one `→ <verb> <object>` line at immediate flush. Canonical set for standard TDD: `→ reading <file>`, `→ writing test <name>`, `→ running xcodebuild RED`, `→ writing code <file>`, `→ running xcodebuild GREEN`, `→ committing WIP <summary>`. Component-task dispatch emits the line `→ dispatching create-component <componentName>` before the Skill-tool call, then relays the plugin skill's progress lines verbatim under indent ` →`.
51
51
 
52
52
  #### Input contract
53
53
 
54
54
  Phase 2 consumes the Phase 1 output object conforming to `$HOME/.claude/schemas/planning-output.schema.json` - the task graph (`tasks[]` with `id`, `title`, `type`, `files`, and optional `dependsOn` / `acceptanceCriteria`) plus the architecture review notes. Tasks execute in dependency order; the schema's `dependsOn` field drives the ready-task picker.
55
55
 
56
- **Plan Todo iteration (opt-in)**: gated by `prefs.global.planTodos.enabled` (default: `false`). When enabled and Phase 1 Step 11 emitted a `plan.todos[]`, Phase 2 iterates via `$HOME/.claude/lib/plan-todos.sh next/start/complete/fail` instead of walking `tasks[]` directly. When disabled, the loop walks `tasks[]` from `planning-output` - TDD contract is unchanged. Full helper loop + state semantics: `$HOME/.claude/multi-agent-refs/features/plan-todos.md`. A todo with `sourceTag: Reuse` binds the file analysis already found; `Modify` edits in place. Writing a new file over a `Reuse` step is a Locked 11 violation and Phase 3 flags it.
56
+ **Plan Todo iteration (opt-in)**: gated by `prefs.global.planTodos.enabled` (default `false`). When enabled and Phase 1 Step 11 emitted `plan.todos[]`, iterate via `$HOME/.claude/lib/plan-todos.sh next/start/complete/fail/drift`; disabled, walk `tasks[]`, same TDD contract. Each todo records RED and GREEN runs (`tdd-red` / `tdd-green`); `complete` checks them per `prefs.global.tddEvidence.mode`. Loop + exemptions: `$HOME/.claude/multi-agent-refs/features/plan-todos.md`. A todo with `sourceTag: Reuse` binds the file analysis already found; `Modify` edits in place. Writing a new file over a `Reuse` step is a Locked 11 violation and Phase 3 flags it.
57
57
 
58
58
  **Shadow-Git checkpoints (opt-in)**: gated by `prefs.global.shadowGit.enabled` (default: `false`). When enabled, the orchestrator snapshots the worktree via `$HOME/.claude/lib/shadow-git.sh` so sub-phase rollback is possible without touching the project's real `.git` history. Lifecycle: `shadow-git.sh init` (Phase 0 baseline), `shadow-git.sh snapshot` (per step after `plan-todos complete`), `shadow-git.sh restore <sha> --files` (rollback). Modes: `per-todo-step` (default) or `per-tool-call`. Full wiring + storage cap: `$HOME/.claude/multi-agent-refs/features/shadow-git.md`.
59
59
 
@@ -88,6 +88,7 @@ When re-entering from Phase 3:
88
88
  5. `rejected` items are never touched. Log their IDs + triage reasons for audit only.
89
89
  6. After rework, increment `state.phases["2"].retryCount`; hard-kill at `retryCount === 3` and escalate to the user. Do not loop indefinitely. When `prefs.global.autopilotCircuitBreaker.enabled` is true (default), record the hard-kill before escalating as `state.circuitBreaker = {tripped: true, trigger: 3, detail: "rework cycles exhausted", checkpoint: {phase: 3, step: "re-entry", iteration: <n>}, trippedAt, counters: {reworkCycles: <n>}}`: the rework-storm trigger; `autopilotCircuitBreaker.maxReworkCycles` (default 3) is bounded above by this cap.
90
90
  7. When `state.reviewIterations[-1].delta` exists, `stillPresent[]` findings come first in the task list, quoted `STILL PRESENT after round N-1's fix`; repeating the previous attempt is the loop the circuit-breaker stops.
91
+ 8. Before each rework dispatch, `rework-escalation.mjs` picks the rung; findings and the prior report go as files (`features/rework-escalation.md`).
91
92
 
92
93
  If the latest iteration has `triage.approved === true` AND `accepted === []`, Phase 2 was entered by mistake - log the anomaly and return to Phase 3.
93
94
 
@@ -103,7 +104,7 @@ $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 2 rework.started \
103
104
  For each task (respecting dependency order), on start and on end: `bash $HOME/.claude/scripts/phase-tracker.sh sub 2 <task_id> "<title>" in_progress|completed|failed`.
104
105
 
105
106
  1. **Update task status: `in_progress` (required - do not skip or defer)**
106
- - **Why mandatory**: the board/tracker visually stays in "Todo" until this runs. Skipping it means reviewers and the rest of the team cannot see that work has started, and automations downstream (status reports, kanban swimlanes) will be wrong.
107
+ - **Why**: the board shows "Todo" until this runs, and status reports and swimlanes read it.
107
108
  - **Where it applies**:
108
109
  - Jira: transition issue to "In Progress" via REST API (`/rest/api/2/issue/{id}/transitions`)
109
110
  - GitHub Projects V2: `updateProjectV2ItemFieldValue` on the Status field (singleSelectOptionId)
@@ -112,10 +113,7 @@ For each task (respecting dependency order), on start and on end: `bash $HOME/.c
112
113
  - Re-read the Status field after mutation.
113
114
  - If the value is NOT `In Progress` (Jira) / `In Develop` (Projects V2), re-fetch current option IDs and retry ONCE.
114
115
  - If verify still fails after retry → log error, do NOT proceed to TDD. The mutation silently failed and continuing would leave the task mis-tracked.
115
- 2. **Multi-submodule awareness**: If the project has submodules (detected in Phase 0 Step 7), development may span multiple submodules. For example, a UI component might need:
116
- - New types/tokens in the **common** submodule
117
- - The component itself in the **uicomponents** submodule
118
- - Work in BOTH submodules in the same task - this is normal and expected
116
+ 2. **Multi-submodule awareness**: a task may span the submodules Phase 0 Step 7 detected (e.g. tokens in **common**, the component in **uicomponents**); that is expected.
119
117
  3. **TDD cycle** (Launch Agent with `model: "sonnet"`) - gated by `state.testPolicy`: `tdd` = the loop below; `tests-after` = skip RED, author the same tests once green; `none` = author no tests (existing ones never weakened), report "tests not written - project policy" rather than a gap.
120
118
 
121
119
  **RED - Write ONE failing test first:**
@@ -100,9 +100,9 @@ Persist the totals as `state.diffRisk` (Phase 4 `risk` section, Phase 5, `run-me
100
100
  | `ui_critical` | 1.5 | path matches `*View.swift`, `*Screen.kt`, `*Configuration.swift`, etc. |
101
101
  | `loc_changed` | 1.0 | base sensitivity to total `+/-` lines |
102
102
 
103
- **Reviewer prompt injection**: when `$RISK_JSON` is non-empty, the orchestrator builds a `${PRIORITY_FILES}` block (numbered list of top-N files with their score + signals) and injects it once per reviewer. Reviewer prompt template (`code-reviewer.md`) treats it as advisory and does not echo it back. Triage does not see the priority list - its job is to filter the merged findings, not the diff.
103
+ **Reviewer prompt injection**: when `$RISK_JSON` is non-empty, the orchestrator builds a `${PRIORITY_FILES}` block (numbered list of top-N files with their score + signals) and injects it once per reviewer. Reviewers (`code-reviewer.md`) treat it as advisory and do not echo it back. Triage does not see the priority list.
104
104
 
105
- **Gate behavior**: **never blocking**. If risk scoring fails (git, parse, validator), continue with no priority hint: reviewers get the full diff in default order. Failures are logged:
105
+ **Gate behavior**: if risk scoring fails (git, parse, validator), continue with no priority hint: reviewers get the full diff in default order. Failures are logged:
106
106
 
107
107
  ```bash
108
108
  [ -z "$RISK_JSON" ] && $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 3 review.diff_risk_skipped reason=$REASON
@@ -119,11 +119,11 @@ $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 3 review.diff_risk \
119
119
  files=$(jq '.totals.files' <<< "$RISK_JSON")
120
120
  ```
121
121
 
122
- **Opt-out**: `prefs.global.diffRiskAdvisory = false` skips this step (no script, no priority block). Default `true`: the cost is bounded and the signal-to-noise is measured against the golden-task fixtures.
122
+ **Opt-out**: `prefs.global.diffRiskAdvisory = false` skips this step. Default `true`.
123
123
 
124
124
  #### Step 1.76 - Test-integrity gate (produces BLOCKING findings)
125
125
 
126
- Step 1.75's `test_lines_removed` hint is too weak for what it detects: a suite made green by deleting tests. This makes it blocking findings triage must adjudicate. Pure function of the full report. `--out` always writes the file, the empty result included.
126
+ Step 1.75's `test_lines_removed` hint is too weak for a suite made green by deleting tests, so this makes it blocking findings triage must adjudicate. Pure function of the full report. `--out` always writes the file, the empty result included.
127
127
 
128
128
  ```bash
129
129
  TI_FILE="$WORKTREE/.pipeline/test-integrity.json"
@@ -136,6 +136,8 @@ TI_COUNT=$(jq -r '.count // 0' "$TI_FILE" 2>/dev/null || echo 0)
136
136
 
137
137
  The gate never blocks the phase; it *emits* blocking findings (unreadable input: zero). Feed it the FULL report: `--top` hides a shrinking test file below the cut. **No opt-out**: a run that can switch off its own anti-reward-hacking control cannot be trusted to report a pass.
138
138
 
139
+ **TDD evidence**: `node $HOME/.claude/lib/tdd-evidence.mjs unverified --state "$STATE_FILE"` lists todos closed without RED/GREEN or by `--no-tests`, as reviewer notes, not findings.
140
+
139
141
  #### Step 1.761 - Owned-path gate (BLOCKING findings)
140
142
 
141
143
  Phase 2 Gate 0 again, same base:
@@ -146,7 +148,7 @@ node $HOME/.claude/scripts/owned-path-gate.mjs --repo "$WORKTREE" --base "origin
146
148
  --state "$STATE_FILE" --out "$OP_FILE"
147
149
  ```
148
150
 
149
- `--out` drops the old file; exit 2 writes none. A missing `$OP_FILE` is a gate error, never "no findings" (`review-decision-gate.mjs` exit 3 under the gates). Findings merge at Step 3.0. Contract: `features/repo-profile.md`. Step 1.762: `features/valven.md`.
151
+ `--out` drops the old file; exit 2 writes none. A missing `$OP_FILE` is a gate error, never "no findings" (`review-decision-gate.mjs` exit 3 under the gates). Findings merge at Step 3.0. Contract: `features/repo-profile.md`. Step 1.762: `features/valven.md`; opt-in 1.763: `features/mutation-probe.md`.
150
152
 
151
153
  #### Step 1.77 - Reviewer scope (cost gate)
152
154
 
@@ -378,11 +380,11 @@ ANON=$(jq -n --argjson r "$REVIEWERS_JSON" --arg t "$TASK_ID" --argjson i "$ITER
378
380
 
379
381
  `$REVIEWERS_JSON` is `state.reviewIterations[i].reviewers`. Findings come back with `foundBy: "Source A|B|C"` and every identity key removed. Persist the map to `state.reviewIterations[i].anonymizationMap` for Phase 5 per-reviewer telemetry, and **never put the map in a prompt**.
380
382
 
381
- Then append the deterministic findings (Steps 1.76, 1.761, 1.762, 2.7):
383
+ Then append the deterministic findings (Steps 1.76-1.763, 2.7):
382
384
 
383
385
  ```bash
384
386
  SA_FILE="$WORKTREE/.pipeline/security-audit-$ITERATION.json"
385
- MERGED=$(printf '%s' "$ANON" | cat - "$TI_FILE" "$OP_FILE" "$VG_FILE" "$SA_FILE" 2>/dev/null \
387
+ MERGED=$(printf '%s' "$ANON" | cat - "$TI_FILE" "$OP_FILE" "$VG_FILE" "$MP_FILE" "$SA_FILE" 2>/dev/null \
386
388
  | jq -s '.[0] + ([.[1:][] | .findings // []] | add // [])')
387
389
  ```
388
390
 
@@ -390,7 +392,7 @@ Deterministic findings keep their `tag` (`test_integrity`, `owned_path`, `valven
390
392
 
391
393
  ##### 3.1 Short-circuit: no findings
392
394
 
393
- If **merged** findings `length === 0`, **skip triage**: write empty result `{"accepted": [], "deferred": [], "rejected": [], "approved": true}`, log, proceed to Phase 3. Note this is the merged count from 3.0: a run with zero reviewer findings but a non-empty test-integrity set must NOT short-circuit.
395
+ If **merged** findings `length === 0`, **skip triage**: write empty result `{"accepted": [], "deferred": [], "rejected": [], "approved": true}`, log, proceed to Phase 3. This is the merged count from 3.0: zero reviewer findings plus any gate finding must NOT short-circuit.
394
396
 
395
397
  **Autopilot** (or `MULTI_AGENT_UNATTENDED=1`): the triage output, this empty one too, goes through `verify-citations.mjs "$TRIAGE_FILE" --repo "$WORKTREE" --worktree --state "$STATE_FILE"` (every bucket, each `quote` against its line, pre-existing claims against the base commit): `$HOME/.claude/multi-agent-refs/features/unattended-gates.md` section 5. Then `review-decision-gate.mjs` (end of 3.7).
396
398
 
@@ -574,11 +576,11 @@ Merge the JSON into `state.reviewIterations[-1].delta` via `write-state.mjs`; em
574
576
 
575
577
  #### Step 4 - Consensus + Action (triage-driven)
576
578
 
577
- If `triage.consensus.verdict` is `split` or `unverified`, surface `consensus.disagreements[]` to the user before acting: interactive modes show the split and ask whether to treat the unverified agreement as a pass (picker-contract: Trust / Re-review / Treat-as-blocking); autopilot logs `review.consensus=<verdict>` and proceeds on the triage verdict. Never silently average a split into a pass.
579
+ If `triage.consensus.verdict` is `split` or `unverified`, surface `consensus.disagreements[]` to the user before acting: interactive modes show the split and ask whether to treat the unverified agreement as a pass (picker-contract: Trust / Re-review / Treat-as-blocking); autopilot logs `review.consensus=<verdict>`, proceeds on the triage verdict, recorded per `features/autopilot-decisions.md`. Never silently average a split into a pass.
578
580
 
579
581
  Act **only on triage.accepted**:
580
582
 
581
- - **accepted.blocking** → back to Phase 2 (max 3 iterations, with reflection prompt citing only accepted items). The reflection prompt names findings by `fingerprint` and quotes `delta.stillPresent` first, marked `STILL PRESENT after round N-1's fix`. When Step 3.7 ran and `state.reviewIterations[-1].verifyByTest.redTests[]` is non-empty, the reflection prompt cites each red test: "a failing repro test already exists at <testRef>; make it green; do not delete or weaken it."
583
+ - **accepted.blocking** → back to Phase 2 (max 3 iterations, rung per `features/rework-escalation.md`, reflection prompt cites only accepted items). The reflection prompt names findings by `fingerprint` and quotes `delta.stillPresent` first, marked `STILL PRESENT after round N-1's fix`. When Step 3.7 ran and `state.reviewIterations[-1].verifyByTest.redTests[]` is non-empty, the reflection prompt cites each red test: "a failing repro test already exists at <testRef>; make it green; do not delete or weaken it."
582
584
  - **accepted.important** → fix and re-review
583
585
  - **accepted.suggestion** → apply if reasonable
584
586
  - **deferred** → append to Phase 5 report as "follow-up items" (do not block)
@@ -595,7 +597,7 @@ node $HOME/.claude/scripts/learnings-ledger.mjs add --kind fact \
595
597
  --scope "<file-or-area glob from the finding>" --task "$TASK_ID"
596
598
  ```
597
599
 
598
- Statement shape: the durable rule/root cause, not the symptom - "force-unwrapped optional in async callback path", not "fixed crash in LoginView". Always pass `--diagnosis` with the causal why (Reflexion: the verbal reason prevents recurrence; a bare outcome does not). Dedup built in (exit 2 = already stored). Lessons re-enter Phase 1 + triage via `learnings-ledger.mjs brief` (renders diagnosis as "(why: ...)").
600
+ Statement: the root cause, not the symptom ("force-unwrapped optional in async callback path", not "fixed crash in LoginView"). Always pass `--diagnosis`, the causal why: a bare outcome does not prevent recurrence. Dedup built in (exit 2 = already stored). Lessons re-enter Phase 1 + triage via `learnings-ledger.mjs brief` (renders diagnosis as "(why: ...)").
599
601
 
600
602
  Progress line: ` → writing lesson to learnings ledger ({N} entries)`
601
603
 
@@ -170,6 +170,8 @@ Skipped sections: when `planTodos.enabled` is false or no `plan.todos[]` was emi
170
170
 
171
171
  (the last three columns come from `reviewIterations[i].delta.counts` (Phase 3 Step 3.8) and read `-` on iteration 1 and on runs that predate the delta; a tripped circuit-breaker is one extra line under the table: `Circuit-breaker tripped: trigger {n}, {detail}`)
172
172
 
173
+ (autopilot: `decisions-log.mjs render --state "$STATE_FILE"` output verbatim; own heading, nothing when empty)
174
+
173
175
  ## Files Changed
174
176
 
175
177
  (git diff --stat output)
@@ -317,8 +319,6 @@ bash $HOME/.claude/scripts/memory-save.sh "$PROJECT_ROOT" --from-json memory-syn
317
319
  - **user**: only if the user disclosed new context about their role, expertise, or team layout
318
320
  - **Otherwise return `[]`** - blank is the correct default. Do not save derivable facts (code structure, git history, command outputs).
319
321
 
320
- Zero new memories is normal and expected. A typical run produces one or none.
321
-
322
322
  Tracker: `phase-tracker.sh sub 5 3 "Knowledge+Memory" completed`.
323
323
 
324
324
  Log: "Phase 5: Knowledge captured ({N} entries to {files})"
@@ -335,7 +335,7 @@ If user does not respond at the channels multi-select menu within 30 minutes (wa
335
335
  4. Session exits cleanly. State persisted: `state.currentPhase=7, state.waitingFor="user-channels-choice", state.channelsTimeout=true`.
336
336
  5. Resume contract: `/multi-agent:resume <task-id>` re-opens the channels menu with the original state bundle (pipeline log, PR metadata, prefs pre-ticks).
337
337
 
338
- Rationale: silently posting defaults to Jira / Confluence after a timeout would leak wrong-tone content to external systems. Hard-stop + resume is the safer policy.
338
+ Rationale: posting defaults after a timeout would publish text nobody approved.
339
339
 
340
340
  ---
341
341