@mmerterden/multi-agent-pipeline 14.0.0 → 14.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/README.md +4 -4
  3. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +1 -1
  4. package/package.json +1 -1
  5. package/pipeline/commands/multi-agent/dev/SKILL.md +5 -1
  6. package/pipeline/commands/multi-agent/log/SKILL.md +1 -1
  7. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
  8. package/pipeline/commands/multi-agent/status/SKILL.md +1 -1
  9. package/pipeline/commands/multi-agent/sync/SKILL.md +9 -4
  10. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +72 -0
  11. package/pipeline/multi-agent-refs/features/worktree-finalize.md +66 -0
  12. package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -3
  13. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +4 -4
  14. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +17 -4
  15. package/pipeline/multi-agent-refs/phases/phase-7-report.md +4 -1
  16. package/pipeline/preferences-template.json +2 -1
  17. package/pipeline/schemas/agent-state.schema.json +13 -0
  18. package/pipeline/schemas/criteria-manifest.schema.json +223 -53
  19. package/pipeline/schemas/prefs.schema.json +5 -0
  20. package/pipeline/schemas/token-budget.json +2 -2
  21. package/pipeline/scripts/build-stack-plugins.mjs +21 -0
  22. package/pipeline/scripts/gc-worktrees.sh +3 -2
  23. package/pipeline/scripts/migrate-prefs.mjs +31 -0
  24. package/pipeline/scripts/render-work-summary.sh +51 -3
  25. package/pipeline/scripts/skill-conformance.mjs +10 -0
  26. package/pipeline/scripts/test-integrity-gate.mjs +10 -2
  27. package/pipeline/scripts/worktree-finalize.sh +299 -0
  28. package/pipeline/skills/.skills-index.json +9 -9
  29. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  30. package/pipeline/skills/skills-index.md +1 -1
package/CHANGELOG.md CHANGED
@@ -16,6 +16,147 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [14.1.1] - 2026-07-30
20
+
21
+ Three defects that all shared one shape: a name written in one place and read in
22
+ another, so the mechanism looked implemented, ran without error, and did nothing.
23
+
24
+ - **Phase 0 wrote the base-ref field its own exit gate does not read.** `/multi-agent:dev`
25
+ recorded `"baseRefFreshness"` while `phase0-exit-gate.mjs` requires `baseFetchStatus`
26
+ with one of `fresh | cached-stale | local-branch | aborted`. The value vocabulary was
27
+ already correct - only the field name differed - so every dev-mode run failed its own
28
+ exit gate with `baseFetchStatus="<unset>"` even when all four Phase 0 pickers had
29
+ actually run. A gate that always fails is as useless as one that never fails. The
30
+ canonical name appeared in five places and the wrong one in exactly one: the dev
31
+ command's own doc. `smoke-phase-0-multi-repo.sh` now asserts that the name the exit
32
+ gate reads equals the name the phase doc documents, and that no shipped file names an
33
+ alternative, with a planted-line probe proving the detector can fire.
34
+ - **Branch memory never populated.** Phase 0 Step 3 read
35
+ `prefs.global.recentBranches[{projectKey}]` while its own step 7 wrote the legacy
36
+ `prefs.projects[].branches`, which `prefs.schema.json` marks pre-v2.1.0. Both spots
37
+ also described a `{name, lastUsed}` entry the schema rejects (`branch` is required and
38
+ `additionalProperties` is false), so a literal implementation would have failed prefs
39
+ validation and the dedup - which keys on `branch` - would have accumulated a duplicate
40
+ every run. The "reused from last run" picker option could therefore never appear.
41
+ `migrate-prefs.mjs` carries stranded legacy entries into the canonical LRU, stamped
42
+ with the migration time because the legacy field never recorded a real one and an epoch
43
+ stamp would be pruned by the TTL on first read; `count: 0` marks them seeded rather
44
+ than observed.
45
+ - **The sync skill hardcoded the author's git identity.** `git config user.name`,
46
+ `user.email` and `gh auth switch --user` carried literal values in the plugin-publish
47
+ block, while the same file's other two publish blocks used `{identity.name}` and
48
+ `{owner}`. Since the file ships to every installation, a downstream user's
49
+ plugins-repo commits would have been attributed to someone else and their `gh` account
50
+ switched under them - and it contradicted the pipeline's own rule that the git author
51
+ is always the user's identity.
52
+
53
+ Leak-gate coverage, which is why the third defect had gone unnoticed:
54
+
55
+ - `smoke-personal-data.sh` only ever scanned `pipeline/`, but the package also publishes
56
+ `install/`, `docs/`, `index.js`, `install.js`, `README.md` and `CHANGELOG.md`, and every
57
+ tracked file is public regardless of what npm ships. `CHANGELOG.md` was additionally in
58
+ the `--exclude` list. A second pass now scans every tracked file outside `pipeline/`,
59
+ driven by `git ls-files` so the covered set stays exactly "what is public" with no
60
+ second list to maintain. `LICENSE`, `package.json` and `CODE_OF_CONDUCT.md` are exempt,
61
+ because a package must name its author and a code of conduct must give a real contact.
62
+ - Added patterns for the author's own name, personal email and `gh auth switch` account -
63
+ none were checked before, which is precisely how a literal identity survived in a
64
+ shipped command - plus the employer's abbreviation where it is used as a symbol or
65
+ workspace prefix, bounded so ordinary words that merely contain those letters
66
+ (`HEALTHY`, `RHYTHM`) do not match.
67
+ - `--exclude-dir` for `.git`, `node_modules`, `.worktrees`, `.next` and `DerivedData`.
68
+ Without it, `--root` mode scanned `.git/logs`, so auditing any checkout was guaranteed
69
+ to "fail" on commit metadata no consumer receives, burying the real findings.
70
+ - Five leaks removed from public files: a real corporate email in `CHANGELOG-archive.md`,
71
+ a corporate toolkit name in both changelogs, the author's website in `docs/adr/0008`
72
+ and `docs/internal/`, and corporate symbol/repo/task literals in `docs/internal/`.
73
+ These are gone from HEAD; git history still contains them.
74
+
75
+ ## [14.1.0] - 2026-07-29
76
+
77
+ Two things the pipeline was supposed to do and did not: use the skills a project's
78
+ own toolkit says apply, and clean up after itself.
79
+
80
+ ### Added
81
+
82
+ - **Phase 3 asks the stack toolkit which of its skills govern the task.** Each
83
+ `ai-<platform>-engineering-toolkit` already ships an `index` skill holding a
84
+ 30-plus row intent-to-skill table, maintained beside the skills it points at.
85
+ Phase 3 dispatched to that plugin for exactly one case (`taskType == "component"`),
86
+ so `bugfix` / `feature` / `refactor` / `chore` had no skill dispatch at all:
87
+ whichever skills the host surfaced by description match were the ones used, and
88
+ nothing recorded or required any of them. That was the dev-side half of the gap
89
+ v14.0.0 closed on the review side - review asked "was this built to the rules it
90
+ was supposed to follow" while nobody had chosen any rules.
91
+
92
+ The routing table is NOT copied into this repo. A second copy would drift the
93
+ moment the plugin shipped a skill, and the copy here would be the stale one, so
94
+ the pipeline asks rather than knows. `smoke-stack-skill-routing.sh` check 5 fails
95
+ the build if a routing table appears in a shipped file (verified against a planted
96
+ 6-row table). Routed skills land in `telemetry.skillCalls[]` with
97
+ `routedBy: "<toolkit>:index@<version>"`, so Phase 4 conformance can hold the run to
98
+ what its own toolkit chose. An absent or disabled toolkit is a recorded no-op, not
99
+ a halt - a backend repo has no toolkit and must still run.
100
+ Contract: `refs/features/stack-skill-routing.md`.
101
+
102
+ - **Phase 6 removes a task's worktree once its PR is open** (`worktree-finalize.sh`,
103
+ gated by `prefs.global.settings.worktreeAutoRemoveOnPr`, default **true**). It
104
+ salvages `agent-state.json`, `phase-tracker.json`, `triage-output.json`,
105
+ `.pipeline/`, `.build.log`, `.test.log` and `.review-diff.txt` into the log dir
106
+ first, because Phase 7's triage-memory ingest, the learnings-ledger distill,
107
+ `render-work-summary.sh`, `:resume`, `:status` and `:log` all read them - and the
108
+ first three are `[ -f ]`-guarded, so a removal without salvage would have degraded
109
+ silently rather than failing.
110
+
111
+ It keeps the branch and **does not check it out**. `git worktree remove` leaves the
112
+ branch as an ordinary local branch, so nothing is lost, while a checkout would move
113
+ the user's HEAD and could collide with their own uncommitted work on another
114
+ branch. Phase 5 removes-then-checks-out on purpose because it is a test handoff;
115
+ this is not. Verified end to end: HEAD stays put, the user's uncommitted file
116
+ survives, the branch is still checkoutable on demand.
117
+
118
+ Every destructive path is gated and each skips with a reason rather than failing:
119
+ real uncommitted changes, an unpushed HEAD, `--local` mode, a cwd inside the tree,
120
+ an unregistered path. `--force` appears nowhere. Contract:
121
+ `refs/features/worktree-finalize.md`. New state: `worktreeRemovedAt`,
122
+ `artifactsPath`.
123
+
124
+ - Gates: `smoke-stack-skill-routing.sh` (12 checks) and `smoke-worktree-finalize.sh`
125
+ (28 checks, exercising real git repos rather than grepping the doc).
126
+
127
+ ### Changed
128
+
129
+ - `render-work-summary.sh` gained a log-dir fallback for state and tracker files.
130
+ It resolved them only from the worktree, so it exited 2 and the entire Work Summary
131
+ vanished from the PR body and the Jira comment. Its sibling
132
+ `render-agent-log-cost.sh` has had that fallback all along.
133
+ - `:resume`, `:status` and `:log` no longer treat a missing worktree as a broken run
134
+ when `worktreeRemovedAt` is set: state is read from `artifactsPath`, and resume asks
135
+ before moving the user's HEAD.
136
+ - `gc-worktrees.sh` and `/multi-agent:garbage-collect` both claimed the finishing
137
+ command owned worktree removal. That is now true rather than aspirational.
138
+ - Token budget 52200 -> 52700, after compressing 414 tokens out of Phase 3 and
139
+ Phase 6 first, per the discipline recorded in `token-budget.json`.
140
+
141
+ ### Fixed
142
+
143
+ - **Every finding the test-integrity gate produced was unattributable.** It read
144
+ `f.file` from the diff-risk report, which declares and emits `path`, so each
145
+ finding carried `file: undefined` and read "Test file 'undefined' shrinks". That
146
+ is useless to the developer and rejected by `reviewer-output.schema.json`, which
147
+ requires `file` with `minLength: 1` - on the one gate that deliberately has no
148
+ opt-out. It stayed invisible because the smoke's own fixtures used `"file"`, the
149
+ key the bug read: the fixture matched the bug instead of the schema. Fixtures
150
+ corrected, and a new assertion checks the finding names a real path (verified by
151
+ reintroducing the bug).
152
+
153
+ - **The command inventories kept a renamed command alive.** `finish` survived the
154
+ v14.0.0 rename to `ship` in all three inventory lists, because
155
+ `smoke-command-inventory.sh` only proved nothing was MISSING and `ship` appears
156
+ elsewhere in those files' prose. Added check 4b: every name in an inventory must be
157
+ a command that exists in the tree (verified against a planted `ghostcmd`).
158
+
159
+
19
160
  ## [14.0.0] - 2026-07-29
20
161
 
21
162
  The `--dev` family reviews its own work, and review now checks the code against the
@@ -2565,7 +2706,7 @@ Analysis open-question resolver + repo hygiene hardening.
2565
2706
  blockquotes), humanizer punctuation policy, no Figma access (Locked 30 - design-gap
2566
2707
  rows get only Defer + a re-run recommendation), no auto-commit. Command inventory
2567
2708
  33 -> 34; `/multi-agent:analysis` Phase 5 report now suggests the resolver when
2568
- Section 20 has open rows. Pattern ported from the ai-mobile-toolkit resolver skills.
2709
+ Section 20 has open rows. Pattern ported from a private stack toolkit's resolver skills.
2569
2710
  - **Dead references removed.** `analysis.md` Reusable refs no longer points at a
2570
2711
  non-existent `fetch-wiki.sh` (the wiki fetch chain is inline: clone -> gh api ->
2571
2712
  WebFetch); `refs/features/external-context-injection.md` figma row routed to the real
package/README.md CHANGED
@@ -62,11 +62,11 @@ The discipline behind all of this - bounded loops, evidence gates, token-budgete
62
62
  | Autopilot | `/multi-agent:autopilot "task"` | All 8 phases, no confirmations |
63
63
  | Dev | `/multi-agent:dev "task"` | Init → Dev → Review → Test → Commit → Report |
64
64
  | Local | `/multi-agent:local "task"` | Full pipeline, current branch (no worktree) |
65
- | Finish | `/multi-agent:ship` | Run the review→test→commit→report tail over local work |
65
+ | Ship | `/multi-agent:ship` | Run the review→test→commit→report tail over local work |
66
66
  | Audit | `/multi-agent:design-check` | Mock-mode vs Figma conformance, local-only |
67
67
  | Audit | `/multi-agent:testflight-validation` | Pre-submission gates for a TestFlight build: static archive audit → Apple's `altool --validate-app` → Review-Guidelines check. Validates only, never uploads |
68
68
 
69
- Helpers: `setup`, `status`, `resume #N`, `review`, `test`, `channels`, `stack`, `update`, `sync`, `refactor`, `jira`, `issue`, `analysis`, `create-jira`, `save`, `routines`, `forget`. 43 commands in all - full list: `/multi-agent:help`.
69
+ Helpers: `setup`, `status`, `resume #N`, `review`, `test`, `channels`, `stack`, `update`, `sync`, `refactor`, `jira`, `issue`, `analysis`, `create-jira`, `save`, `routines`, `forget`. 44 commands in all - full list: `/multi-agent:help`.
70
70
 
71
71
  ## Stacks
72
72
 
@@ -80,12 +80,12 @@ This enables the matching plugin (+ the shared `ai-common` plugin) in the repo's
80
80
 
81
81
  ## Tool support
82
82
 
83
- The pipeline runs natively on **Claude Code**, **Copilot CLI** and **Codex CLI** - all three install from the same `pipeline/` source and get the same 43 commands.
83
+ The pipeline runs natively on **Claude Code**, **Copilot CLI** and **Codex CLI** - all three install from the same `pipeline/` source and get the same 44 commands.
84
84
 
85
85
  | Tool | Flag | What it installs |
86
86
  |---|---|---|
87
87
  | Claude Code | `--claude` (default) | slash commands + skills + agents + `PreToolUse` secret-scan hook |
88
- | Copilot CLI | `--copilot` | instructions + 43 sub-command skills + scripts |
88
+ | Copilot CLI | `--copilot` | instructions + 44 sub-command skills + scripts |
89
89
  | Codex CLI | `--codex` | one router skill + 43 specs as refs + 8 agent TOML + `AGENTS.md` block + `codex mcp add` |
90
90
 
91
91
  Filter skills by stack with `--platform=ios\|android\|all`.
@@ -10,7 +10,7 @@ Two pressures collided during the v7.x line:
10
10
 
11
11
  1. **Installer monolith.** `install.js` reached 1246 LOC by v7.9.1. It mixed flag parsing, three platform installers (Claude / Copilot / six third-party adapters), telemetry, a dev-only file exclusion list, and a 200-line static generator for `copilot-instructions.md`. Reasoning about install behaviour required holding the whole file in your head; reviewers asked for documentation we could not produce because every change touched untyped, intertwined sections.
12
12
 
13
- 2. **Secret leaks at deploy time.** During the v7.9.1 production deploy of `mmerterden.dev`, a `vercel deploy --token=vcp_...` invocation failed. The Vercel CLI printed the failed argv verbatim in its retry hint. That leaked the deploy token into the conversation transcript and forced a token rotation. A second incident in the same release window - `git -c user.email=...` overriding the repo-local config - pushed seven commits with the wrong author identity, which Vercel's contributor gate then blocked. Both classes of failure share a root cause: privileged values flowed through argv when the codebase had no audited boundary between provider tools and the orchestration layer.
13
+ 2. **Secret leaks at deploy time.** During the v7.9.1 production deploy of the project website, a `vercel deploy --token=vcp_...` invocation failed. The Vercel CLI printed the failed argv verbatim in its retry hint. That leaked the deploy token into the conversation transcript and forced a token rotation. A second incident in the same release window - `git -c user.email=...` overriding the repo-local config - pushed seven commits with the wrong author identity, which Vercel's contributor gate then blocked. Both classes of failure share a root cause: privileged values flowed through argv when the codebase had no audited boundary between provider tools and the orchestration layer.
14
14
 
15
15
  We needed an architectural answer that prevented both classes of failure from recurring without expanding scope into "rewrite everything as TypeScript" - the project's zero-dependency philosophy (ADR-4) is a hard constraint.
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "14.0.0",
3
+ "version": "14.1.1",
4
4
  "description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -91,9 +91,13 @@ The agent CANNOT make these Phase 0 decisions automatically; it suggests, then w
91
91
  3. Cancel
92
92
  Confirm? [1/2/3]
93
93
  ```
94
- - User picks `2` → log warning + record `"baseRefFreshness": "stale"` in
94
+ - User picks `2` → log warning + record `"baseFetchStatus": "cached-stale"` in
95
95
  `agent-state.json`, proceed from local ref. Phase 6 push needs network anyway,
96
96
  so re-prompt there if still unreachable.
97
+ - Option `1` (fetch succeeded) records `"fresh"`; a local-branch base records
98
+ `"local-branch"`; option `3` records `"aborted"`. The field name and this
99
+ four-value vocabulary are what `phase0-exit-gate.mjs` reads, so a run that writes
100
+ anything else cannot close Phase 0.
97
101
 
98
102
  Always show what was **observed** next to what was **classified**. The previous
99
103
  wording asserted `Detected: <host> unreachable (VPN/DNS)` for every failure mode,
@@ -15,7 +15,7 @@ Show the task's detailed agent-log.md report.
15
15
  1. **Parse task ID** - extract the `#N` form from the argument.
16
16
  - No argument → find the most recent (highest-ID) worktree.
17
17
 
18
- 2. **Find the worktree** - search the known repos:
18
+ 2. **Find the worktree** - search the known repos. A task whose worktree was removed after its PR (Phase 6 finalize) is found under `$HOME/.claude/logs/multi-agent/<project>/<task-id>/` instead; the `agent-log.md` was always there, and `artifacts/agent-state.json` holds the state. Look there before reporting "task not found":
19
19
  ```bash
20
20
  find ~/my-ios-app/.worktrees/ ~/my-figma-app/.worktrees/ ~/my-ui-components/.worktrees/ -name "agent-state.json" -maxdepth 2 2>/dev/null
21
21
  ```
@@ -19,6 +19,7 @@ Resume a paused or failed task from the last successful phase.
19
19
  2. **Read + validate state** - parse `agent-state.json`:
20
20
  - Validate first: `node $HOME/.claude/scripts/validate-state.mjs <state-file>` (resume-safety check, tolerant of legacy shapes). On non-zero exit, do NOT guess a phase - surface the errors and stop with `ERR: agent-state.json is unsafe to resume; inspect it or 'kill #N' and restart.`
21
21
  - Confirm the worktree (`worktreePath` / `projects[].worktreePath`) exists and is usable; if missing or locked, run the Phase 0 "Worktree stale-lock heal" before continuing.
22
+ - **Unless `state.worktreeRemovedAt` is set.** Then the worktree was removed on purpose by Phase 6 once the PR opened, the branch is still local, and the artefacts live under `state.artifactsPath`. Do NOT heal or recreate it: read state from `artifactsPath`, and if the remaining work needs a checkout (a Phase 7 pause needs none), ask before moving the user's HEAD - they may be mid-work on another branch, which is exactly why the removal did not check the branch out.
22
23
  - `currentPhase` - last completed phase
23
24
  - `status` - `paused` | `failed` | `in_progress`
24
25
  - `haltReason` - if set, show it so the user knows why the run stopped; clear it on successful re-entry
@@ -14,7 +14,7 @@ Show every active and completed task as a table.
14
14
  - `~/my-figma-app/.worktrees/`
15
15
  - `~/my-ui-components/.worktrees/`
16
16
 
17
- 2. **Scan worktrees** - read `agent-state.json` in each worktree dir:
17
+ 2. **Scan worktrees** - read `agent-state.json` in each worktree dir. **Also scan the log dir**, because a task whose PR is open has no worktree any more (Phase 6 removes it and salvages its state): `find $HOME/.claude/logs/multi-agent -maxdepth 4 -name agent-state.json -path '*/artifacts/*'`. (`-maxdepth 4`, not 3: Phase 6 always passes `--project`, so the salvaged copy lands at `<project>/<task-id>/artifacts/agent-state.json`, which a depth-3 scan can never reach.) Merge both sets by `taskId`, preferring the worktree copy when both exist, and render a finalized task with its `worktreeRemovedAt` rather than omitting it - a task that shipped should not vanish from status.
18
18
  ```bash
19
19
  find {repo}/.worktrees/ -name "agent-state.json" -maxdepth 2
20
20
  ```
@@ -245,10 +245,15 @@ node "$HOME/multi-agent-pipeline/pipeline/scripts/build-stack-plugins.mjs"
245
245
  ```bash
246
246
  cd "$PLUGINS_REPO"
247
247
  if ! git diff --quiet; then
248
- git config user.name "Mert Erden"; git config user.email "mmerterden@gmail.com"
248
+ # Identity resolves from prefs.global.identities[] routed by platformIdentityRouting,
249
+ # exactly as Phase 6 does. Never hardcode a name, email or gh account here: this file
250
+ # ships to every installation, so a literal identity attributes a downstream user's
251
+ # plugins-repo commits to someone else and switches their gh account out from under them.
252
+ git config user.name "{identity.name}"
253
+ git config user.email "{identity.email}"
249
254
  git add -A
250
255
  git commit -m "chore: rebuild stack plugins from pipeline shared/external"
251
- gh auth switch --user mmerterden 2>/dev/null || true
256
+ gh auth switch --user {owner} 2>/dev/null || true
252
257
  git push origin main
253
258
  fi
254
259
  ```
@@ -490,10 +495,10 @@ same 43 specs as reference files rather than as peer skills, via Step 2b - see
490
495
 
491
496
  ```
492
497
  analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, design-check, dev,
493
- dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, forget, garbage-collect,
498
+ dev-autopilot, dev-local, dev-local-autopilot, diff-explain, forget, garbage-collect,
494
499
  help, ios-coding-standard, issue, jira, kill, language, local,
495
500
  local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
496
- routines, save, scan, search, setup, stack, status, sync, test, testflight-validation, uninstall, update
501
+ routines, save, scan, search, setup, ship, stack, status, sync, test, testflight-validation, uninstall, update
497
502
  ```
498
503
 
499
504
  **NOT synced**: `$HOME/.claude/multi-agent-refs/*` - lazy-load references, Claude Code specific
@@ -0,0 +1,72 @@
1
+ # Stack skill routing - letting the toolkit plugin choose its own skills
2
+
3
+ > **TLDR** - When a stack toolkit plugin is enabled, Phase 3 asks that plugin's own `index` skill which of its skills apply to this task, loads them before writing code, and records each into `state.telemetry.skillCalls[]`. The routing table lives in the plugin; the pipeline copies none of it.
4
+
5
+ ## Why this exists
6
+
7
+ Phase 3 dispatched to the toolkit plugin for exactly one case, `taskType === "component"` (see `component-dispatch.md`). Every other task - `bugfix`, `feature`, `refactor`, `chore` - had no skill dispatch at all: whichever skills the host happened to surface by description match were the ones that got used, and nothing recorded or required any of them.
8
+
9
+ That is the dev-side half of the gap `features/skill-conformance.md` closes on the review side. Review now asks "was this built to the rules it was supposed to follow"; without this step, the answer for a non-component task was "there were no declared rules, because nobody chose any".
10
+
11
+ The fix is not a routing table in the pipeline. Each `ai-<platform>-engineering-toolkit` already ships one: an `index` skill whose description says *"Load this first when unsure which skill applies"*, holding a 30-plus row intent-to-skill map maintained alongside the skills it points at. A second copy in this repo would drift the moment the plugin shipped a new skill, and the pipeline's copy would be the stale one.
12
+
13
+ So the pipeline's job is to **ask**, not to know.
14
+
15
+ ## When it runs
16
+
17
+ Phase 3 pre-flight, before any code is written, for **every** `taskType`. Component tasks keep their dedicated dispatch in `component-dispatch.md`; this step runs in addition, because the reference skills (architecture, naming, file placement, tokens) apply to a component build too.
18
+
19
+ ## Resolution
20
+
21
+ Platform comes from the same mapping component dispatch uses, so the two cannot disagree:
22
+
23
+ | `state.platform` / detected stack | Toolkit |
24
+ |---|---|
25
+ | ios, swift | `ai-ios-engineering-toolkit` |
26
+ | android, kotlin | `ai-android-engineering-toolkit` |
27
+ | anything else | no toolkit - step is a recorded no-op |
28
+
29
+ The toolkit is enabled per repo (`.claude/settings.local.json` / `~/.claude/settings.json` `enabledPlugins`). **Not enabled is not an error here**, unlike component dispatch: a backend or web repo legitimately has no toolkit, and halting would make the pipeline unusable outside mobile. Record the no-op and continue.
30
+
31
+ Two marketplaces may ship the same toolkit name (a public one and a corporate one). Resolve whichever is enabled and record its **name and version** in the ledger entry, because the routing table and the skill set differ between versions - a finding that cites a skill has to be traceable to the version that defined it.
32
+
33
+ ## The call
34
+
35
+ ```text
36
+ Skill(<toolkit>:index, args: "<task title + one-line intent>")
37
+ ```
38
+
39
+ The index returns which `reference/` and `workflow/` skills apply. Load each via the Skill tool before writing code. A typical task pulls one workflow skill plus one or more reference skills.
40
+
41
+ Emit one progress line per loaded skill per `progress-contract.md`, so the user can see which standards the run bound itself to rather than inferring it afterwards.
42
+
43
+ ## Recording - what makes this checkable
44
+
45
+ Append one `state.telemetry.skillCalls[]` entry per skill actually loaded:
46
+
47
+ ```json
48
+ {"skill": "ai-ios-engineering-toolkit:reference/architecture", "phase": 3,
49
+ "targetFiles": ["Domains/Checkin/Sources/CheckinScene.swift"],
50
+ "routedBy": "ai-ios-engineering-toolkit:index@0.13.0", "timestamp": "<ISO-8601>"}
51
+ ```
52
+
53
+ `routedBy` names the index and version that chose it. That is the difference between "the model happened to read a skill" and "the toolkit said this skill governs this task".
54
+
55
+ What Phase 4 actually does with it, precisely: Step 1.78 lists these entries in the manifest under `ledger.routedByToolkit`, so a reviewer and the Phase 7 report can see which skills the project's own toolkit selected. It does **not** give them extra weight in the coverage maths. The deterministic resolver stays primary because an unrecorded load and no load are indistinguishable in state, and no `routedBy` tag changes that - the tag says who chose the skill, not that the code honoured it.
56
+
57
+ ## Failure modes, and why none of them halt
58
+
59
+ | Situation | Behaviour |
60
+ |---|---|
61
+ | No toolkit for this stack | recorded no-op, continue |
62
+ | Toolkit not enabled in this repo | recorded no-op, continue (component dispatch still halts for its own case) |
63
+ | `index` resolves but routes to a skill that does not exist in this version | record the miss with the version, load the rest, continue. A stale row in a plugin's table must not stop a run |
64
+ | `index` itself does not resolve | record and fall back to the host's own description matching, which is the pre-v14.1.0 behaviour - no worse than before |
65
+
66
+ Nothing here blocks Phase 3. What is downstream is visibility, not enforcement: routed skills appear in the manifest's `ledger.routedByToolkit`, and a task that recorded nothing shows up as `ledgerSource: derived` with its coverage gap stated. Enforcement over rule IDs is the registry's job (`features/skill-conformance.md`), not this step's.
67
+
68
+ ## What this deliberately does NOT do
69
+
70
+ - It does not decide which skills apply. Copying the plugin's routing into this repo would put the authoritative table in the wrong place and guarantee drift.
71
+ - It does not fail a run for a missing skill. The pipeline's contract is to ask and record, not to require that a third-party plugin be complete.
72
+ - It does not replace `component-dispatch.md`. That path owns the component build itself, including the `figma-validate` pre-check and the halt-on-incomplete-state rule.
@@ -0,0 +1,66 @@
1
+ # Worktree finalize - removing a task's worktree once its PR is open
2
+
3
+ > **TLDR** - Phase 6 step 9. Once the PR exists the worktree is dead weight, so it is removed: artefacts are salvaged into the log dir first, the branch is kept and deliberately NOT checked out, and every destructive path is gated. Gated by `prefs.global.settings.worktreeAutoRemoveOnPr` (default **true**). Script: `worktree-finalize.sh`.
4
+
5
+ ## Why it exists
6
+
7
+ A finished task's worktree is a full second checkout that nobody needs after the PR is open, and removing it is the step people forget. `.worktrees/` then accumulates copies of the repo until `/multi-agent:kill` or `:garbage-collect` is run by hand.
8
+
9
+ Removing it at PR-open is only safe because of the salvage, so the two are one step and not two.
10
+
11
+ ## What it will not do
12
+
13
+ **No `git checkout` of the task branch.** `git worktree remove` leaves the branch as an ordinary local branch: the ref, its commits, and the ability to `git checkout <branch>` later all survive untouched. Checking it out here would move the user's HEAD out from under them and can collide with their own uncommitted work on another branch. Phase 5 removes-then-checks-out on purpose, because it is handing the branch over for manual testing; this step is not.
14
+
15
+ **No `git branch -D`.** The branch is the deliverable.
16
+
17
+ **No `--force`, ever.** `git worktree remove` refusing is a safety feature. The clean-tree check runs before it, so a refusal at that point means something unexpected (a lock, a submodule, permissions) and forcing past unexpected dirt is how work gets lost.
18
+
19
+ ## Preconditions - each one skips with a reason, none is an error
20
+
21
+ | Condition | Why it blocks |
22
+ |---|---|
23
+ | `worktreePath == projectRoot` (`--local` mode) | there is no worktree; removing it would delete the user's checkout |
24
+ | cwd is inside the worktree | a shell left on a deleted inode is worse than a leftover directory, and Phase 6 legitimately `cd`s into the worktree earlier |
25
+ | not a registered worktree of the project root | a mistyped path must not delete an unrelated directory |
26
+ | real uncommitted changes | never discarded; see the artefact carve-out below |
27
+ | HEAD not on the remote | removing a worktree whose commits exist nowhere else is data loss, not cleanup |
28
+
29
+ Exit codes: `0` removed, `3` skipped with a reason (report and continue to Phase 7), `1` usage error.
30
+
31
+ ### The artefact carve-out, and why `--untracked-files=no` is wrong
32
+
33
+ The pipeline's own artefacts live inside the worktree and are untracked, so a raw `git status --porcelain` is never empty at PR-open. Left unhandled the removal would never fire and the feature would look implemented while doing nothing.
34
+
35
+ So exactly these paths are forgiven, and nothing else:
36
+
37
+ ```
38
+ agent-state.json phase-tracker.json triage-output.json
39
+ .review-diff.txt .build.log .test.log .pipeline/
40
+ ```
41
+
42
+ Suppressing all untracked files instead (`--untracked-files=no`) would have been shorter and wrong: a source file the developer created but never `git add`ed is invisible to it, and that file would be destroyed silently.
43
+
44
+ ## Salvage
45
+
46
+ Copied into `$HOME/.claude/logs/multi-agent/<project>/<task-id>/artifacts/` before removal, each only if present - a task that never reached Phase 4 has no triage output and that is not an error.
47
+
48
+ This is why the removal is safe:
49
+
50
+ | Consumer | Reads | Without salvage |
51
+ |---|---|---|
52
+ | Phase 7 triage-memory ingest | `triage-output.json` | `[ -f ]`-guarded, so it degrades **silently**: the triage corpus and learnings ledger stop being fed and no error appears |
53
+ | Phase 7 learnings-ledger distill | same file | same silent degradation |
54
+ | `render-work-summary.sh` | `agent-state.json`, `phase-tracker.json` | exits 2, so the Work Summary vanishes from the PR body and the Jira comment |
55
+ | `:resume` | `agent-state.json` | cannot continue a Phase 7 pause |
56
+ | `:status`, `:log` | `agent-state.json` | the task becomes invisible |
57
+
58
+ `state.worktreeRemovedAt` and `state.artifactsPath` record the outcome. The timestamp is what tells a reader that a worktree-less task was finished-and-tidied rather than killed - without it, a missing worktree is indistinguishable from a broken run.
59
+
60
+ ## Multi-repo
61
+
62
+ Run serially per repo, and only **after** `update_sibling_links`: that function issues an update per PR and the loop `cd`s per repo, so removing repo 1's worktree mid-loop breaks repos 2..N.
63
+
64
+ ## Interaction with the existing removal sites
65
+
66
+ `gc-worktrees.sh` and `/multi-agent:garbage-collect` never touch a registered, healthy worktree - they sweep orphans. Both name this step as the owner of finishing-a-task removal, which is now true rather than aspirational.
@@ -73,7 +73,7 @@ Used for: input parsing, branch naming, commit messages.
73
73
  |---|---|---|---|
74
74
  | Project picked (Step 2) | `global.recentProjects` | `[{path, label, count, lastUsed}]` | 20 |
75
75
  | Multi-repo group picked or saved (Step 2) | `global.recentGroups` | `[{label?, repos[], count, lastUsed}]` | 10 |
76
- | Branch picked (Step 3) | `global.recentBranches[{projectKey}]` | `[{name, lastUsed}]` (TTL `settings.branchTtlDays`, default 15d) | implicit (TTL prunes) |
76
+ | Branch picked (Step 3) | `global.recentBranches[{projectKey}]` | `[{branch, lastUsed, count?}]` (TTL `settings.branchTtlDays`, default 15d) | 10 (TTL also prunes) |
77
77
  | Service ping (any external API call) | `global.serviceStatus[{service}]` | `{ok, checkedAt, reason?}` (TTL `settings.serviceStatusCacheSeconds`, default 300s) | n/a |
78
78
  | Git identity routed (Step 6a) | `projects[{name}].lastIdentity` | int (index into `global.identities`) | n/a |
79
79
 
@@ -261,7 +261,10 @@ Scan `$HOME` (maxdepth 2) for project markers (`.xcodeproj`, `Package.swift`, `b
261
261
  header: "Base branch"
262
262
  options: origin/develop (Recommended, reused from last run) | origin/main | release/8.4.0 | Other
263
263
  ```
264
- 7. User picks → store as `baseBranch`. Save to `prefs.projects[{project}].branches` (dedup, max 10).
264
+ 7. User picks → store as `baseBranch`, and append `{branch, lastUsed, count?}` to
265
+ `prefs.global.recentBranches[{projectKey}]` (dedup by `branch`, cap 10) - what the TTL
266
+ filter below reads. Key is `branch`, not `name`. Never the legacy
267
+ `projects[{project}].branches`.
265
268
 
266
269
  **MUST: this step is not skippable (BLOCKING).** The only legitimate skip is rule 4
267
270
  above - `baseBranch` already supplied in the input. Everything else asks. A run once
@@ -277,7 +280,7 @@ which still writes the fields - it does not leave them unset.
277
280
 
278
281
  **TTL filter for recent branches**:
279
282
 
280
- - `prefs.global.recentBranches[{projectKey}][]` carries `{name, lastUsed}`. Filter to those whose `lastUsed` is within `settings.branchTtlDays` (default 15).
283
+ - `prefs.global.recentBranches[{projectKey}][]` carries `{branch, lastUsed, count?}`. Filter to those whose `lastUsed` is within `settings.branchTtlDays` (default 15).
281
284
  - Stale entries (>TTL) are pruned in-place during the read - keeps the picker uncluttered without a separate cleanup pass.
282
285
  - The filtered "Recent" list precedes the fresh `git branch -r` list; cap at 5 visible recent entries.
283
286
 
@@ -8,7 +8,7 @@ Per Locked decision 30, Phase 3 Dev consumes the analysis document as the sole d
8
8
 
9
9
  Pre-flight steps (run in order, abort on failure).
10
10
 
11
- **Steps 1, 2, 3, 5 and 6 apply only when Phase 1 ran.** In the `--dev` family (`state.onlyDevelop === true`) there is no analysis document by design, so those steps are recorded as `not-applicable (no Phase 1 in this mode)` and skipped - an unconditional abort here would make every fast mode impossible, which is the contradiction the modes have always carried in practice. Steps 4, 7 and 8 apply in every mode.
11
+ **Steps 1, 2, 3, 5 and 6 apply only when Phase 1 ran.** In the `--dev` family (`state.onlyDevelop === true`) there is no analysis doc by design, so they are recorded `not-applicable (no Phase 1 in this mode)` and skipped - an unconditional abort there would make every fast mode impossible. Steps 4, 7, 8 and 9 apply in every mode.
12
12
 
13
13
  1. **Analysis document presence** (Phase 1 modes only): locate `analysis/<feature-slug>-<platform>.md` for the active platform.
14
14
  - Path resolution: `state.run.repoPath` + `/analysis/<feature>-<platform>.md`
@@ -28,7 +28,7 @@ Pre-flight steps (run in order, abort on failure).
28
28
 
29
29
  6. **Conventions handoff**: read `analysis Section 13.1 Concept Table` (Pass B output with footnotes). Persist concept-to-realization mapping into `state.dev.conventions[<concept>]`. Phase 3 implementation uses these names verbatim (e.g., if Section 13.1 says "State holder: PassengerFlightViewModel", Phase 3 names the class exactly `PassengerFlightViewModel`).
30
30
 
31
- 7. **MCP forbidden**: any attempt to call `mcp__claude_ai_Figma__*` in Phase 3 is a violation. The smoke gate `smoke-no-mcp-in-dev-phases.sh` (see CHANGELOG [9.0.0]) checks `state.telemetry.mcpCalls[]` after run and fails if Phase 3 contributed any entry.
31
+ 7. **MCP forbidden**: calling `mcp__claude_ai_Figma__*` in Phase 3 is a violation. `smoke-no-mcp-in-dev-phases.sh` reads `state.telemetry.mcpCalls[]` after the run and fails if Phase 3 contributed an entry.
32
32
 
33
33
  8. **Criteria ledger (required, every mode)**: the moment this phase consults a skill, a marketplace plugin skill, a stack guide or a module `CLAUDE.md` in order to write code, append an entry to `state.telemetry.skillCalls[]`:
34
34
 
@@ -36,9 +36,9 @@ Pre-flight steps (run in order, abort on failure).
36
36
  {"skill": "ios-coding-standard", "phase": 3, "targetFiles": ["Sources/Login/LoginViewModel.swift"], "timestamp": "<ISO-8601>"}
37
37
  ```
38
38
 
39
- `targetFiles` is required: a skill applied to the wrong files still reads as "applied" without it, so coverage could not be attributed. Append at the moment of consultation, not retrospectively at the end of the phase.
39
+ `targetFiles` is required - without it a skill applied to the wrong files still reads as "applied". Append at the moment of consultation, not at the end of the phase. Phase 4 Step 1.78 treats this as self-report only and resolves criteria independently; it is the one signal separating "applied to the wrong files" from "never opened".
40
40
 
41
- This is a **self-report**, and Phase 4 treats it as such. Step 1.78 resolves the criteria independently and defaults `ledger.source` to `derived`; a skill named here that the resolver cannot bind to a changed file is flagged rather than believed. The record still earns its keep, because it is the only signal that separates "this skill was applied to the wrong files" from "this skill was never opened" - and because a phase that has to name what it followed tends to follow something.
41
+ 9. **Stack skill routing (every `taskType`, when a stack toolkit plugin is enabled)**: ask the enabled `ai-<platform>-engineering-toolkit`'s own `index` skill which skills govern this task, load them BEFORE writing code, and record each into `state.telemetry.skillCalls[]` with `routedBy: "<toolkit>:index@<version>"`. The routing table stays in the plugin - a copy here would be the stale one. No toolkit, or none enabled, is a recorded no-op, not a halt. Contract: [`features/stack-skill-routing.md`]($HOME/.claude/multi-agent-refs/features/stack-skill-routing.md).
42
42
 
43
43
  The analysis document is the SOLE design source in Phase 3. Variant choices, padding values, color tokens, copy strings, accessibility identifiers, and test method names all come from the rendered Pass B cells. If something is missing in the analysis doc, the fix is to re-run `/multi-agent:analysis`, not to fetch from Figma.
44
44
 
@@ -89,9 +89,20 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
89
89
  - In mixed mode (some local, some remote), only prompt for the remote-backed repos; local ones auto-skip.
90
90
  - No -> Phase 7
91
91
  - Yes -> Create PR with technical description (see below)
92
- 9. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply with `gh issue edit {issueNo} --body "{updated body}"` and preserve every other section unchanged.
93
- 10. **NEVER close or resolve the issue** - neither GitHub Issue nor Jira. Issues require team review (4 approvals) before closing. Only post a comment with commit/PR URLs.
94
- 11. Log: "Phase 6: Commit {sha} - PR #{number}"
92
+ 9. **Worktree finalize (gated by `settings.worktreeAutoRemoveOnPr`, default true)**: run **from the project root** - step 3 leaves the shell inside the worktree and the script refuses there.
93
+
94
+ ```bash
95
+ cd "$PROJECT_ROOT"
96
+ FIN=$(bash $HOME/.claude/scripts/worktree-finalize.sh --json \
97
+ --worktree "$WT_PATH" --project-root "$PROJECT_ROOT" \
98
+ --task-id "$TASK_ID" --project "$PROJECT" --branch "$BRANCH")
99
+ ```
100
+
101
+ The script salvages before removing, keeps the branch, and does **not** check it out - the user's HEAD and uncommitted work stay put. On success it stamps `worktreeRemovedAt`, `artifactsPath` and `worktreePath: null` into the **salvaged** `agent-state.json` itself. Do NOT write those via `write-state.mjs "$STATE_FILE"`: in single-repo mode that path went away with the worktree, so the write fails, the fields land nowhere, and Phase 7 reads a dead path and silently skips the triage ingest. After a removal re-point `STATE_FILE` at `$(jq -r .artifactsPath <<< "$FIN")/agent-state.json`. Exit 3 is a safe skip (uncommitted changes, unpushed or detached HEAD, `--local`, cwd inside the tree, preference off): report and continue. Never `--force`. Contract: [`features/worktree-finalize.md`]($HOME/.claude/multi-agent-refs/features/worktree-finalize.md).
102
+
103
+ 10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply with `gh issue edit {issueNo} --body "{updated body}"` and preserve every other section unchanged.
104
+ 11. **NEVER close or resolve the issue** - neither GitHub Issue nor Jira. Issues require team review (4 approvals) before closing. Only post a comment with commit/PR URLs.
105
+ 12. Log: "Phase 6: Commit {sha} - PR #{number}, worktree {removed|kept: <reason>}"
95
106
 
96
107
  #### Step 3 - PR Description (technical detail for reviewers)
97
108
 
@@ -294,11 +305,13 @@ done
294
305
  update_sibling_links "${PR_URLS[@]}"
295
306
  ```
296
307
 
308
+ **Then finalize each repo's worktree**, serially and only now - `update_sibling_links` updates one PR per repo and the loop `cd`s per repo, so removing repo 1's worktree mid-loop breaks 2..N. Run `worktree-finalize.sh` once per repo with that repo's `--worktree` / `--project-root` / `--project`; each skips or removes independently.
309
+
297
310
  `update_sibling_links` does an UPDATE call per PR (Bitbucket: PUT with `version+1`; GitHub: `gh pr edit --body-file`). Idempotent: re-running with the same set is a no-op.
298
311
 
299
312
  ##### GitHub issue body - list all PRs
300
313
 
301
- Step 9's "Issue body update" extends to multi-repo: instead of a single `### Pull Requests` line, write one row per repo:
314
+ Step 10's "Issue body update" extends to multi-repo: instead of a single `### Pull Requests` line, write one row per repo:
302
315
 
303
316
  ```
304
317
  ### Pull Requests
@@ -212,7 +212,10 @@ This is independent of the channels-side `reportContent.costSummary` (which gate
212
212
  **Triage memory ingest (mandatory):** after Phase 4 produces a final triage output, persist the accepted/deferred/rejected rows into the per-repo triage corpus so Phase 1 enrichment and Phase 4 prior-art lookup can recall them on future tasks. Idempotent - re-running on the same task writes 0 rows.
213
213
 
214
214
  ```bash
215
- TRIAGE_PATH="$WORKTREE/triage-output.json"
215
+ # Salvaged copy first: Phase 6 removes the worktree once the PR is open, and this
216
+ # reader is `[ -f ]`-guarded, so a wrong path degrades SILENTLY.
217
+ TRIAGE_PATH="$(jq -r '.artifactsPath // empty' "$STATE_FILE" 2>/dev/null)/triage-output.json"
218
+ [ -f "$TRIAGE_PATH" ] || TRIAGE_PATH="$WORKTREE/triage-output.json"
216
219
  if [ -f "$TRIAGE_PATH" ]; then
217
220
  node $HOME/.claude/scripts/triage-memory.mjs ingest \
218
221
  --triage "$TRIAGE_PATH" \
@@ -29,7 +29,8 @@
29
29
  "repoScanDays": 90,
30
30
  "identityRoutingEnabled": false,
31
31
  "multiRepoEnabled": false,
32
- "pushMustSucceed": true
32
+ "pushMustSucceed": true,
33
+ "worktreeAutoRemoveOnPr": true
33
34
  },
34
35
  "defaultJiraKey": null,
35
36
  "reportChannels": {
@@ -162,6 +162,10 @@
162
162
  "timestamp": {
163
163
  "type": "string",
164
164
  "description": "ISO-8601 time of the consultation."
165
+ },
166
+ "routedBy": {
167
+ "type": "string",
168
+ "description": "Set when a stack toolkit's own index skill chose this skill, as '<toolkit>:index@<version>'. Recorded so a finding can be traced to the index version that selected it - the skill set differs between plugin versions. Phase 4 Step 1.78 surfaces these separately in the manifest ledger; it does NOT grant them extra trust, because the resolver stays primary either way."
165
169
  }
166
170
  }
167
171
  }
@@ -685,6 +689,15 @@
685
689
  }
686
690
  }
687
691
  }
692
+ },
693
+ "worktreeRemovedAt": {
694
+ "type": ["string", "null"],
695
+ "format": "date-time",
696
+ "description": "Set when Phase 6 removed the worktree after opening the PR. Its presence is what tells :resume, :status and :log that a worktree-less task is finished-and-tidied rather than broken - without it a missing worktree is indistinguishable from a killed run."
697
+ },
698
+ "artifactsPath": {
699
+ "type": ["string", "null"],
700
+ "description": "Directory the worktree's artefacts were salvaged into before removal (agent-state, phase-tracker, triage-output, .pipeline/, build+test logs, review diff). Phase 7 and :resume read from here when worktreePath is gone."
688
701
  }
689
702
  }
690
703
  }