@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.
- package/CHANGELOG.md +46 -0
- package/README.md +8 -6
- package/README.tr.md +7 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +7 -6
- package/docs/features.md +9 -4
- package/install/catalog-history.json +1 -1
- package/install/templates/claude-hooks.json +14 -1
- package/install/templates/codex-hooks.json +18 -0
- package/manifest.json +126 -98
- package/package.json +1 -1
- package/pipeline/agents/task-clarifier.md +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
- package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/postmortem/SKILL.md +90 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
- package/pipeline/contract/fixtures/runs-failed.json +1 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
- package/pipeline/contract/fixtures/runs-running.json +1 -1
- package/pipeline/lib/analysis-jira-write.sh +70 -0
- package/pipeline/lib/credential-inventory.sh +5 -5
- package/pipeline/lib/decisions.mjs +130 -0
- package/pipeline/lib/fetch-crashlytics.sh +6 -8
- package/pipeline/lib/model-rung.sh +1 -1
- package/pipeline/lib/plan-todos.sh +177 -37
- package/pipeline/lib/post-pr-review.sh +4 -6
- package/pipeline/lib/route-state.sh +1 -1
- package/pipeline/lib/shadow-git.sh +4 -5
- package/pipeline/lib/task-graph.mjs +446 -0
- package/pipeline/lib/tdd-evidence.mjs +579 -0
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
- package/pipeline/multi-agent-refs/analysis-template.md +4 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -11
- package/pipeline/multi-agent-refs/features/analysis-jira.md +42 -0
- package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
- package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
- package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +21 -14
- package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
- package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
- package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
- package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
- package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
- package/pipeline/multi-agent-refs/help/en.md +2 -0
- package/pipeline/multi-agent-refs/help/tr.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +18 -16
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
- package/pipeline/multi-agent-refs/picker-contract.md +3 -2
- package/pipeline/multi-agent-refs/response-shape.md +65 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
- package/pipeline/rules/response-shape.md +18 -0
- package/pipeline/schemas/agent-state.schema.json +36 -1
- package/pipeline/schemas/phases.json +4 -0
- package/pipeline/schemas/plan-todos.schema.json +104 -3
- package/pipeline/schemas/planning-output.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +122 -3
- package/pipeline/scripts/_code-graph.mjs +2 -2
- package/pipeline/scripts/_jsonl.mjs +151 -0
- package/pipeline/scripts/_plan-overlap.mjs +231 -0
- package/pipeline/scripts/_redact.mjs +124 -10
- package/pipeline/scripts/_transcript-hosts.mjs +406 -0
- package/pipeline/scripts/analysis-story-tree.mjs +153 -8
- package/pipeline/scripts/autopilot-publish.mjs +12 -1
- package/pipeline/scripts/build-stack-plugins.mjs +3 -3
- package/pipeline/scripts/capture-flush.sh +7 -7
- package/pipeline/scripts/code-graph-rules/android.json +1 -1
- package/pipeline/scripts/cost-analyze.mjs +32 -43
- package/pipeline/scripts/cost-table.json +35 -19
- package/pipeline/scripts/decisions-log.mjs +118 -0
- package/pipeline/scripts/dep-bump.mjs +1556 -0
- package/pipeline/scripts/edit-syntax-gate.mjs +362 -0
- package/pipeline/scripts/edit-syntax-gate.sh +32 -0
- package/pipeline/scripts/find-polluter.mjs +281 -0
- package/pipeline/scripts/gate-ledger.mjs +46 -13
- package/pipeline/scripts/gc-abandoned.sh +2 -2
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
- package/pipeline/scripts/git-path.mjs +1 -2
- package/pipeline/scripts/host-hooks.mjs +265 -0
- package/pipeline/scripts/jira-attach.sh +1 -1
- package/pipeline/scripts/keychain.py +3 -3
- package/pipeline/scripts/mutation-probe.mjs +484 -0
- package/pipeline/scripts/output-quality-check.sh +1 -1
- package/pipeline/scripts/phase-tracker.sh +5 -6
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
- package/pipeline/scripts/plan-complexity.mjs +259 -0
- package/pipeline/scripts/pre-push-check.sh +3 -6
- package/pipeline/scripts/review-decision-gate.mjs +33 -1
- package/pipeline/scripts/rework-escalation.mjs +264 -0
- package/pipeline/scripts/test-strength.mjs +5 -3
- package/pipeline/scripts/transcript-index.mjs +1131 -0
- package/pipeline/scripts/uninstall.mjs +90 -4
- package/pipeline/scripts/update-issue-progress.sh +4 -6
- package/pipeline/scripts/usage-report.mjs +2 -2
- package/pipeline/scripts/validate-planning.mjs +48 -6
- package/pipeline/skills/.skill-manifest.json +20 -12
- package/pipeline/skills/.skills-index.json +23 -1
- package/pipeline/skills/shared/README.md +5 -3
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
- package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +69 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- 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 **
|
|
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
|
|
22
|
-
>
|
|
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
|
|
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
|
|
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-
|
|
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.**
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
# ...
|
|
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
|
|