@ikunin/sprintpilot 1.0.5 → 2.0.5

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 (35) hide show
  1. package/README.md +48 -1
  2. package/_Sprintpilot/Sprintpilot.md +14 -1
  3. package/_Sprintpilot/manifest.yaml +1 -1
  4. package/_Sprintpilot/modules/autopilot/config.yaml +22 -0
  5. package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +45 -0
  6. package/_Sprintpilot/modules/autopilot/profiles/large.yaml +22 -0
  7. package/_Sprintpilot/modules/autopilot/profiles/legacy.yaml +35 -0
  8. package/_Sprintpilot/modules/autopilot/profiles/medium.yaml +5 -0
  9. package/_Sprintpilot/modules/autopilot/profiles/nano.yaml +35 -0
  10. package/_Sprintpilot/modules/autopilot/profiles/small.yaml +5 -0
  11. package/_Sprintpilot/modules/git/config.yaml +8 -0
  12. package/_Sprintpilot/modules/ma/config.yaml +42 -0
  13. package/_Sprintpilot/scripts/agent-adapter.js +247 -0
  14. package/_Sprintpilot/scripts/cached-read.js +238 -0
  15. package/_Sprintpilot/scripts/check-prereqs.js +139 -0
  16. package/_Sprintpilot/scripts/dispatch-layer.js +192 -0
  17. package/_Sprintpilot/scripts/git-portable.js +219 -0
  18. package/_Sprintpilot/scripts/infer-dependencies.js +594 -0
  19. package/_Sprintpilot/scripts/inject-tasks-section.js +279 -0
  20. package/_Sprintpilot/scripts/list-remaining-stories.js +295 -0
  21. package/_Sprintpilot/scripts/log-timing.js +425 -0
  22. package/_Sprintpilot/scripts/mark-done-stories-tasks.js +254 -0
  23. package/_Sprintpilot/scripts/merge-shards.js +339 -0
  24. package/_Sprintpilot/scripts/preflight-merge.js +235 -0
  25. package/_Sprintpilot/scripts/resolve-dag.js +559 -0
  26. package/_Sprintpilot/scripts/resolve-profile.js +355 -0
  27. package/_Sprintpilot/scripts/state-shard.js +602 -0
  28. package/_Sprintpilot/scripts/submodule-lock.js +130 -0
  29. package/_Sprintpilot/scripts/summarize-timings.js +362 -0
  30. package/_Sprintpilot/scripts/sync-status.js +13 -0
  31. package/_Sprintpilot/scripts/with-retry.js +145 -0
  32. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +572 -42
  33. package/bin/sprintpilot.js +4 -0
  34. package/lib/commands/install.js +157 -1
  35. package/package.json +1 -1
@@ -10,17 +10,33 @@ You do NOT hardcode the workflow sequence. After each completed skill, read its
10
10
 
11
11
  ### Shell portability
12
12
 
13
- The executing shell may be bash, zsh, PowerShell, or cmd — translate bash idioms as needed:
13
+ The executing shell may be bash, zsh, Git Bash (Claude Code's default on Windows), PowerShell, or cmd. The workflow itself uses dedicated Node helpers for the operations that were most fragile across shells the few remaining shell idioms below are the ones that work uniformly under bash, zsh, Git Bash, PowerShell, and cmd:
14
14
 
15
- | Bash | PowerShell |
15
+ | Idiom | Where used | Portable across |
16
+ |---|---|---|
17
+ | `2>&1` (merge stderr → stdout) | `git ... 2>&1` | bash, zsh, Git Bash, PowerShell, cmd |
18
+ | `\|\|` (run-on-failure chaining) | `git switch X \|\| git checkout X` | bash, zsh, Git Bash, PowerShell ≥7, cmd |
19
+
20
+ Idioms that were **previously inlined and have been replaced with Node helpers** (do NOT regress them):
21
+
22
+ | Old idiom | Replaced by |
16
23
  |---|---|
17
- | `A && B` | `A; if ($LASTEXITCODE -eq 0) { B }` |
18
- | `A \|\| true` | `A; $LASTEXITCODE = 0` |
19
- | `2>/dev/null` | `2>$null` |
20
- | `rm -rf <dir>` | `Remove-Item -Recurse -Force <dir>` |
21
- | `if [ -f X ]; then ... fi` | `if (Test-Path -PathType Leaf X) { ... }` |
24
+ | `git config --get K 2>/dev/null \|\| echo unset` | `git-portable.js config-get K --default unset` |
25
+ | `git worktree list --porcelain \| grep -c '^worktree '` | `git-portable.js count-worktrees` |
26
+ | `VAR=$(git ... rev-parse --git-common-dir)` | `git-portable.js common-dir` (output captured into `{{git_common}}`) |
27
+ | `git add A B C 2>/dev/null \|\| true` | `git-portable.js safe-add A B C` |
22
28
 
23
- For cross-platform file ops prefer the Node helpers under `_Sprintpilot/scripts/`, or inline: `node -e "require('fs').rmSync('<path>', {recursive: true, force: true})"`. When a step chains commands with `&&` and you cannot express it in one line, run them separately and STOP on any failure.
29
+ If you find yourself writing a new pipe (`\|`), POSIX redirect (`2>/dev/null`), shell substitution (`$(...)`), shell variable assignment (`VAR=value`), or POSIX-only tool (`grep`, `sed`, `awk`, `xargs`, `find -exec`), reach for a Node helper instead either an existing script under `_Sprintpilot/scripts/`, a new helper added to `git-portable.js`, or an inline `node -e "..."` snippet (which is portable across every host).
30
+
31
+ Common cross-platform inline snippets:
32
+
33
+ | Need | Inline Node snippet |
34
+ |---|---|
35
+ | Recursive remove | `node -e "require('fs').rmSync('<path>', {recursive: true, force: true})"` |
36
+ | File-exists check (exit 0/1) | `node -e "process.exit(require('fs').existsSync('<path>')?0:1)"` |
37
+ | Read JSON, extract field | `node -e "console.log(JSON.parse(require('fs').readFileSync('<file>','utf8')).<field>)"` |
38
+
39
+ When a step chains commands with `&&` and the chain cannot be expressed via a Node helper, run them separately and STOP on any failure.
24
40
 
25
41
  ---
26
42
 
@@ -38,6 +54,7 @@ Long autopilot runs will fill the context window. To prevent state loss:
38
54
  - **All state lives in files, never only in memory.** After every step, write progress to `{state_file}`.
39
55
  - **Story boundary = session boundary.** Never split a single story across sessions. Always finish the current story fully (dev → review → patches → done) before ending a session.
40
56
  - **Proactive session handoff.** After `{{session_story_limit}}` stories have been **fully implemented** in one session (default: 3) — meaning their complete cycle is finished (dev-story GREEN + code-review + patches + artifacts committed) — write state and tell user to start a new session with `/sprint-autopilot-on`. Configurable via `autopilot.session_story_limit` in `modules/autopilot/config.yaml`. Creating a story file does NOT count toward the limit — only finishing step 7 does. Do not wait for compaction to happen.
57
+ - **Sprint-complete checkpoint (MANDATORY).** The moment step 2 detects `sprint_is_complete = true`, the current session NEVER runs step 10 itself. It writes `current_bmad_step = "sprint-finalize-pending"` to `{state_file}`, releases the lock, emits a handoff report, and STOPS. The next `/sprint-autopilot-on` invocation boots in a fresh context, sees the pending state, and runs step 10. This is non-negotiable because step 10 is the context-rot failure zone — its CRITICAL deterministic script calls were reliably skipped when packed into the tail of a long session.
41
58
  - **On startup: check for saved state first.** If `{state_file}` exists, this is a RESUME — read it and skip to the saved story/step. Never re-do completed work.
42
59
 
43
60
  ### Menu and interaction handling — CRITICAL
@@ -119,6 +136,17 @@ Resolve:
119
136
 
120
137
  **`{state_file}` schema** (referenced as `STATE_FIELDS` below): `last_updated`, `current_story`, `current_bmad_step`, `completed_skill`, `next_skill`, `session_stories_done`, `stories_remaining`, `git_enabled`, `platform`, `in_worktree`, `pr_base`. Always update `last_updated` on every write.
121
138
 
139
+ **PR 6 state-write policy (`autopilot.coalesce_state_writes`):**
140
+
141
+ When the resolved profile sets `autopilot.coalesce_state_writes: true` (nano/small/medium/large by default; `legacy` false), state writes route through `state-shard.js` using a `sprint`-keyed shard as the authoritative state for sprint-level fields, and per-story shards for story-scoped fields. Policy:
142
+
143
+ - **Critical keys** (`current_story`, `current_bmad_step`, `in_worktree`, `patch_commits`) always go to shard via `state-shard.js batch`, which auto-flushes and writes straight through because the script recognizes them as crash-recovery keys.
144
+ - **Non-critical fields** (test counts, file lists, next_skill, session_stories_done, stories_remaining, etc.) go to `state-shard.js batch`, accumulating in the pending buffer. Flushed at each story boundary (step 7) and session checkpoint (step 9).
145
+ - **Merged authoritative state** (`autopilot-state.yaml`) is rebuilt via `merge-shards.js` at story boundary + session checkpoint + sprint complete.
146
+ - **Rollback** (`coalesce_state_writes: false`): every `Update {state_file}` action writes directly to `autopilot-state.yaml` via the existing STATE_FIELDS shape — no shard indirection. This is the v1.0.5 path byte-for-byte.
147
+
148
+ When the flag is `false`, the direct-write instructions below are authoritative. When `true`, substitute each `Update {state_file} with STATE_FIELDS: <changes>` with a `state-shard.js batch --story sprint --json <changes>` call, followed by a `merge-shards.js --project-root "{{project_root}}"` at the story boundary / checkpoint. The merged `autopilot-state.yaml` remains the single source of truth for resume-after-crash.
149
+
122
150
  ### Git integration bootstrap
123
151
 
124
152
  <action>Check if `{project-root}/_Sprintpilot/manifest.yaml` exists</action>
@@ -134,18 +162,57 @@ Resolve:
134
162
  - `{{create_pr}}` from `git.push.create_pr` (true)
135
163
  - `{{pr_template}}` from `git.push.pr_template` ("modules/git/templates/pr-body.md")
136
164
  - `{{cleanup_on_merge}}` from `git.worktree.cleanup_on_merge` (true)
165
+ - `{{granularity}}` from `git.granularity` ("story"). Resolver override wins below.
166
+ - `{{worktree_enabled}}` from `git.worktree.enabled` (true). Resolver override wins below.
167
+ - `{{squash_on_merge}}` from `git.squash_on_merge` (false). Resolver override wins below.
168
+ </action>
169
+ <action>**Apply profile overrides** via resolver — run each and set only if the resolver returns a value:
170
+ - `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get git.granularity` → override `{{granularity}}`.
171
+ - `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get git.worktree.enabled` → override `{{worktree_enabled}}`.
172
+ - `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get git.squash_on_merge` → override `{{squash_on_merge}}`.
137
173
  </action>
138
174
  <action>Read `{project-root}/_Sprintpilot/modules/autopilot/config.yaml` (if present) and set:
139
175
  - `{{session_story_limit}}` from `autopilot.session_story_limit` (default: 3). A value of 0 disables the limit (run until sprint complete).
140
176
  - `{{retrospective_mode}}` from `autopilot.retrospective_mode` (default: `auto`). Valid values: `auto` | `stop` | `skip`. Any unknown value falls back to `auto`.
141
177
  If the file or either key is missing, fall back to the defaults above.
142
178
  </action>
179
+ <action>**Resolve profile-driven flow** — run:
180
+ `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get autopilot.implementation_flow`
181
+ Output: `full` or `quick`. Set `{{implementation_flow}}` = output. Default to `full` if the call fails.
182
+ Run: `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get autopilot.session_story_limit` → override `{{session_story_limit}}` if the resolver produces a different value than config.yaml (profile overrides config silence). Same pattern for `autopilot.retrospective_mode`.
183
+ </action>
184
+ <action>**Resolve coalesce flag** — run:
185
+ `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get autopilot.coalesce_state_writes` → `{{coalesce_state_writes}}`. Default `false` on failure.
186
+ </action>
187
+ <!-- PR 11: detect the running host and resolve parallel-dispatch config. -->
188
+ <action>**Detect host agent** — run:
189
+ `node {{project_root}}/_Sprintpilot/scripts/agent-adapter.js detect --project-root "{{project_root}}"`
190
+ Parse the JSON output: set `{{host_agent}}` = host, `{{host_supports_parallel}}` = supports_parallel, `{{host_confidence}}` = confidence.
191
+ </action>
192
+ <action>**Resolve parallelism flags** via the profile resolver:
193
+ - `{{parallel_stories}}` from `ma.parallel_stories` (default false).
194
+ - `{{max_parallel_stories}}` from `ma.max_parallel_stories` (default 2).
195
+ - `{{experimental_parallel_on_gemini}}` from `ma.experimental_parallel_on_gemini` (default false).
196
+ </action>
197
+ <!-- Gemini CLI opt-in: when the user explicitly sets
198
+ experimental_parallel_on_gemini=true AND the detected host is
199
+ gemini-cli at HIGH confidence, promote supports_parallel=true
200
+ with a one-line warning. Worktree-scoped subagents aren't shipped
201
+ upstream yet, so this is user-opt-in-per-project. -->
202
+ <check if="{{experimental_parallel_on_gemini}} is true AND {{host_agent}} is gemini-cli AND {{host_confidence}} is high">
203
+ <action>Set `{{host_supports_parallel}}` = true</action>
204
+ <action>Log once: "EXPERIMENTAL: parallel_stories enabled on Gemini CLI via ma.experimental_parallel_on_gemini=true. Worktree-scoped subagents are not yet shipped upstream (gemini-cli#22967); expect possible serialization or quota throttling."</action>
205
+ </check>
206
+ <action>Silently coerce `{{parallel_stories}}` to false when `{{host_supports_parallel}}` is false OR `{{host_confidence}}` is not `high`. Log once:
207
+ `parallel_stories requested but host '{{host_agent}}' does not declare parallel support (confidence={{host_confidence}}); running sequentially`.
208
+ </action>
143
209
  </check>
144
210
 
145
211
  <check if="manifest does NOT exist">
146
212
  <action>Set `{{git_enabled}}` = false</action>
147
213
  <action>Set `{{session_story_limit}}` = 3</action>
148
214
  <action>Set `{{retrospective_mode}}` = `auto`</action>
215
+ <action>Set `{{implementation_flow}}` = `full`</action>
149
216
  <action>Log: "No _Sprintpilot/manifest.yaml found — running stock autopilot (no git)"</action>
150
217
  </check>
151
218
 
@@ -174,6 +241,11 @@ Resolve:
174
241
  <action>Set `{{platform}}` = "git_only"</action>
175
242
  </check>
176
243
 
244
+ <!-- PR 10: disable gc.auto on the main repo so git's auto-GC doesn't
245
+ race with concurrent worktree operations during the sprint. Save
246
+ the prior value so we can restore it at sprint complete (step 10). -->
247
+ <action>**Save + disable main-repo gc.auto**: set `{{original_gc_auto_main}}` = output of `node {{project_root}}/_Sprintpilot/scripts/git-portable.js config-get gc.auto --default unset --project-root "{{project_root}}"`, then `git -C {{project_root}} config --local gc.auto 0`.</action>
248
+
177
249
  <action>**Lock file** — run: `node {{project_root}}/_Sprintpilot/scripts/lock.js acquire`
178
250
  Output will be one of:
179
251
  - `ACQUIRED:<session-id>` → proceed
@@ -191,6 +263,26 @@ Resolve:
191
263
  Log: "Platform detected: {{platform}}"
192
264
  </action>
193
265
 
266
+ <!-- PR 7 CONDITIONAL BOOT WORK: a clean repo — main worktree only, zero
267
+ in-progress stories — can skip the slow health-check + branch
268
+ reconciliation below. Gate honored by non-legacy, non-large profiles
269
+ (large keeps full reconciliation for compliance/uptime reasons). -->
270
+ <action>Read `autopilot.conditional_boot_work` from the resolver:
271
+ `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get autopilot.conditional_boot_work` → `{{conditional_boot_work}}`. Default to `false` on failure.
272
+ </action>
273
+ <action>Count worktrees (cross-platform; replaces a `... | grep -c` pipe that needed POSIX shell):
274
+ `node {{project_root}}/_Sprintpilot/scripts/git-portable.js count-worktrees --project-root "{{project_root}}"` → `{{worktree_count}}`. The script fails open to 2 internally if git itself errors, matching the previous "fail-open to force full path" semantic.
275
+ </action>
276
+ <action>Count in-progress stories: read `{status_file}` and count stories whose status is NOT in {`done`, `backlog`}. Set `{{in_progress_count}}`. Fail-open to 1 (force full path) if the file is unreadable.</action>
277
+
278
+ <check if="{{conditional_boot_work}} is true AND {{worktree_count}} is 1 AND {{in_progress_count}} is 0">
279
+ <action>Log: "Boot fast-path (PR 7): clean repo — skipping health-check + branch reconciliation"</action>
280
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js once --story "sprint" --phase "boot.fast-path" --meta "{\"reason\":\"clean-repo\"}" --project-root "{{project_root}}"` — ignore failures.</action>
281
+ <action>Set `{{git_enabled}}` = true, `{{platform}}` = detected value</action>
282
+ </check>
283
+
284
+ <check if="NOT ({{conditional_boot_work}} is true AND {{worktree_count}} is 1 AND {{in_progress_count}} is 0)">
285
+
194
286
  <action>**Worktree health check** — run:
195
287
  `node {{project_root}}/_Sprintpilot/scripts/health-check.js --base-branch {{base_branch}} --status-file {{status_file}}`
196
288
  Output classifies each worktree as CLEAN_DONE, COMMITTED, STALE, DIRTY, or ORPHAN.
@@ -242,6 +334,8 @@ Resolve:
242
334
  </action>
243
335
 
244
336
  <action>Set `{{git_enabled}}` = true, `{{platform}}` = detected value</action>
337
+
338
+ </check><!-- end PR 7 conditional boot work (non-fast-path branch) -->
245
339
  </check>
246
340
 
247
341
  ---
@@ -256,7 +350,7 @@ Resolve:
256
350
  <action>Read `{state_file}` fully</action>
257
351
  <action>Extract saved state:
258
352
  - `{{current_story}}` — story in progress when last session ended
259
- - `{{current_bmad_step}}` — BMAD step that was active (2–7)
353
+ - `{{current_bmad_step}}` — BMAD step that was active (2–7), or the reserved value `sprint-finalize-pending` meaning "the prior session detected sprint-complete and checkpointed; this session's only job is step 10"
260
354
  - `{{completed_skill}}` — last skill that ran
261
355
  - `{{next_skill}}` — next skill recommended at save time
262
356
  - `{{session_stories_done}}` = 0 (reset counter for new session)
@@ -294,6 +388,32 @@ Resolve:
294
388
  - Update `{state_file}` with reconciled values
295
389
  </action>
296
390
 
391
+ <!-- Already-completed short-circuit. If the prior session wrote
392
+ sprint-complete (CRITICAL 5/7) but crashed before the state file
393
+ delete (CRITICAL 7/7), this invocation would otherwise loop into
394
+ the finalize handoff forever. Detect it and exit cleanly. -->
395
+ <check if="{{current_bmad_step}} is sprint-complete">
396
+ <action>Delete `{state_file}` — clean up the stale terminal marker.</action>
397
+ <action>Report: "Sprint already complete. Nothing to do — run `bmad-sprint-planning` to plan the next sprint, then re-run `/sprint-autopilot-on`." Then STOP.</action>
398
+ </check>
399
+
400
+ <!-- Fresh-context finalize path. If the prior session exited with
401
+ current_bmad_step = "sprint-finalize-pending", this invocation's
402
+ ONLY job is to run step 10 with a clean window. Verify the
403
+ sprint is still genuinely complete before jumping; if a user
404
+ added new work in between, fall through to the normal loop. -->
405
+ <check if="{{current_bmad_step}} is sprint-finalize-pending">
406
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/list-remaining-stories.js --status-file "{status_file}" --format envelope` — parse stdout JSON.</action>
407
+ <check if="parsed .state is sprint-complete">
408
+ <action>Log: "Resuming for sprint finalization with fresh context (context-rot mitigation)."</action>
409
+ <goto step="10">Run finalization</goto>
410
+ </check>
411
+ <check if="parsed .state is NOT sprint-complete">
412
+ <action>Log: "sprint-finalize-pending flag was set but new work appeared since checkpoint. Clearing flag and resuming normal execution."</action>
413
+ <action>Update `{state_file}`: `current_bmad_step = null`.</action>
414
+ </check>
415
+ </check>
416
+
297
417
  <!-- Resume from a `retrospective_mode: stop` pause. -->
298
418
  <check if="{state_file}.paused_at is epic-complete-awaiting-retrospective">
299
419
  <action>Set `{{paused_epic_id}}` from `{state_file}.paused_epic_id`. Check if epic `{{paused_epic_id}}` is `done` in `{status_file}` OR an artifact exists at `{implementation_artifacts}/retrospectives/epic-{{paused_epic_id}}-*.md`.</action>
@@ -324,7 +444,14 @@ Resolve:
324
444
  - `{{session_stories_done}}` = 0
325
445
  </action>
326
446
  <action>Create master task: "Sprintpilot — Full Sprint Execution" → `in_progress`</action>
327
- <action>Write initial `{state_file}` with STATE_FIELDS: `current_story = null`, `current_bmad_step = null`, `completed_skill = bmad-help`, `session_stories_done = 0`, `stories_remaining = [from sprint-status]`, `in_worktree = false`, `pr_base = {{base_branch}}`.</action>
447
+ <action>**Compute `{{stories_remaining}}`** deterministically — run:
448
+ `node {{project_root}}/_Sprintpilot/scripts/list-remaining-stories.js --status-file "{status_file}" --format envelope`
449
+ The helper always emits a well-formed JSON object on stdout, on every exit path:
450
+ `{"remaining":[...],"state":"pre-planning|sprint-in-progress|sprint-complete|parse-error"}`.
451
+ Parse stdout as JSON → set `{{stories_remaining}}` = `.remaining`.
452
+ Ignore the exit code: `.state` is authoritative and covers every case (missing file, parse error, or normal).
453
+ </action>
454
+ <action>Write initial `{state_file}` with STATE_FIELDS: `current_story = null`, `current_bmad_step = null`, `completed_skill = bmad-help`, `session_stories_done = 0`, `stories_remaining = {{stories_remaining}}`, `in_worktree = false`, `pr_base = {{base_branch}}`.</action>
328
455
  <action>Report to user:
329
456
  ```
330
457
  Sprintpilot ON
@@ -346,17 +473,89 @@ Resolve:
346
473
 
347
474
  <step n="2" goal="Main execution loop — route to correct handler">
348
475
 
349
- <check if="all stories in status_file are done">
350
- <goto step="10">Sprint complete</goto>
476
+ <!-- PR 12 CROSS-EPIC PARALLELISM (experimental, off by default on every
477
+ profile including `large`). All safety rails must pass:
478
+ 1. ma.parallel_epics is true.
479
+ 2. Host confidence is HIGH AND supports_parallel is true (same as
480
+ intra-epic gate in PR 11).
481
+ 3. Two or more epics in dependencies.yaml declare `independent: true`.
482
+ 4. preflight-merge.js reports NO conflicts between all pairs.
483
+ 5. Session-scoped disable flag {{cross_epic_disabled_this_session}}
484
+ is false (flips true after any cross-epic merge conflict).
485
+ Only on the first iteration of the loop — subsequent iterations
486
+ don't re-preflight; they consume the cached safe_pairs list. -->
487
+ <action>Resolve `{{parallel_epics}}` from `ma.parallel_epics` (default false) via the resolver.</action>
488
+ <check if="{{parallel_epics}} is true AND {{host_supports_parallel}} is true AND {{host_confidence}} is high AND {{cross_epic_preflight_done}} is not true AND {{cross_epic_disabled_this_session}} is not true">
489
+ <action>Read `_Sprintpilot/sprints/dependencies.yaml` (if present). Extract every epic id where `epics.<id>.independent` is true. Set `{{independent_epic_ids}}` = comma-joined list of ids.</action>
490
+ <check if="{{independent_epic_ids}} has fewer than 2 ids">
491
+ <action>Log once: "cross-epic parallelism enabled but fewer than 2 epics declare `independent: true` in dependencies.yaml — running sequentially"</action>
492
+ <action>Set `{{cross_epic_preflight_done}}` = true, `{{cross_epic_safe_pairs}}` = []</action>
493
+ </check>
494
+ <check if="{{independent_epic_ids}} has 2 or more ids">
495
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/preflight-merge.js --epics "{{independent_epic_ids}}" --base "{{base_branch}}" --branch-prefix "{{branch_prefix}}" --project-root "{{project_root}}"`. Parse JSON — set `{{cross_epic_safe_pairs}}` = safe_pairs, `{{cross_epic_conflict_pairs}}` = conflict_pairs.</action>
496
+ <action>Log: "EXPERIMENTAL: parallel_epics preflight → safe={{cross_epic_safe_pairs.length}} conflict={{cross_epic_conflict_pairs.length}} checked=N"</action>
497
+ <action>Set `{{cross_epic_preflight_done}}` = true</action>
498
+ </check>
499
+ </check>
500
+
501
+ <!-- Authoritative "sprint complete" check. Read the status file EVERY
502
+ iteration via the coded helper (do not rely on stale state or LLM
503
+ re-parsing). The helper emits a JSON envelope on stdout on every
504
+ exit path, so there is no need to probe $? or stderr — .state is
505
+ authoritative. -->
506
+ <action>**Recalculate `{{stories_remaining}}`** — run:
507
+ `node {{project_root}}/_Sprintpilot/scripts/list-remaining-stories.js --status-file "{status_file}" --format envelope`
508
+ Parse stdout as a single JSON object: `{"remaining":[...],"state":"..."}`.
509
+ - `.state == "sprint-in-progress"` → `{{stories_remaining}}` = `.remaining`, `{{sprint_has_stories}}` = true, `{{sprint_is_complete}}` = false.
510
+ - `.state == "sprint-complete"` → `{{stories_remaining}}` = [], `{{sprint_has_stories}}` = true, `{{sprint_is_complete}}` = true.
511
+ - `.state == "pre-planning"` → `{{stories_remaining}}` = [], `{{sprint_has_stories}}` = false, `{{sprint_is_complete}}` = false.
512
+ - `.state == "parse-error"` → log warning; treat as pre-planning. NEVER declare sprint-complete on a parse failure.
513
+ </action>
514
+ <!-- Sprint-complete handoff. Never run step 10 in the same session that
515
+ first observed the sprint-complete condition — that session has
516
+ spent a long time in context and its tail is the context-rot zone.
517
+ Instead, mark the state as sprint-finalize-pending and stop; the
518
+ next /sprint-autopilot-on boot will route straight to step 10 (via
519
+ the step 1 gate) with a clean window. If we ALREADY are the
520
+ finalize session (current_bmad_step was sprint-finalize-pending on
521
+ entry), step 1 has already jumped to step 10 and we never reach
522
+ this check — so this gate is unambiguous. -->
523
+ <check if="{{sprint_is_complete}} is true">
524
+ <action>Update `{state_file}` with STATE_FIELDS: `current_bmad_step = "sprint-finalize-pending"`, `current_story = null`, `next_skill = null`, `stories_remaining = []`, `session_stories_done = {{session_stories_done}}`.</action>
525
+ <action>Release autopilot lock (idempotent): `node {{project_root}}/_Sprintpilot/scripts/lock.js release` — ignore failures.</action>
526
+ <action>Report to user and STOP this session:
527
+ ```
528
+ Autopilot sprint-complete checkpoint
529
+
530
+ All stories are done. Pausing before finalization so step 10 runs
531
+ with a fresh context — this prevents late-session instruction decay
532
+ from skipping CRITICAL cleanup actions (commit artifacts, generate
533
+ docs, clean up worktrees, emit final report).
534
+
535
+ Completed {{session_stories_done}} stories this session.
536
+ State saved to: {state_file}
537
+
538
+ Run `/sprint-autopilot-on` once more to finalize.
539
+ ```
540
+ </action>
541
+ <action>HALT — do not continue the execution loop. The next invocation enters via step 1, sees `sprint-finalize-pending`, and jumps to step 10.</action>
542
+ </check>
543
+ <check if="{{sprint_has_stories}} is false">
544
+ <action>Log: "Sprint pre-planning: no stories in status file yet. Routing through bmad-help to the next planning skill (do NOT go to step 10)."</action>
351
545
  </check>
352
546
 
353
547
  <check if="{{next_skill}} is empty">
354
548
  <action>**Recover next_skill** — re-read `{status_file}`, find first story with status != "done"</action>
355
- <check if="no undone stories found">
356
- <goto step="10">Sprint complete</goto>
549
+ <check if="no undone stories found AND {{sprint_has_stories}} is true">
550
+ <!-- Fallback sprint-complete detection. Route through the same
551
+ finalize-pending handoff as the primary gate above — never
552
+ jump into step 10 from this long-running session. -->
553
+ <action>Update `{state_file}`: `current_bmad_step = "sprint-finalize-pending"`, `current_story = null`, `next_skill = null`, `stories_remaining = []`.</action>
554
+ <action>Release lock: `node {{project_root}}/_Sprintpilot/scripts/lock.js release` — ignore failures.</action>
555
+ <action>Report: "Sprint complete detected via fallback path. Pausing for fresh-context finalization — run /sprint-autopilot-on once more." Then HALT.</action>
357
556
  </check>
358
- <action>Set `{{current_story}}` = first undone story from `{status_file}`</action>
359
- <action>Invoke `bmad-help` — "Story {{current_story}} needs attention. What is the next required workflow step?"</action>
557
+ <action>If `{{sprint_has_stories}}` is true: set `{{current_story}}` = first undone story from `{status_file}`.</action>
558
+ <action>Invoke `bmad-help` — "Story {{current_story}} needs attention (or: sprint in planning phase — no stories yet). What is the next required workflow step?"</action>
360
559
  <action>Extract `{{next_skill}}` from bmad-help response</action>
361
560
  </check>
362
561
 
@@ -398,10 +597,83 @@ Resolve:
398
597
 
399
598
  <step n="3" goal="Prepare and execute the recommended skill">
400
599
 
600
+ <!-- ──────────────────────────────────────────────────────────────────
601
+ PR 11 PARALLEL DISPATCH (gates):
602
+ - autopilot.parallel_stories: true (resolved at boot into
603
+ {{parallel_stories}})
604
+ - host_supports_parallel: true with high confidence (PR 11
605
+ agent-adapter.js — only Claude Code today)
606
+ - implementation_flow != quick (nano runs sequentially per epic)
607
+ - {{next_skill}} starts a fresh per-story flow (bmad-create-story
608
+ OR bmad-dev-story OR bmad-quick-dev) — never mid-story
609
+ - The current epic's DAG layer has width >= 2 (computed from
610
+ _Sprintpilot/sprints/dependencies.yaml via resolve-dag.js)
611
+
612
+ When all gates pass, the autopilot dispatches every story in the
613
+ current layer concurrently via the host's Agent tool — instead of
614
+ picking one story and running the full per-story flow sequentially.
615
+ This is the integration point that finally exercises the dispatcher
616
+ infrastructure built in PR 9 (resolve-dag) + PR 11 (dispatch-layer).
617
+ Without this block, parallel_stories=true had no behavioral effect.
618
+ ────────────────────────────────────────────────────────────────── -->
619
+ <check if="{{parallel_stories}} is true AND {{host_supports_parallel}} is true AND {{implementation_flow}} is NOT quick AND ({{next_skill}} is bmad-create-story OR {{next_skill}} is bmad-dev-story OR {{next_skill}} is bmad-quick-dev)">
620
+ <action>**Compute current DAG layer.** Set `{{epic_id}}` = leading numeric segment of the FIRST undone story in `{status_file}`. Run:
621
+ `node {{project_root}}/_Sprintpilot/scripts/resolve-dag.js layers --epic "{{epic_id}}" --project-root "{{project_root}}"`
622
+ Parse stdout as a JSON array of arrays (each sub-array is one layer of story keys). Find the FIRST layer that contains at least one story whose status in `{status_file}` is NOT `done`. Filter that layer down to only the undone stories — set `{{active_layer}}` to the resulting list.</action>
623
+ <check if="{{active_layer}} length is 1">
624
+ <action>Single-story layer — no parallel benefit. Continue with normal sequential dispatch below (the standard step-3 flow picks the single story).</action>
625
+ </check>
626
+ <check if="{{active_layer}} length is 2 or more">
627
+ <action>**Parallel dispatch:** run:
628
+ `node {{project_root}}/_Sprintpilot/scripts/dispatch-layer.js --layer "{{active_layer | join(',')}}" --max-parallel "{{max_parallel_stories}}" --project-root "{{project_root}}" --branch-prefix "{{branch_prefix}}" --base-branch "{{base_branch}}"`
629
+ Parse stdout — captures the per-story worktree paths (`stories[*].worktree`) and the `effective_parallel` count.</action>
630
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story sprint --phase "dispatch.layer-{{epic_id}}" --project-root "{{project_root}}"` — ignore failures.</action>
631
+ <action>**Spawn N concurrent sub-agents — IMPORTANT: send a SINGLE message containing N parallel Agent tool calls (one per story).** Do NOT serialize. Each sub-agent runs the FULL per-story flow for its assigned story (bmad-create-story → bmad-check-implementation-readiness → bmad-dev-story → bmad-code-review → patches → mark done) inside its own worktree. The Agent tool calls run concurrently and the message reply contains all sub-agent results.
632
+
633
+ For each story key `K` in `{{active_layer}}`, build one Agent call with `subagent_type=general-purpose` and a self-contained prompt of the form:
634
+ ```
635
+ You are running a single story for the Sprintpilot autopilot.
636
+ - Project root: {{project_root}}
637
+ - Story key: K
638
+ - Worktree: {{project_root}}/.worktrees/K (cd into it for all work)
639
+ - Skill flow (full): bmad-create-story → bmad-check-implementation-readiness → bmad-dev-story → bmad-code-review → apply patch findings → re-run tests → set status=done in {status_file}
640
+ - Skill flow (quick): bmad-quick-dev (single skill; nano profile)
641
+ Use {{implementation_flow}} = `{{implementation_flow}}` to pick which flow.
642
+ Track timing via `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story K --phase <phase> --project-root {{project_root}}` after each skill returns. The explicit `--project-root` is REQUIRED — without it the script falls back to cwd (the worktree), which orphans timing data. With per-story markers (2.0.5+) concurrent sub-agents writing to the same project root no longer race.
643
+ Return a one-line JSON summary on completion: {"story":"K", "status":"done"|"failed", "tests":"<N/M>", "notes":"<short>"}
644
+ ```
645
+
646
+ Wait for ALL N sub-agents to return. Collect results; abort the autopilot ONLY if a sub-agent reports a TRUE BLOCKER per the AUTOPILOT RULES.</action>
647
+ <action>**Merge per-story shards** after the layer completes:
648
+ `node {{project_root}}/_Sprintpilot/scripts/merge-shards.js --archive --project-root "{{project_root}}"` — collapses the per-story state shards into the authoritative project YAMLs and archives the layer's shards under `.archive/layer-<timestamp>/` so the next layer starts clean.</action>
649
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story sprint --phase "_end" --project-root "{{project_root}}"` — closes the open `dispatch.layer-{{epic_id}}` mark.</action>
650
+ <action>Update `{state_file}` with the new STATE_FIELDS (each completed story's status is reflected via the merge above; advance `session_stories_done` by `{{active_layer}}.length`). Then `goto step=2` to re-evaluate the loop.</action>
651
+ <goto step="2">Re-evaluate after parallel layer dispatch</goto>
652
+ </check>
653
+ </check>
654
+
655
+ <!-- PR 4 NANO ROUTING: when the active profile's implementation_flow is
656
+ 'quick', route bmad-dev-story through bmad-quick-dev instead. Quick-dev
657
+ runs Implement → Review → Classify → Commit internally (BMad
658
+ step-oneshot.md), so bmad-create-story / bmad-check-readiness /
659
+ bmad-code-review are not invoked in this flow. -->
660
+ <check if="{{implementation_flow}} is quick AND {{next_skill}} is bmad-dev-story">
661
+ <action>Override `{{next_skill}}` = `bmad-quick-dev`</action>
662
+ <action>Log: "Routing {{current_story}} through bmad-quick-dev per nano profile (implementation_flow=quick)"</action>
663
+ </check>
664
+ <!-- Under quick flow, autopilot never invokes bmad-create-story or
665
+ bmad-check-implementation-readiness; quick-dev reads AC from
666
+ sprint-status.yaml directly. If bmad-help proposes these skills
667
+ while implementation_flow=quick, skip them and advance. -->
668
+ <check if="{{implementation_flow}} is quick AND ({{next_skill}} is bmad-create-story OR {{next_skill}} is bmad-check-implementation-readiness)">
669
+ <action>Log: "Skipping {{next_skill}} under quick flow (nano profile) — quick-dev reads AC directly"</action>
670
+ <action>Set `{{next_skill}}` = `bmad-quick-dev`</action>
671
+ </check>
672
+
401
673
  <action>Set `{{completed_skill}}` = `{{next_skill}}`</action>
402
674
  <action>Create task "{{next_skill}}" → mark `in_progress`</action>
403
675
 
404
- <check if="{{next_skill}} is a per-story skill (bmad-dev-story, bmad-code-review, bmad-create-story)">
676
+ <check if="{{next_skill}} is a per-story skill (bmad-dev-story, bmad-quick-dev, bmad-code-review, bmad-create-story)">
405
677
  <action>Set `{{current_story}}` = first story in `{status_file}` with status `ready-for-dev` or `in-progress`</action>
406
678
 
407
679
  <critical>**Validate story key format** — the key MUST follow the pattern `{epic}-{story}-{title-kebab}` (e.g., `1-2-user-authentication`), NOT just `{epic}-{story}` (e.g., `1-2`).
@@ -415,8 +687,23 @@ Resolve:
415
687
  <action>Create per-story step tasks if not already created</action>
416
688
  </check>
417
689
 
418
- <!-- GIT: Enter worktree before dev-story -->
419
- <check if="{{git_enabled}} AND {{next_skill}} is bmad-dev-story">
690
+ <!-- PR 5: determine per-story epic key + title so the workflow can branch
691
+ per-epic under granularity=epic and decide "is this the last story
692
+ of the epic". Epic ID is the leading numeric segment of the story
693
+ key (e.g. '1-2-foo' → '1'); slug is the epic's title from sprint-
694
+ status.yaml (or the epic header in the epics file). Skip if empty. -->
695
+ <action>Set `{{epic_id}}` = leading numeric segment of `{{current_story}}` (e.g. `1-2-foo` → `1`). If the key doesn't match `^\d+-`, leave `{{epic_id}}` = "".</action>
696
+ <action>Set `{{epic_branch_name}}` = `epic-{{epic_id}}` (only used when `{{granularity}} = epic`).</action>
697
+ <action>**Detect first vs last story of epic** — read `{status_file}` (BMAD-owned; do not modify). Find all stories with the same `{{epic_id}}`:
698
+ - `{{is_first_story_of_epic}}` = true if no story in this epic has status `in-progress` or `done` yet (i.e. current story is the first to enter dev-story / quick-dev).
699
+ - `{{is_last_story_of_epic}}` = true if this is the final undone story in the epic (after this story, all other stories in the epic are `done`).
700
+ Both default to true when `{{epic_id}}` = "" (single-story "epic").
701
+ </action>
702
+
703
+ <!-- GIT: Enter worktree OR create/reuse epic branch before dev-story OR quick-dev.
704
+ Nano's profile sets worktree.enabled=false + granularity=epic, so this
705
+ block falls through to in-place branching. -->
706
+ <check if="{{git_enabled}} AND ({{next_skill}} is bmad-dev-story OR {{next_skill}} is bmad-quick-dev)">
420
707
  <action>**Sanitize branch name**: `node {{project_root}}/_Sprintpilot/scripts/sanitize-branch.js "{{current_story}}" --prefix "{{branch_prefix}}" --max-length 60`. Set `{{branch_name}}` = output. Full ref: `{{branch_prefix}}{{branch_name}}`.</action>
421
708
 
422
709
  <action>**Idempotency check** — if branch is already registered in `{status_file}` for this story AND its worktree exists, skip creation. If registered without worktree → recovery mode (see health check). Otherwise proceed.</action>
@@ -430,23 +717,74 @@ Resolve:
430
717
  Detached HEAD is fine — `git worktree add` below creates a new branch from HEAD.
431
718
  </action>
432
719
 
433
- <action>**Create worktree.** Try: `git worktree add "{{project_root}}/.worktrees/{{current_story}}" -b "{{branch_prefix}}{{branch_name}}" 2>&1`. If it fails because the branch already exists, retry without `-b`: `git worktree add "{{project_root}}/.worktrees/{{current_story}}" "{{branch_prefix}}{{branch_name}}" 2>&1`.
720
+ <!-- PR 5: epic granularity share one branch per epic instead of one per story.
721
+ Under worktree.enabled=false + granularity=epic (nano default),
722
+ subsequent stories of the same epic check out the epic branch in
723
+ place and commit there; no worktree is created. -->
724
+ <check if="{{granularity}} is epic">
725
+ <action>Override `{{branch_name}}` = `{{epic_branch_name}}` (shared across all stories in this epic).</action>
726
+ <check if="{{is_first_story_of_epic}} is true">
727
+ <action>Log: "Epic granularity: creating epic branch {{branch_prefix}}{{epic_branch_name}} for the first story of epic {{epic_id}}"</action>
728
+ </check>
729
+ <check if="{{is_first_story_of_epic}} is false">
730
+ <action>Log: "Epic granularity: reusing existing epic branch {{branch_prefix}}{{epic_branch_name}} for story {{current_story}}"</action>
731
+ </check>
732
+ </check>
733
+
734
+ <check if="{{worktree_enabled}} is false">
735
+ <action>**In-place branching** (granularity=epic or worktree.enabled=false): `git checkout -B {{branch_prefix}}{{branch_name}} 2>&1`. No worktree. Set `{{in_worktree}}` = false. Skip the worktree-add block below.</action>
736
+ </check>
737
+
738
+ <check if="{{worktree_enabled}} is true">
739
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js start --story "{{current_story}}" --phase "worktree.add" --project-root "{{project_root}}"` — ignore failures.</action>
740
+ <action>**Create worktree.** Try: `git worktree add "{{project_root}}/.worktrees/{{current_story}}" -b "{{branch_prefix}}{{branch_name}}" 2>&1`. If it fails because the branch already exists, retry without `-b`: `git worktree add "{{project_root}}/.worktrees/{{current_story}}" "{{branch_prefix}}{{branch_name}}" 2>&1`.
434
741
 
435
742
  If both fail (disk/permissions): log "WARN: worktree add failed — continuing without isolation", set `{{in_worktree}}` = false, and fall back to branch-only mode: `git checkout -b {{branch_prefix}}{{branch_name}}` (retry without `-b` if branch exists). HALT only if the checkout also fails. Git push/PR still work on the branch.
436
- </action>
743
+ </action>
744
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js end --story "{{current_story}}" --phase "worktree.add" --project-root "{{project_root}}"` — ignore failures.</action>
745
+ </check>
437
746
 
438
- <check if="worktree add succeeded">
747
+ <check if="{{worktree_enabled}} is true AND worktree add succeeded">
439
748
  <action>`cd {{project_root}}/.worktrees/{{current_story}}`. All subsequent commands run from here. Set `{{worktree_path}}` = this path.</action>
440
- <action>**Init submodules** if `.gitmodules` exists (check with your file-exists tool or `node -e "process.exit(require('fs').existsSync('.gitmodules')?0:1)"`). Run `git submodule update --init --recursive` (~30s). On failure/hang: warn "Submodule init failed (may need auth). Continuing." and proceed.</action>
749
+ <action>**Disable gc.auto on this worktree** (PR 10): save original value `{{original_gc_auto_worktree}}` = output of `node {{project_root}}/_Sprintpilot/scripts/git-portable.js config-get gc.auto --default unset --project-root "{{project_root}}/.worktrees/{{current_story}}"`, then `git -C {{project_root}}/.worktrees/{{current_story}} config --local gc.auto 0`.</action>
750
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js start --story "{{current_story}}" --phase "worktree.submodule-init" --project-root "{{project_root}}"` — ignore failures.</action>
751
+ <action>**Init submodules** if `.gitmodules` exists (file-exists check via `node -e "process.exit(require('fs').existsSync('.gitmodules')?0:1)"`). PR 10 fast-path:
752
+ Resolve the main repo's common git dir into a captured variable `{{git_common}}` (cross-platform; replaces a `$(...)` shell substitution):
753
+ `node {{project_root}}/_Sprintpilot/scripts/git-portable.js common-dir --project-root "{{project_root}}"`. Trim trailing whitespace; the script exits 1 if git can't resolve the dir — log a warning and skip the submodule fast-path, falling through to the degraded-mode branch below.
754
+ For each submodule path in `.gitmodules`:
755
+ 1. `node {{project_root}}/_Sprintpilot/scripts/submodule-lock.js acquire --submodule "<path>" --project-root "{{project_root}}"` — serializes concurrent submodule updates across worktrees.
756
+ 2. With git ≥ 2.18 (confirmed at boot by check-prereqs): wrap the update with retry to survive ref-lock contention:
757
+ `node {{project_root}}/_Sprintpilot/scripts/with-retry.js -- git -C {{project_root}}/.worktrees/{{current_story}} submodule update --init --recursive --reference "{{git_common}}" --jobs=4 -- <path>`
758
+ With older git (degraded mode flagged by check-prereqs): fall back to `git -C ... submodule update --init --recursive -- <path>`.
759
+ 3. `node {{project_root}}/_Sprintpilot/scripts/submodule-lock.js release --submodule "<path>" --project-root "{{project_root}}"` (best-effort, ignore failures).
760
+ If the loop fails or hangs: warn "Submodule init failed (may need auth). Continuing." and proceed.
761
+ </action>
762
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js end --story "{{current_story}}" --phase "worktree.submodule-init" --project-root "{{project_root}}"` — ignore failures.</action>
441
763
  <action>Set `{{in_worktree}}` = true</action>
442
764
  </check>
443
765
  <action>Update `{state_file}` (write to the worktree copy since cwd is now the worktree)</action>
444
766
  </check>
445
767
 
446
768
  <action>Update `{state_file}` with STATE_FIELDS (set `current_bmad_step = executing`, `completed_skill = <previous skill>`).</action>
769
+ <check if="{{coalesce_state_writes}} is true">
770
+ <action>Mirror critical keys to the shard (bypasses batching for crash-recovery correctness):
771
+ `node {{project_root}}/_Sprintpilot/scripts/state-shard.js batch --story sprint --json "{\"current_bmad_step\":\"executing\",\"current_story\":\"{{current_story}}\",\"completed_skill\":\"<previous skill>\"}" --project-root "{{project_root}}"` — ignore failures.
772
+ </action>
773
+ </check>
447
774
 
448
775
  <!-- Autopilot menu handling rules apply — see AUTOPILOT RULES section above -->
449
776
 
777
+ <!-- PHASE TIMING: emit start/end around every skill invocation.
778
+ Use `{{current_story}}` when set, else the sentinel `sprint` for
779
+ sprint-level skills (bmad-help, bmad-sprint-planning, etc).
780
+ The script is a silent no-op when autopilot.phase_timings is false. -->
781
+ <action>Set `{{timing_story}}` = `{{current_story}}` if non-empty, else `sprint`.</action>
782
+ <!-- Single-call timing via `mark`. The previous start/INVOKE/end triplet
783
+ was reliably broken in long sessions (LLM skipped the `end` call
784
+ ~50% of the time), so most skills had no duration recorded. `mark`
785
+ auto-computes the duration of the PREVIOUS phase from a small marker
786
+ file — one call per transition, no open-phase failure mode. -->
787
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story "{{timing_story}}" --phase "skill.{{next_skill}}" --project-root "{{project_root}}"` — ignore failures.</action>
450
788
  <action>INVOKE `{{next_skill}}` skill using the Skill tool</action>
451
789
  <action>Mark task "{{next_skill}}" as `completed`</action>
452
790
 
@@ -457,8 +795,28 @@ Resolve:
457
795
 
458
796
  <step n="4" goal="Handle skill completion and route to next action">
459
797
 
798
+ <!-- PR 4 NANO ROUTING: quick-dev completion handler.
799
+ Quick-dev's one-shot (step-oneshot.md:44) already ran Implement →
800
+ Review → Classify → Commit internally. Autopilot skips the external
801
+ bmad-code-review step and jumps straight to step 7 (mark story
802
+ done). Escalation safety net: if tests fail or classify severity is
803
+ high, flip implementation_flow to full for the rest of the session. -->
804
+ <check if="{{completed_skill}} was bmad-quick-dev">
805
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js start --story "{{current_story}}" --phase "tests.run" --project-root "{{project_root}}"` — ignore failures.</action>
806
+ <action>Verify tests ran — if not, run them now: report `N/N passed`. Record pass/fail into `{{tests_passed}}`.</action>
807
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js end --story "{{current_story}}" --phase "tests.run" --project-root "{{project_root}}"` — ignore failures.</action>
808
+ <action>Read quick-dev's Classify severity from its stdout/output. If its output mentions `severity: high` or a failing classify, set `{{quickdev_severity_high}}` = true.</action>
809
+ <check if="{{tests_passed}} is false OR {{quickdev_severity_high}} is true">
810
+ <action>**Escalation** — flip the session-scoped flow to `full`: set `{{implementation_flow}}` = `full`. Do NOT write this back to config.yaml; it is session-only. Log decision: `category=scope, phase=autopilot:escalation, impact=medium, "nano story {{current_story}} triggered fallback (tests_passed={{tests_passed}}, severity_high={{quickdev_severity_high}}) — subsequent stories use full cycle"` to `{decision_log_file}`.</action>
811
+ </check>
812
+ <action>Set `{{next_skill}}` = "(none)" — quick-dev handled review + commit internally per BMad step-oneshot.md.</action>
813
+ <goto step="7">Mark story done</goto>
814
+ </check>
815
+
460
816
  <check if="{{completed_skill}} was bmad-dev-story">
817
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js start --story "{{current_story}}" --phase "tests.run" --project-root "{{project_root}}"` — ignore failures.</action>
461
818
  <action>Verify tests ran — if not, run them now: report `N/N passed`</action>
819
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js end --story "{{current_story}}" --phase "tests.run" --project-root "{{project_root}}"` — ignore failures.</action>
462
820
  <action>**Log decisions** — review implementation choices made during dev-story and append entries to `{decision_log_file}` for any architecture, test-strategy, dependency, scope, or workaround decisions (see DECISION LOGGING section)</action>
463
821
 
464
822
  <!-- GIT: Lint, stage, and commit after dev-story -->
@@ -476,7 +834,9 @@ Resolve:
476
834
  - `{patch-title}` → from review finding title, fallback to "code review fix"
477
835
  Read the commit template from `git.commit_templates.story` in config (default: `feat({epic}): {story-title} ({story-key})`).
478
836
  Then run:
479
- `node {{project_root}}/_Sprintpilot/scripts/stage-and-commit.js --message "feat({{epic}}): {{story-title}} ({{current_story}})" --allowlist {{project_root}}/_Sprintpilot/.secrets-allowlist`
837
+ `node {{project_root}}/_Sprintpilot/scripts/log-timing.js start --story "{{current_story}}" --phase "git.commit" --project-root "{{project_root}}"` (ignore failures), then
838
+ `node {{project_root}}/_Sprintpilot/scripts/stage-and-commit.js --message "feat({{epic}}): {{story-title}} ({{current_story}})" --allowlist {{project_root}}/_Sprintpilot/.secrets-allowlist`, then
839
+ `node {{project_root}}/_Sprintpilot/scripts/log-timing.js end --story "{{current_story}}" --phase "git.commit" --project-root "{{project_root}}"` (ignore failures).
480
840
  Output: commit SHA. Set `{{story_commit}}` = output.
481
841
  Warnings (secrets, large files) printed to stderr — review but don't halt unless user says to.
482
842
  </action>
@@ -513,6 +873,39 @@ Resolve:
513
873
  </action>
514
874
  </check>
515
875
 
876
+ <check if="{{completed_skill}} was bmad-sprint-planning">
877
+ <!-- PR-follow-up: sprint-planning populates development_status for the
878
+ first time. Recalculate stories_remaining so the step-2 "sprint
879
+ complete" gate doesn't fire spuriously on the next iteration. -->
880
+ <action>**Recalculate `{{stories_remaining}}`** — run:
881
+ `node {{project_root}}/_Sprintpilot/scripts/list-remaining-stories.js --status-file "{status_file}" --format envelope`
882
+ Parse stdout as `{"remaining":[...],"state":"..."}` → `{{stories_remaining}}` = `.remaining`. Update `{state_file}` with the new value.</action>
883
+ </check>
884
+
885
+ <!-- 2.0.2: auto-infer inter-story dependencies via the autopilot's active
886
+ LLM session. Replaces the hand-authored dependencies.yaml step that
887
+ users never discovered. Gated on autopilot.auto_infer_dependencies
888
+ (default true on small/medium/large; false on nano and legacy).
889
+ The script never calls an LLM — it ingests our JSON output via stdin,
890
+ validates, and writes the sidecar with an AUTO-INFERRED marker.
891
+ Hand-authored sidecars (no marker) are detected and respected. -->
892
+ <check if="{{completed_skill}} was bmad-sprint-planning">
893
+ <action>Resolve `{{auto_infer_dependencies}}` from `autopilot.auto_infer_dependencies` (default false) via:
894
+ `node {{project_root}}/_Sprintpilot/scripts/resolve-profile.js get autopilot.auto_infer_dependencies --project-root "{{project_root}}"`</action>
895
+ <check if="{{auto_infer_dependencies}} is true">
896
+ <action>Set `{{distinct_epic_ids}}` = sorted unique leading-numeric segments of every key in `{{stories_remaining}}` (e.g. `1-1-foo` → `1`). Skip the loop if empty (pre-planning edge case).</action>
897
+ <foreach var="{{epic_id}}" in="{{distinct_epic_ids}}">
898
+ <action>Generate the inference prompt: run `node {{project_root}}/_Sprintpilot/scripts/infer-dependencies.js scaffold-prompt --epic "{{epic_id}}" --project-root "{{project_root}}"` and capture stdout as `{{infer_prompt}}`.</action>
899
+ <action>**Execute the inference inline.** The prompt instructs you to read four files (sprint-status.yaml, epics.md, architecture.md, dependencies.yaml-if-present) and emit ONE JSON object with shape `{"version":1,"epic":"{{epic_id}}","dependencies":{...},"rationale":{...}}`. Read those files NOW. Reason about which stories share modules, files, or describe themselves as building on each other (the prompt's RULES section enumerates the legitimate triggers). Emit the JSON envelope. No prose, no markdown fences. Set `{{infer_json}}` to the envelope.</action>
900
+ <action>Pipe `{{infer_json}}` into `node {{project_root}}/_Sprintpilot/scripts/infer-dependencies.js write --epic "{{epic_id}}" --project-root "{{project_root}}"`. Parse stdout JSON.
901
+ - On exit 0 (`wrote: true`) → log: `"Inferred {{response.edges_inferred}} dependency edges for epic {{epic_id}} (hash {{response.hash}}; user_overrides_preserved: {{response.user_overrides_preserved}})"`.
902
+ - On exit 2 (`wrote: false`, `reason: existing-hand-authored`) → log: `"Existing dependencies.yaml is hand-authored — skipping LLM inference for epic {{epic_id}}; user-authored sidecar wins"`. Do NOT halt.
903
+ - On exit 1 (validation error) → log the error envelope verbatim, then log: `"LLM dependency inference rejected for epic {{epic_id}} — falling back to linear ordering. Re-run /sprint-autopilot-on to retry, or hand-author the sidecar."`. Do NOT halt the autopilot — `resolve-dag.js` will fall back to the `ordering` strategy on dispatch.
904
+ </action>
905
+ </foreach>
906
+ </check>
907
+ </check>
908
+
516
909
  <check if="{{completed_skill}} was bmad-sprint-planning AND {{git_enabled}}">
517
910
  <action>If `{{has_origin}}` is true, run `git fetch origin` (log a warning on failure and continue — do not abort).
518
911
  If `{{has_origin}}` is false, skip the fetch.</action>
@@ -520,7 +913,19 @@ Resolve:
520
913
  </check>
521
914
 
522
915
  <check if="{{completed_skill}} was bmad-create-story">
523
- <action>Verify story file has `- [ ]` checkboxes in Tasks/Subtasks section. If missing, re-run create-story.</action>
916
+ <!-- PR-follow-up (greenfield e2e fix): bmad-create-story sometimes omits
917
+ the Tasks/Subtasks section entirely. Re-running the skill doesn't
918
+ always fix it. Fall back to a deterministic script so the story
919
+ file ALWAYS has checkboxes dev-story can mark — no LLM prose in
920
+ this path. -->
921
+ <action>Locate the story file for `{{current_story}}` — glob
922
+ `_bmad-output/**/story-{{current_story}}.md` and
923
+ `_bmad-output/**/{{current_story}}.md`. Set `{{story_file_path}}`.
924
+ </action>
925
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/inject-tasks-section.js --story-file "{{story_file_path}}"`
926
+ — idempotent. The script checks for an existing `## Tasks` / `## Subtasks` section with at least one `- [ ]` checkbox and returns `{"action":"skip"}` if present. Otherwise it extracts every AC entry from the bounded `## Acceptance Criteria` section and appends a correctly-formatted section with one `- [ ] <AC>` bullet per entry.
927
+ Parse stdout JSON; the script never throws on a well-formed story file, so treat any non-zero exit as a log-and-proceed signal (the sprint can still complete without this guarantee — only the "task checkboxes marked" assertion degrades).
928
+ </action>
524
929
  </check>
525
930
 
526
931
  <check if="{{completed_skill}} was bmad-create-story AND {{git_enabled}}">
@@ -557,7 +962,11 @@ Resolve:
557
962
  Log: "next_skill was empty but undone stories remain — resolved to {{next_skill}} for {{current_story}}".
558
963
  </action>
559
964
  <check if="all stories in status_file are done">
560
- <goto step="10">Sprint complete</goto>
965
+ <!-- Same finalize-pending handoff as step 2 — never run step 10 in
966
+ the session that first noticed sprint-complete. -->
967
+ <action>Update `{state_file}`: `current_bmad_step = "sprint-finalize-pending"`, `current_story = null`, `next_skill = null`, `stories_remaining = []`.</action>
968
+ <action>Release lock: `node {{project_root}}/_Sprintpilot/scripts/lock.js release` — ignore failures.</action>
969
+ <action>Report: "Sprint complete detected during skill-recovery. Pausing for fresh-context finalization — run /sprint-autopilot-on once more." Then HALT.</action>
561
970
  </check>
562
971
  </check>
563
972
 
@@ -603,6 +1012,8 @@ For any finding that is DISMISSED (contradicts AC or is a false positive):
603
1012
  <action>Mark "[story] Apply patches" → `completed`</action>
604
1013
 
605
1014
  <!-- Re-run code review to sync sprint-status.yaml — patches resolved all findings, so code-review will now set story to done -->
1015
+ <!-- Single-call timing (mark) — see step-3 comment for rationale. -->
1016
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story "{{current_story}}" --phase "skill.bmad-code-review.rereview" --project-root "{{project_root}}"` — ignore failures.</action>
606
1017
  <action>Re-invoke `bmad-code-review` using the Skill tool.
607
1018
  The review layers already ran — this pass will see zero unresolved findings and set the story status to `done` in sprint-status.yaml (code-review owns that transition per step-04-present.md:92).
608
1019
  Instruct: "Re-verify code review for story {{current_story}} — all patch findings have been applied. Update story status accordingly."
@@ -627,8 +1038,19 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
627
1038
  - Final test count: `N/N passed`
628
1039
  </action>
629
1040
 
630
- <!-- GIT: Push, PR, exit worktree -->
631
- <check if="{{git_enabled}} AND {{in_worktree}}">
1041
+ <!-- PR 5: epic granularity — defer push/PR until the LAST story of the epic.
1042
+ For intermediate stories (granularity=epic AND not is_last_story_of_epic):
1043
+ the work is already committed to the epic branch locally via stage-and-
1044
+ commit.js; no push or PR is attempted. The epic's last story pushes
1045
+ the accumulated commits and opens one PR for the whole epic. -->
1046
+ <check if="{{granularity}} is epic AND {{is_last_story_of_epic}} is false">
1047
+ <action>Log: "Epic granularity: skipping push/PR for {{current_story}} — will push at end of epic {{epic_id}}"</action>
1048
+ <action>Set `{{push_status}}` = "deferred", `{{pr_url}}` = "DEFERRED", `{{merge_status}}` = "deferred"</action>
1049
+ <!-- Skip the whole git-push block; fall through to the artifact-sync block below. -->
1050
+ </check>
1051
+
1052
+ <!-- GIT: Push, PR, exit worktree (story granularity OR last story of an epic) -->
1053
+ <check if="{{git_enabled}} AND ({{granularity}} is story OR {{is_last_story_of_epic}} is true) AND ({{in_worktree}} OR {{worktree_enabled}} is false)">
632
1054
  <check if="{{push_auto}} is true">
633
1055
  <action>**Push branch**.
634
1056
  Run: `git push -u origin {{branch_prefix}}{{branch_name}} 2>&1`
@@ -666,14 +1088,20 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
666
1088
  </action>
667
1089
 
668
1090
  <check if="{{create_pr}} is false OR {{platform}} is git_only OR {{pr_url}} is null or SKIPPED">
669
- <action>**Merge story branch to main.** If `{{has_origin}}` is false (local-only), substitute `origin/{{base_branch}}` → `{{base_branch}}` and skip all `git push origin` / `git fetch origin` calls below.
670
- 1. `git checkout -B {{base_branch}} origin/{{base_branch}}` then `git merge {{branch_prefix}}{{branch_name}} --no-edit`.
671
- 2. On success: `git push origin {{base_branch}}`, set `{{merge_status}}` = "merged".
672
- 3. On conflict: `git merge --abort`, `git fetch origin`, re-checkout base, retry merge once. On retry success: push + merged. On retry failure: `{{merge_status}}` = "failed", log warning, continue — the branch is preserved and boot reconciliation retries next session.
1091
+ <action>**Merge story/epic branch to main.** If `{{has_origin}}` is false (local-only), substitute `origin/{{base_branch}}` → `{{base_branch}}` and skip all `git push origin` / `git fetch origin` calls below.
1092
+ Choose merge strategy by `{{squash_on_merge}}` (PR 5): if true, use `git merge --squash` + single commit; otherwise standard merge commit.
1093
+ 1. `git checkout -B {{base_branch}} origin/{{base_branch}}`
1094
+ 2. If `{{squash_on_merge}}` is true:
1095
+ `git merge --squash {{branch_prefix}}{{branch_name}}` then
1096
+ `git commit -m "feat({{epic_id}}): epic {{epic_id}} ({{branch_prefix}}{{branch_name}})"`.
1097
+ Otherwise: `git merge {{branch_prefix}}{{branch_name}} --no-edit`.
1098
+ 3. On success: `git push origin {{base_branch}}`, set `{{merge_status}}` = "merged".
1099
+ 4. On conflict: `git merge --abort`, `git fetch origin`, re-checkout base, retry merge once. On retry success: push + merged. On retry failure: `{{merge_status}}` = "failed", log warning, continue — the branch is preserved and boot reconciliation retries next session.
1100
+ 5. **PR 12 cross-epic conflict interlock**: if this merge conflict involved two independent epics AND `{{parallel_epics}}` is true, set `{{cross_epic_disabled_this_session}}` = true and log "EXPERIMENTAL: cross-epic merge conflict detected; disabling parallel_epics for the remainder of this session." The flag resets on next session start.
673
1101
 
674
1102
  `{{merge_status}}` is persisted by the sync-status.js call later in this step (via `--merge-status`). Do NOT call sync-status.js here — it does full block replacement and would destroy other fields.
675
1103
  </action>
676
- <check if="{{cleanup_on_merge}} is true">
1104
+ <check if="{{cleanup_on_merge}} is true AND {{in_worktree}} is true">
677
1105
  <action>**Cleanup worktree** (ignore failures — may already be gone): `git worktree remove .worktrees/{{current_story}} --force` then `git worktree prune`</action>
678
1106
  </check>
679
1107
  </check>
@@ -701,6 +1129,17 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
701
1129
  <action>**Increment `{{session_stories_done}}` by 1** — this is the ONLY place the counter ticks up. It runs only after the story's full implementation cycle (dev-story GREEN + code-review + patches + artifacts committed + optional push/PR). Creating a story file in step 3 never increments this counter.</action>
702
1130
  <action>Remove `{{current_story}}` from `{{stories_remaining}}` list</action>
703
1131
 
1132
+ <!-- PR 6 STORY-BOUNDARY FLUSH: if coalescing is on, flush the sprint
1133
+ shard's pending buffer now (writes accumulated non-critical fields
1134
+ to the shard) and merge all shards into the authoritative
1135
+ autopilot-state.yaml / decision-log.yaml. Fast on a single-story
1136
+ sprint; amortized when multiple stories complete per session. -->
1137
+ <check if="{{coalesce_state_writes}} is true">
1138
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/state-shard.js flush --story sprint --project-root "{{project_root}}"` — ignore failures.</action>
1139
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/state-shard.js flush --story "{{current_story}}" --project-root "{{project_root}}"` — ignore failures (no-op if no per-story shard was ever batched).</action>
1140
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/merge-shards.js --project-root "{{project_root}}"` — ignore failures. Produces merged autopilot-state.yaml + decision-log.yaml.</action>
1141
+ </check>
1142
+
704
1143
  <action>Report: "Story {{current_story}} done — N/N passing{{#if pr_url}} — PR: {{pr_url}}{{/if}}"</action>
705
1144
 
706
1145
  <action>Check if ALL stories in this epic are `done`</action>
@@ -788,6 +1227,13 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
788
1227
  </check>
789
1228
 
790
1229
  <action>Update `{state_file}` with STATE_FIELDS.</action>
1230
+ <check if="{{coalesce_state_writes}} is true">
1231
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/state-shard.js flush --story sprint --project-root "{{project_root}}"` — ignore failures.</action>
1232
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/merge-shards.js --project-root "{{project_root}}"` — ignore failures.</action>
1233
+ </check>
1234
+
1235
+ <!-- Phase-timing session snapshot (no-op if autopilot.phase_timings is false). -->
1236
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/summarize-timings.js --session-only --format md --quiet --project-root "{{project_root}}"` — ignore failures. The stdout line is the artifact path; include it in the checkpoint report if non-empty.</action>
791
1237
 
792
1238
  <action>Read `{decision_log_file}` — count medium/high decisions from this session's stories</action>
793
1239
 
@@ -827,15 +1273,88 @@ No work will be repeated.
827
1273
 
828
1274
  <step n="10" goal="Sprint complete — emit summary and next steps">
829
1275
 
830
- <!-- GIT: Exit worktree if still in one -->
1276
+ <!-- CRITICAL-PATH-FIRST: these 6 actions MUST run before anything else in
1277
+ step 10. They put the repo into a correct terminal state — orphan
1278
+ worktrees removed, lock released, artifacts committed to main,
1279
+ sprint-complete state marked, task checkboxes marked, final verification.
1280
+ Even if the rest of step 10 is interrupted (SIGTERM, LLM improvises an
1281
+ early summary, context exhaustion), the harness invariants hold. DO NOT
1282
+ reorder or skip these — the order matters: worktrees-before-lock so a
1283
+ concurrent autopilot session doesn't see orphans, lock-before-commit
1284
+ so commits run with the repo already unlocked, etc. -->
1285
+
1286
+ <!-- GIT: Exit worktree if still in one (prerequisite for the rest) -->
831
1287
  <check if="{{in_worktree}}">
832
1288
  <action>`cd {{project_root}}` — return to project root</action>
833
1289
  <action>Set `{{in_worktree}}` = false</action>
834
1290
  </check>
835
1291
 
836
- <action>Verify: all stories `done`, all retrospectives `done` in `{status_file}`</action>
1292
+ <!-- Run the mark-tasks helper AS THE VERY FIRST action of step 10.
1293
+ The LLM reliably executes the first critical action but sometimes
1294
+ skips later ones once it feels "done" with the sprint. Putting this
1295
+ first ensures every done-story's checkboxes get marked before the
1296
+ LLM loses focus. -->
1297
+ <action>**[CRITICAL 1/7] Mark all done stories' Task checkboxes** — deterministic helper. Run:
1298
+ `node {{project_root}}/_Sprintpilot/scripts/mark-done-stories-tasks.js --status-file "{status_file}" --project-root "{{project_root}}"` — ignore failures. This finds every story with status="done" in sprint-status.yaml and replaces `- [ ]` with `- [x]` in its story file.
1299
+ </action>
1300
+
1301
+ <action>**[CRITICAL 2/7] Remove autopilot-owned worktrees** — no-op if none exist. Only worktrees we created (paths inside `{{project_root}}/.worktrees/`) are removed; any user-placed worktree elsewhere is left untouched.
1302
+ <check if="{{git_enabled}}">
1303
+ Run: `git worktree list --porcelain` to enumerate. For each worktree whose path starts with `{{project_root}}/.worktrees/`:
1304
+ - `git worktree remove <path> --force 2>&1` (ignore failures — best-effort).
1305
+ Then once: `git worktree prune 2>&1` (ignore failures).
1306
+ This is the SINGLE authoritative cleanup site for autopilot-owned worktrees — do not wait for a later "safety net" cleanup that might not run.
1307
+ </check>
1308
+ </action>
1309
+
1310
+ <action>**[CRITICAL 3/7] Release lock immediately** — before any slow operation:
1311
+ Run: `node {{project_root}}/_Sprintpilot/scripts/lock.js release` — ignore failures. Idempotent.
1312
+ </action>
1313
+
1314
+ <action>**[CRITICAL 4/7] Commit any final artifacts + planning docs to main** — even if the rest of step 10 is interrupted. The checkout is deliberately non-destructive: we never use `-B` or `-f` (those silently wipe local commits or modifications). If the working tree is dirty, the commit step still runs; if the checkout fails, the commit is skipped with a warning rather than forced.
1315
+ <check if="{{git_enabled}}">
1316
+ 1. `git switch {{base_branch}} 2>&1 || git checkout {{base_branch}} 2>&1` (plain switch; if `{{base_branch}}` doesn't exist locally, `git checkout --track origin/{{base_branch}}` once to create it tracking origin). Never use `-B` or `-f` — they discard work.
1317
+ 2. If `{{has_origin}}` is true: `git pull --ff-only origin {{base_branch}} 2>&1` (fast-forward only; if the pull would require a merge, log a warning and continue — do NOT `reset --hard`).
1318
+ 3. Stage artifacts via the cross-platform helper (replaces `git add … 2>/dev/null || true`):
1319
+ `node {{project_root}}/_Sprintpilot/scripts/git-portable.js safe-add _bmad-output/planning-artifacts _bmad-output/implementation-artifacts/sprint-status.yaml _bmad-output/implementation-artifacts/decision-log.yaml _bmad-output/implementation-artifacts/git-status.yaml _bmad-output/stories _bmad-output/implementation-artifacts/retrospectives --project-root "{{project_root}}"`
1320
+ The script filters paths to those that actually exist on disk before invoking git, so missing paths are skipped silently with no shell error suppression needed.
1321
+ 4. If `git diff --cached --quiet` exits non-zero: `git commit -m "docs: sprint artifacts — planning + stories + retrospective"` then (if `{{has_origin}}` is true) `git push origin {{base_branch}}` (warn on failure, do not halt).
1322
+ </check>
1323
+ </action>
1324
+
1325
+ <action>**[CRITICAL 5/7] Mark sprint-complete state**: update `{state_file}`: `current_bmad_step = "sprint-complete"`, `current_story = null`, `next_skill = null`, `stories_remaining = []`, `session_stories_done = {{session_stories_done}}`. This signals the test harness and any next /sprint-autopilot-on invocation that the sprint is genuinely done.</action>
1326
+
1327
+ <action>**[CRITICAL 6/7] Verify** — run:
1328
+ `node {{project_root}}/_Sprintpilot/scripts/list-remaining-stories.js --status-file "{status_file}" --format envelope`
1329
+ Parse stdout as JSON. If `.state` is `sprint-complete` AND `.remaining` is empty, the sprint is verified complete. If `.state` is anything else (e.g. `sprint-in-progress` with leftover stories), log a warning — but do NOT revert the above actions. The worktrees are gone; the lock is released; the artifacts are committed; the state is marked complete; task checkboxes are marked. Manual recovery only.
1330
+ </action>
1331
+
1332
+ <!-- Final terminal-state marker. Kept inside the CRITICAL block because
1333
+ the previous "Delete state_file" action at the tail of step 10 was
1334
+ being skipped by context-rot in long sessions (observed in e2e runs:
1335
+ "autopilot-state.yaml still exists" warning), leaving a stale state
1336
+ file that confused the next /sprint-autopilot-on boot. CRITICAL 5/7
1337
+ already wrote sprint-complete as a belt-and-suspenders in case this
1338
+ delete crashes mid-run; step 1 also short-circuits on a leftover
1339
+ sprint-complete state so accidental re-invocation doesn't loop. -->
1340
+ <action>**[CRITICAL 7/7] Delete state_file** — `node -e "require('node:fs').rmSync('{state_file}', { force: true })"` (idempotent; never halt on failure). Removes `autopilot-state.yaml` so the next invocation starts cleanly. CRITICAL 5/7 already wrote `current_bmad_step = sprint-complete` to the same file as a crash-safe marker, so if this delete fails mid-run the next session still sees the terminal state.</action>
1341
+
1342
+ <!-- Close the last open `mark` phase so its duration gets recorded.
1343
+ Without this, the very last skill's duration would be lost (no
1344
+ subsequent mark fires after sprint-complete). -->
1345
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story sprint --phase _end --project-root "{{project_root}}"` — ignore failures.</action>
1346
+
837
1347
  <action>Run full test suite — report `N/N passed`</action>
838
1348
 
1349
+ <!-- PR 6 SPRINT-COMPLETE FLUSH + ARCHIVE (no-op if coalescing is off). -->
1350
+ <check if="{{coalesce_state_writes}} is true">
1351
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/state-shard.js flush --story sprint --project-root "{{project_root}}"` — ignore failures.</action>
1352
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/merge-shards.js --archive --project-root "{{project_root}}"` — ignore failures. --archive moves merged shards to .archive/layer-&lt;timestamp&gt;/ (the script generates the ISO timestamp itself; no shell `$(...)` substitution needed) so the next sprint starts clean.</action>
1353
+ </check>
1354
+
1355
+ <!-- Final phase-timing hotspot report (no-op if autopilot.phase_timings is false). -->
1356
+ <action>Run: `node {{project_root}}/_Sprintpilot/scripts/summarize-timings.js --format md --quiet --project-root "{{project_root}}"` — ignore failures. The stdout line is the artifact path; include it in the sprint report if non-empty.</action>
1357
+
839
1358
  <!-- Generate project documentation after sprint completion -->
840
1359
  <action>**Resolve stack** — set `{{stack}}` = `{ name, install_cmd, run_cmd, test_cmd }` using the first successful source:
841
1360
 
@@ -866,21 +1385,32 @@ No work will be repeated.
866
1385
  <action>**Generate documentation** — invoke `bmad-document-project`. If unavailable or it fails, write a minimal README using `{{stack}}`: project name + description (from brief/PRD); install/run/test lines for each non-null `{{stack.*_cmd}}` (omit lines where null); architecture overview if `architecture.md` exists.</action>
867
1386
 
868
1387
  <check if="{{git_enabled}}">
869
- <action>**Commit final artifacts + docs to main.**
870
- 1. `git checkout -B {{base_branch}} origin/{{base_branch}}`
871
- 2. `git add _bmad-output/ README.md docs/` (ignore missing-path errors)
872
- 3. If `git diff --cached --quiet` exits non-zero: `git commit -m "docs: project documentation and final artifacts"` then `git push origin {{base_branch}}` (warn on push failure).
1388
+ <action>**Commit README + docs to main** (sprint artifacts already committed in CRITICAL 4/7 above — this picks up README.md / docs/ that were generated later by bmad-document-project). Non-destructive checkout — never `-B` or `-f`:
1389
+ 1. `git switch {{base_branch}} 2>&1 || git checkout {{base_branch}} 2>&1`.
1390
+ 2. If `{{has_origin}}` is true: `git pull --ff-only origin {{base_branch}} 2>&1` (warn on non-ff; do NOT reset).
1391
+ 3. Stage docs via the cross-platform helper:
1392
+ `node {{project_root}}/_Sprintpilot/scripts/git-portable.js safe-add README.md docs/ --project-root "{{project_root}}"`
1393
+ (paths are filtered to those that exist; missing paths are skipped).
1394
+ 4. If `git diff --cached --quiet` exits non-zero: `git commit -m "docs: project documentation"` then (if `{{has_origin}}`) `git push origin {{base_branch}}` (warn on push failure, do not halt).
873
1395
  </action>
874
1396
  </check>
875
1397
 
876
1398
  <action>**Collect report data** from `{status_file}` (stories grouped by epic with titles, totals, final test count; PR/MR URLs, patch/dismissed counts per story if git_enabled) and `{decision_log_file}` (medium/high-impact decisions; counts of `review-accept`, `review-triage`, code-review rounds; per-story patches-applied / findings-dismissed).</action>
877
1399
 
878
1400
  <check if="{{git_enabled}}">
879
- <action>**Cleanup remaining worktrees** (safety net): `git worktree list --porcelain` → for each non-main worktree: `git worktree remove <path> --force` then `git worktree prune` (log + continue on failure).</action>
880
- <action>Release lock: `node {{project_root}}/_Sprintpilot/scripts/lock.js release`</action>
1401
+ <!-- Worktree cleanup already ran as CRITICAL 2/7 above. Intentionally
1402
+ no duplicate here relying on early cleanup to avoid orphans
1403
+ when the LLM short-circuits late actions. -->
1404
+ <action>(No-op: worktree cleanup already executed in CRITICAL 2/7.)</action>
1405
+ <!-- PR 10: restore main-repo gc.auto to its prior value. -->
1406
+ <action>**Restore main-repo gc.auto**:
1407
+ if `{{original_gc_auto_main}}` is "unset": `git config --local --unset gc.auto` (ignore failure — may already be unset).
1408
+ else: `git config --local gc.auto {{original_gc_auto_main}}`.
1409
+ </action>
1410
+ <action>Belt-and-suspenders: `node {{project_root}}/_Sprintpilot/scripts/lock.js release` (already released earlier in step 10; this is idempotent and catches any race where the early release failed).</action>
881
1411
  </check>
882
1412
 
883
- <action>Delete `{state_file}` sprint complete</action>
1413
+ <!-- State file deletion moved to CRITICAL 7/7 above for reliability. -->
884
1414
  <action>Mark master task "Sprintpilot — Full Sprint Execution" → `completed`</action>
885
1415
 
886
1416
  <action>Read template `{{project_root}}/_Sprintpilot/templates/sprint-report.txt`, fill mustache placeholders with the collected data, and print the result verbatim as the final message.</action>