@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
@@ -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)
@@ -152,6 +153,46 @@ gets one clone per channel: its own label (the story's identity plus
152
153
  the label search, and a link is checked before it is sent, so a re-run doubles
153
154
  neither.
154
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
+
155
196
  ## The document lists its tickets
156
197
 
157
198
  After the write, `analysis-tickets-writeback.mjs` puts a `## Tickets` table at
@@ -176,6 +217,7 @@ are values the site fills in:
176
217
  types (`analysis-jira-write.sh --subtask-types`), and saves the answer
177
218
  (`analysis-story-tree.mjs --save-subtasks`): each chosen type becomes a role
178
219
  mapped to itself, and "no sub-tasks" sets `subtaskRolesDeclined`
220
+ - `cloneLinkType` and `dependencyLinkType` are the site's own link type names
179
221
  - `epicLinkFieldId`, when set, is the site's own field id; otherwise it is
180
222
  discovered
181
223
  - `subtaskIssueType: null` means discover it - `createmeta` returns whichever
@@ -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.
@@ -46,6 +46,10 @@ discovered later:
46
46
  - A host with no such transcripts (Copilot, Codex) reports `UNMEASURED`, never
47
47
  zero. Zero would read as "you spent nothing" when it means "I could not see".
48
48
 
49
+ A record is priced by its exact wire ID: a rung's `modelId`, a Copilot CLI
50
+ `copilotModelId` at its own model's rate from the table's `models` map, or a
51
+ previous-generation ID in the table's `legacy` map at that ID's own rate. A
52
+ dated snapshot (`<id>-<date>`) prices as the longest known ID it extends.
49
53
  Records whose model is not in `cost-table.json` are COUNTED as unpriced and left
50
54
  out of the total. Folding them in at zero would make an unknown model look free,
51
55
  which is the direction that hurts.
@@ -0,0 +1,176 @@
1
+ # Feature: Dependency Bump and Fix
2
+
3
+ <!-- toc -->
4
+ - [1. Shape of a run](#1-shape-of-a-run)
5
+ - [2. The script](#2-the-script)
6
+ - [3. What each phase does](#3-what-each-phase-does)
7
+ - [4. Scope of a fix round](#4-scope-of-a-fix-round)
8
+ - [5. Rollback](#5-rollback)
9
+ - [6. Settings](#6-settings)
10
+ - [7. Never](#7-never)
11
+ <!-- /toc -->
12
+
13
+ **Pattern**: bump one dependency, build and test, and when the bump breaks
14
+ something, fix only that breakage. A bump PR that also refactors, reformats or
15
+ "while I was here" fixes unrelated code cannot be reviewed as a bump, and a fix
16
+ loop with no cap turns a version bump into an open-ended rewrite.
17
+
18
+ Entry: `/multi-agent:deps [package[@version]] [--major]`. It is the normal
19
+ pipeline with a fixed task, not a second flow: same Phase 0 intake, worktree,
20
+ branch rules, Phase 2 dev agent and exit gate, Phase 3 review gates, Phase 4
21
+ commit and PR rules, Phase 5 channels.
22
+
23
+ ## 1. Shape of a run
24
+
25
+ | Field | Value |
26
+ |---|---|
27
+ | `state.mode` | `"deps"` |
28
+ | `state.taskType` | `chore` |
29
+ | Branch | `chore/deps-{kebab(name)}-{kebab(to)}`, kebab and collision rules from Phase 0 Step 4 |
30
+ | Commit | `chore(deps): bump {name} from {from} to {to}`, taken verbatim from `dep-bump.mjs apply` (`commitMessage`), plus ` [{jiraId}]` when the run carries one |
31
+ | PR body | the bump, its risk, the lockfile command, `alsoMoves`, the changelog excerpt, each fix and the error line it answers; `Ref:` / `Related:`, never `Closes` / `Fixes` / `Resolves` |
32
+ | `state.depBump` | the apply output plus `rounds`, `scope` and, on rollback, `rolledBack` and `stillFailing` |
33
+
34
+ ## 2. The script
35
+
36
+ `$HOME/.claude/scripts/dep-bump.mjs` owns every deterministic step; the model
37
+ never edits a version string by hand. JSON on stdout.
38
+
39
+ | Command | Does |
40
+ |---|---|
41
+ | `detect --dir D` | Ecosystems and manifests: SwiftPM `Package.swift`, Gradle `gradle/*.versions.toml`, npm `package.json`, pip `requirements*.txt` / `pyproject.toml`, each with its lockfile |
42
+ | `list --dir D [--registry]` | Candidates: `current` (lockfile, else declared), `latest`, `latestInMajor` (highest non-breaking), `risk`, `riskInMajor`, `source` |
43
+ | `apply --dir D --ecosystem E --name N --to V [--allow-major]` | One bump: manifest edit plus the lockfile through the ecosystem tool; writes `.pipeline/dep-bump.json` |
44
+ | `surface --dir D --ecosystem E --name N` | Files that import or reference the dependency |
45
+ | `scope --dir D [--log L]` | Files changed since the recorded base, split into `inScope` (manifest, lockfile and the surface recorded at apply), `newImporters` and `outOfScope`; exit 1 when anything is out of scope or a new importer has no cited error line |
46
+ | `notes --dir D --ecosystem E --name N --from A --to B` | The local changelog sections between the two versions, else the repository URL to read release notes from |
47
+ | `rollback --dir D` | Restores what the bump owns (manifest, lockfile, the files `scope` saw the fix touch in scope) from the recorded base and removes those the base lacks; every other change is left in place and listed under `kept` |
48
+ | `config` | The resolved `depBump.*` settings |
49
+
50
+ SwiftPM identities compare case-insensitively (`--name Alamofire` and
51
+ `alamofire` are one package). The surface covers every module name the package
52
+ is imported under: the identity, the repository name in the url, the products
53
+ named by `.product(name:package:)` and by-name target dependencies in the
54
+ package manifests. Packages an Xcode project adds through File > Packages keep
55
+ their pins in `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`
56
+ or `*.xcworkspace/xcshareddata/swiftpm/Package.resolved` and their requirement
57
+ in `project.pbxproj`. `detect` and `list` report them with `readOnly: true` and
58
+ a note; `apply` refuses them (exit 1), because dep-bump does not edit
59
+ `project.pbxproj`. Such a bump is made in Xcode.
60
+
61
+ For Gradle the surface is the build scripts that name the catalog alias and
62
+ the Kotlin and Java files whose imports start at the Maven group
63
+ (`com.google.code.gson`), the artifact id or the alias stem, each with an
64
+ optional major suffix (`okhttp3` for `com.squareup.okhttp3:okhttp`, `retrofit2`
65
+ for `retrofit`). A library whose package root is none of these (Gson's
66
+ `com.google.gson`) is not found by `surface`, so a fix in such a file is out of
67
+ scope and needs the error line that names it, cited per file (section 4).
68
+
69
+ Offline listing reads what is already on disk: the lockfile for the current
70
+ version, SwiftPM `.build/checkouts/<id>` tags and the Gradle module cache for
71
+ newer ones. `--registry` asks the ecosystem's own tool (`npm view`,
72
+ `git ls-remote --tags`, Maven or PyPI metadata over `curl`); a failed query is
73
+ a `note` on that package, never a failed listing. Registry and lockfile tools
74
+ run in the manifest's directory (a workspace member's lockfile tool in the
75
+ workspace root), so the repo's own `.npmrc` registries and scopes apply. A
76
+ name or url that does not fit its ecosystem's grammar (a url that is not
77
+ http(s), ssh, git, file or `git@host:`, a name starting with `-`) is never
78
+ passed to a tool; the package gets a `registry query skipped` note.
79
+
80
+ Risk follows semver: a major step is breaking, and so is a minor step under
81
+ `0.x` and any step under `0.0.x`. `+build` metadata is ignored, and for Gradle
82
+ a Maven release qualifier (`-jre`, `-android`, `.Final`, `-RELEASE`, `.GA`,
83
+ `.SP1`) is part of a stable version, not a prerelease. A version that does not
84
+ parse is `unknown` and is treated as major, and so is a prerelease target.
85
+ `apply` exits 3 on a major, unknown or prerelease bump without `--allow-major`,
86
+ and touches nothing.
87
+
88
+ A pip requirement with several clauses (`foo>=1.0,<2.0`) is bumped on its
89
+ primary clause, the first `==`, `===`, `~=`, `>=` or `>`; `<`, `<=` and `!=`
90
+ are never primary, and a requirement made only of them is not listed. When
91
+ another clause would exclude the target (`<2.0` against `2.1`), `apply` exits 1
92
+ and changes nothing: rewriting a bound the repo set is a decision, not part of
93
+ the bump.
94
+
95
+ `apply` refuses a worktree with uncommitted changes to tracked files (exit 1,
96
+ `dirty` lists them), so the base it records holds no work of anyone else's.
97
+
98
+ `apply` is atomic. When the lockfile tool fails, the manifest and the lockfile
99
+ are restored byte for byte and the tool's output tail is in `lock.output`.
100
+ Lockfile tools: `swift package resolve`, `./gradlew dependencies --write-locks`
101
+ (only with a `gradle.lockfile`), the resolved npm-family manager with scripts
102
+ disabled (`package-manager.mjs`; yarn 2 and later take `--mode=skip-build`, yarn
103
+ 1 `--ignore-scripts`), `poetry lock` on Poetry 2 and `poetry lock --no-update`
104
+ before it, `uv lock --upgrade-package`. A workspace member's lockfile is the
105
+ workspace root's (the nearest directory above it with a lockfile and a
106
+ `workspaces` field or `pnpm-workspace.yaml`), and the tool runs there; a
107
+ lockfile above the manifest in a directory that declares no workspaces fails
108
+ the bump rather than letting it go ahead with no lockfile. Membership is not
109
+ matched against the `workspaces` globs. A shared Gradle `version.ref` moves every module bound to
110
+ it; `alsoMoves` names them and they join the surface.
111
+
112
+ ## 3. What each phase does
113
+
114
+ | Phase | In a deps run |
115
+ |---|---|
116
+ | 0 Init | Candidate pick and the major gate happen in the command before Phase 0. Then the normal intake (account, repo, base branch, workspace) with the free-text task `chore(deps): bump {name} from {from} to {to}`, `taskType` `chore`, branch from section 1 |
117
+ | 1 Plan | No analysis document (`chore`). The plan is fixed: one todo, "apply the bump", plus one per failing area once Phase 2 has a red run. `notes` output and the `list` record are the plan's evidence. The approval gate shows the bump, its risk and `alsoMoves` |
118
+ | 2 Dev | `apply` in the worktree first, before any agent runs. Then the Phase 2 exit gate as written (build, lint, tests, evidence gates). Green: straight to Phase 3 with no dev agent. Red: fix rounds (section 4) |
119
+ | 3 Review | The standard gates and reviewers over the whole diff. `scope` runs first; every `outOfScope` file is a blocking finding unless the round's report ties it to a build or test error line |
120
+ | 4 Commit | `commitMessage` from `apply`, the PR body from section 1. Never auto-close an issue |
121
+ | 5 Report | Channels as in every mode |
122
+
123
+ ## 4. Scope of a fix round
124
+
125
+ A round is one Phase 2 dev dispatch, given exactly this:
126
+
127
+ - the failing build or test log tail, from the exit gate;
128
+ - `surface` output: the files that use the dependency;
129
+ - the `notes` excerpt (breaking changes, migration notes). With no local
130
+ changelog, read the release notes for the versions in between from the
131
+ repository URL it returns (`gh release view`, the releases page) when
132
+ reachable, and say when they were not;
133
+ - the rule: change only what the bump broke. A file outside the surface may be
134
+ touched only when a compile or test error names it, and the report cites that
135
+ error line per file.
136
+
137
+ The surface is the one `apply` recorded. A file that starts importing the
138
+ dependency during a fix round does not join it: `scope` reports it under
139
+ `newImporters`, and each one passes only when the build or test log handed to
140
+ `scope --log` has an error line naming that file. The line is returned as
141
+ `cited`; a new importer with `cited: null` fails scope like an out-of-scope
142
+ file.
143
+
144
+ After each round: `scope --log <exit-gate log>`, then the exit gate again. Out-of-scope files with no
145
+ cited error are reverted before the next round. Green ends the loop. Every round
146
+ is recorded in `state.depBump.rounds[]` with the files it touched and the
147
+ errors it answered.
148
+
149
+ ## 5. Rollback
150
+
151
+ A run still red after `depBump.maxFixRounds` rounds (default 2), or out of
152
+ scope after its last round, is not committed: `dep-bump.mjs rollback` returns
153
+ the manifest, the lockfile and the files the fix rounds touched to the recorded
154
+ base, removes the ones the base did not have, and leaves every other change in
155
+ place (`kept`); the run ends with a report instead of a PR. The report names the bump, each round, the errors still failing with their
156
+ log lines, the files that would need to change and the changelog section that
157
+ explains it. `state.depBump.rolledBack` is `true`. The branch is left with no
158
+ commit; nothing is pushed.
159
+
160
+ ## 6. Settings
161
+
162
+ | Key | Default | Effect |
163
+ |---|---|---|
164
+ | `depBump.maxFixRounds` | 2 | Fix rounds before rollback; 0 rolls back any bump that is not green as-is |
165
+ | `depBump.queryRegistry` | true | `list` gets `--registry`; false lists offline only |
166
+
167
+ Read through `node "$HOME/.claude/scripts/dep-bump.mjs" config`. Asserted by
168
+ `test/dep-bump.test.mjs` and `smoke-dep-bump.sh`.
169
+
170
+ ## 7. Never
171
+
172
+ - A major, unparseable or prerelease bump without `--major` or an explicit confirmation.
173
+ - A version edit by hand, or a lockfile edited as text.
174
+ - More than one dependency per run (a shared `version.ref` is one bump).
175
+ - A fix outside the bump's surface with no error line behind it.
176
+ - A commit after the round cap is reached red.
@@ -11,17 +11,17 @@
11
11
  - [Codex CLI](#codex-cli)
12
12
  <!-- /toc -->
13
13
 
14
- > Contract last revised in **v10.6.0** (Fable 5 restored as top tier). The version tag here tracks the last substantive change to this contract, not the pipeline release.
14
+ > Contract last revised in **v20.25.0** (current Claude generation; Copilot CLI model ids per rung). The version tag here tracks the last substantive change to this contract, not the pipeline release.
15
15
 
16
16
  Personas route to the top available intelligence tier they declare in
17
17
  `preferredModel`. That tier can be quota-limited or temporarily unavailable.
18
18
  This contract defines when and how the orchestrator falls back, deterministically,
19
19
  without ever editing persona files at runtime.
20
20
 
21
- > **Fable 5 available again (2026-07).** `claude-fable-5` is the top tier once
22
- > more. Architect (`ios/android/backend-architect`), Reviewer-1 (`code-reviewer`),
21
+ > **Fable is the top tier.** The `fable` rung resolves to `claude-fable-5-1`.
22
+ > Architect (`ios/android/backend-architect`), Reviewer-1 (`code-reviewer`),
23
23
  > and triage personas declare `preferredModel: fable` - the deepest-reasoning
24
- > roles. **If Fable 5 is unavailable, the first fallback is opus (`claude-opus-5`).**
24
+ > roles. **If Fable is unavailable, the first fallback is opus (`claude-opus-5-5`).**
25
25
  > Security and other `preferredModel: opus` personas keep opus as their top tier.
26
26
  > The `premiumTierUntil` date gate stays in the contract as a generic mechanism
27
27
  > for any plan-window-limited premium tier.
@@ -35,7 +35,7 @@ fable -> opus -> sonnet -> haiku
35
35
  The orchestrator walks **one step down per trigger** from whichever tier a
36
36
  persona prefers: a `fable` persona degrades `fable -> opus` first, then
37
37
  `opus -> sonnet`, then `sonnet -> haiku`; an `opus` persona starts at
38
- `opus -> sonnet`. So a Fable 5 outage transparently promotes Opus 5 into the
38
+ `opus -> sonnet`. So a Fable outage transparently promotes Opus into the
39
39
  architect/reviewer roles with no file edits.
40
40
 
41
41
  ### Rung names are the contract; model IDs are not
@@ -46,11 +46,16 @@ model ID each rung resolves to lives in exactly two places: `scripts/cost-table.
46
46
  `phases/phase-3-review.md`. A generation move edits those; it never renames a rung and
47
47
  never touches a persona file.
48
48
 
49
- Current resolution: `fable` -> `claude-fable-5`, `opus` -> `claude-opus-5`,
50
- `sonnet` -> `claude-sonnet-5`, `haiku` -> `claude-haiku-4-5`.
49
+ Current resolution: `fable` -> `claude-fable-5-1`, `opus` -> `claude-opus-5-5`,
50
+ `sonnet` -> `claude-sonnet-5-5`, `haiku` -> `claude-haiku-5-5`. These are the Claude Code
51
+ wire IDs. Copilot CLI dispatches only the models `copilot help config` lists, with dotted
52
+ names, so its column in the reviewer table names those (`claude-opus-4.7`,
53
+ `claude-sonnet-4.6`), recorded per rung as `copilotModelId` in the cost table (haiku:
54
+ `claude-haiku-4.5`). The fable rung has none and is not offered on Copilot CLI; a persona
55
+ that prefers it dispatches the opus rung there.
51
56
 
52
- **Rate limits do not follow the rung.** Claude Opus 5 draws on a pool separate from
53
- the combined Opus 4.x pool, so a generation move does not inherit the previous
57
+ **Rate limits do not follow the rung.** A new generation can draw on a rate-limit pool
58
+ separate from the previous one, so a generation move does not inherit the previous
54
59
  generation's headroom. Budget-ceiling downgrades below are unaffected - they read the
55
60
  cost ledger, not the provider's quota - but a 429 on the opus rung after a generation
56
61
  move is a limits question, not a fallback-contract question.
@@ -119,8 +124,8 @@ Scope is Claude Code only, and the reason differs per host:
119
124
 
120
125
  | Host | Effect of `fableEnabled: false` | Why |
121
126
  |---|---|---|
122
- | Claude Code | Every `preferredModel: fable` persona (`ios/android/backend-architect`, `code-reviewer`, triage) starts on `opus` | This is the only host where the rung means Fable 5 |
123
- | Copilot CLI | none | Fable 5 is not offered there; its personas never sat on this rung |
127
+ | Claude Code | Every `preferredModel: fable` persona (`ios/android/backend-architect`, `code-reviewer`, triage) starts on `opus` | This is the only host where the rung means Fable 5.1 |
128
+ | Copilot CLI | none | Fable is not offered there; its personas never sat on this rung |
124
129
  | Codex CLI | none, deliberately | The `fable` rung there means `gpt-5.6 @ xhigh`, a different model on a different account. Switching it off from a knob named after an Anthropic model would surprise a Codex user, so it does not |
125
130
 
126
131
  **Phase 4 loses a reviewer, and says so.** Reviewer 1 lands on `opus`, which
@@ -174,7 +179,8 @@ prints it once instead:
174
179
  `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
180
  Then export the override for every `preferredModel` persona this run. Interactive modes
176
181
  may instead ask once (`Keep top tier (usage credits) / Fall back to <model>`);
177
- autopilot never asks - it falls back and logs.
182
+ autopilot never asks - it falls back, logs, and lists the fallback in `state.decisions[]`
183
+ (`features/autopilot-decisions.md`).
178
184
  2. **Dispatch error (per dispatch).** On a model-unavailable / quota error from a
179
185
  top-tier dispatch and `onDispatchError: true`: retry the same persona once with
180
186
  `PHASE_MODEL_OVERRIDE=<fallbackModel>`. If that retry also fails with a dispatch
@@ -244,11 +250,12 @@ better internals.
244
250
  ## Non-goals
245
251
 
246
252
  - No automatic re-upgrade mid-run (a run that fell back stays fallen back -
247
- deterministic over clever).
253
+ deterministic over clever). Rework escalation (`features/rework-escalation.md`)
254
+ moves one rework dispatch up a rung and refuses to on a run that fell back.
248
255
  - No edits to `pipeline/agents/*.md` at runtime; frontmatter is install-time
249
256
  configuration only.
250
257
  - Copilot CLI reviewer set is out of scope: Copilot CLI pins its own three
251
- reviewer models (GPT-5.4 + Opus + Sonnet - Fable 5 is not offered there) and
258
+ reviewer models (GPT-5.4 + Opus + Sonnet - Fable is not offered there) and
252
259
  does not use this persona ladder. Only Claude Code dispatches Reviewer-1 on
253
260
  Fable.
254
261
 
@@ -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