@ikunin/sprintpilot 2.2.30 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +232 -413
  2. package/_Sprintpilot/Sprintpilot.md +76 -6
  3. package/_Sprintpilot/bin/autopilot.js +752 -66
  4. package/_Sprintpilot/lib/orchestrator/action-ledger.js +208 -0
  5. package/_Sprintpilot/lib/orchestrator/adapt.js +93 -15
  6. package/_Sprintpilot/lib/orchestrator/profile-rules.js +7 -16
  7. package/_Sprintpilot/lib/orchestrator/sprint-plan.js +488 -0
  8. package/_Sprintpilot/lib/orchestrator/state-store.js +9 -5
  9. package/_Sprintpilot/lib/orchestrator/user-command-applier.js +107 -0
  10. package/_Sprintpilot/lib/orchestrator/user-commands.js +124 -1
  11. package/_Sprintpilot/lib/orchestrator/verify.js +10 -17
  12. package/_Sprintpilot/manifest.yaml +4 -1
  13. package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +18 -4
  14. package/_Sprintpilot/modules/git/config.yaml +15 -9
  15. package/_Sprintpilot/modules/ma/config.yaml +29 -27
  16. package/_Sprintpilot/scripts/dispatch-layer.js +12 -15
  17. package/_Sprintpilot/scripts/infer-dependencies.js +706 -254
  18. package/_Sprintpilot/scripts/log-timing.js +6 -10
  19. package/_Sprintpilot/scripts/merge-shards.js +21 -23
  20. package/_Sprintpilot/scripts/post-green-gates.js +3 -1
  21. package/_Sprintpilot/scripts/resolve-dag.js +452 -280
  22. package/_Sprintpilot/scripts/sprint-plan.js +1068 -0
  23. package/_Sprintpilot/scripts/state-shard.js +13 -5
  24. package/_Sprintpilot/scripts/summarize-timings.js +2 -3
  25. package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +30 -2
  26. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.orchestrator.md +36 -10
  27. package/_Sprintpilot/skills/sprintpilot-dependency-graph/SKILL.md +63 -0
  28. package/_Sprintpilot/skills/sprintpilot-dependency-graph/workflow.md +227 -0
  29. package/_Sprintpilot/skills/sprintpilot-plan-sprint/SKILL.md +67 -0
  30. package/_Sprintpilot/skills/sprintpilot-plan-sprint/workflow.md +435 -0
  31. package/_Sprintpilot/skills/sprintpilot-sprint-progress/SKILL.md +53 -0
  32. package/_Sprintpilot/skills/sprintpilot-sprint-progress/workflow.md +169 -0
  33. package/lib/commands/install.js +186 -10
  34. package/package.json +1 -1
@@ -19,6 +19,32 @@
19
19
  // the stored alternative as the next action and clears the pending
20
20
  // entry. Validation rejects this kind when no alternative is pending
21
21
  // in state — see user-command-applier.js for the runtime check.
22
+ // trigger_retrospective { reason?: string }
23
+ // Force-routes the orchestrator into RETROSPECTIVE for the current
24
+ // epic regardless of `remaining_stories_in_epic`. Used when the user
25
+ // explicitly wants to close out an epic with deferred stories still
26
+ // in the queue (BMad has no formal `skipped`/`deferred` status for
27
+ // stories, so the orchestrator otherwise counts them as remaining
28
+ // and routes to next-story instead of retro).
29
+ //
30
+ // v2.3.0 — plan-aware mid-flight commands. These operate on
31
+ // sprint-plan.yaml via the Phase 2 primitives. DAG-aware validation
32
+ // lives in the applier (it needs the live plan + helper).
33
+ // reorder_queue { order: string[] }
34
+ // Rewrite priorities so the plan's pending stories match `order`.
35
+ // Validated against the DAG: every upstream of each story must be
36
+ // positioned BEFORE it OR plan-terminal. Inline edit — no phase
37
+ // change.
38
+ // add_to_sprint { story_keys: string[], position?: 'end'|'after:<key>'|<int>, issue_ids?: object }
39
+ // Add stories to plan.stories[]. Each key must exist in sprint-status,
40
+ // be non-terminal there, and not already in plan. Optional issue_ids
41
+ // map populates issue_id per added story.
42
+ // remove_from_sprint { story_keys: string[], mark_status?: 'skipped'|'deferred' }
43
+ // Mark stories with plan_status=skipped (default) or 'deferred'.
44
+ // Downstream-in-plan stories get a warning side effect.
45
+ // replan_sprint { reason?: string }
46
+ // Halt at next story_done boundary and emit invoke_skill for
47
+ // /sprintpilot-plan-sprint. The skill rebuilds the plan from scratch.
22
48
  //
23
49
  // Validation returns { ok: true, command } | { ok: false, errors: string[] }.
24
50
 
@@ -34,10 +60,17 @@ const COMMAND_KINDS = [
34
60
  'change_profile',
35
61
  'pause',
36
62
  'accept_alternative',
63
+ 'trigger_retrospective',
64
+ // v2.3.0 — plan-aware mid-flight commands.
65
+ 'reorder_queue',
66
+ 'add_to_sprint',
67
+ 'remove_from_sprint',
68
+ 'replan_sprint',
37
69
  ];
38
70
 
39
71
  const STORY_KEY_RE = /^[A-Za-z0-9._-]{1,64}$/;
40
72
  const DECISION_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
73
+ const VALID_REMOVE_STATUSES = ['skipped', 'deferred'];
41
74
 
42
75
  function isPlainObject(v) {
43
76
  return typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -73,11 +106,99 @@ function validateOne(cmd) {
73
106
  case 'abort_sprint':
74
107
  case 'force_continue':
75
108
  case 'pause':
76
- case 'accept_alternative': {
109
+ case 'accept_alternative':
110
+ case 'trigger_retrospective': {
77
111
  if ('reason' in cmd && cmd.reason !== undefined && typeof cmd.reason !== 'string')
78
112
  errors.push(`${cmd.kind}.reason must be string when present`);
79
113
  break;
80
114
  }
115
+ case 'reorder_queue': {
116
+ if (!Array.isArray(cmd.order) || cmd.order.length === 0) {
117
+ errors.push('reorder_queue.order must be a non-empty array of story keys');
118
+ break;
119
+ }
120
+ const seen = new Set();
121
+ for (const k of cmd.order) {
122
+ if (!nonEmptyString(k) || !STORY_KEY_RE.test(k)) {
123
+ errors.push(`reorder_queue.order entry ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`);
124
+ continue;
125
+ }
126
+ if (seen.has(k)) {
127
+ errors.push(`reorder_queue.order contains duplicate key ${JSON.stringify(k)}`);
128
+ continue;
129
+ }
130
+ seen.add(k);
131
+ }
132
+ break;
133
+ }
134
+ case 'add_to_sprint': {
135
+ if (!Array.isArray(cmd.story_keys) || cmd.story_keys.length === 0) {
136
+ errors.push('add_to_sprint.story_keys must be a non-empty array');
137
+ break;
138
+ }
139
+ for (const k of cmd.story_keys) {
140
+ if (!nonEmptyString(k) || !STORY_KEY_RE.test(k)) {
141
+ errors.push(
142
+ `add_to_sprint.story_keys entry ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`,
143
+ );
144
+ }
145
+ }
146
+ if (cmd.position !== undefined && cmd.position !== null) {
147
+ const p = cmd.position;
148
+ const isEnd = p === 'end';
149
+ const isAfter = typeof p === 'string' && p.startsWith('after:') && p.length > 6;
150
+ const isInt = typeof p === 'number' && Number.isFinite(p);
151
+ if (!isEnd && !isAfter && !isInt) {
152
+ errors.push(
153
+ "add_to_sprint.position must be 'end', 'after:<key>', or an integer index",
154
+ );
155
+ }
156
+ }
157
+ if (cmd.issue_ids !== undefined && cmd.issue_ids !== null) {
158
+ if (!isPlainObject(cmd.issue_ids)) {
159
+ errors.push('add_to_sprint.issue_ids must be an object map { story_key: issue_id }');
160
+ } else {
161
+ for (const [k, v] of Object.entries(cmd.issue_ids)) {
162
+ if (!STORY_KEY_RE.test(k)) {
163
+ errors.push(
164
+ `add_to_sprint.issue_ids key ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`,
165
+ );
166
+ }
167
+ if (typeof v !== 'string' && v !== null) {
168
+ errors.push(`add_to_sprint.issue_ids[${k}] must be a string or null`);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ break;
174
+ }
175
+ case 'remove_from_sprint': {
176
+ if (!Array.isArray(cmd.story_keys) || cmd.story_keys.length === 0) {
177
+ errors.push('remove_from_sprint.story_keys must be a non-empty array');
178
+ break;
179
+ }
180
+ for (const k of cmd.story_keys) {
181
+ if (!nonEmptyString(k) || !STORY_KEY_RE.test(k)) {
182
+ errors.push(
183
+ `remove_from_sprint.story_keys entry ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`,
184
+ );
185
+ }
186
+ }
187
+ if (cmd.mark_status !== undefined && cmd.mark_status !== null) {
188
+ if (!VALID_REMOVE_STATUSES.includes(cmd.mark_status)) {
189
+ errors.push(
190
+ `remove_from_sprint.mark_status must be one of ${VALID_REMOVE_STATUSES.join(', ')}`,
191
+ );
192
+ }
193
+ }
194
+ break;
195
+ }
196
+ case 'replan_sprint': {
197
+ if ('reason' in cmd && cmd.reason !== undefined && typeof cmd.reason !== 'string') {
198
+ errors.push('replan_sprint.reason must be string when present');
199
+ }
200
+ break;
201
+ }
81
202
  case 'override_decision': {
82
203
  if (!nonEmptyString(cmd.decision_id)) errors.push('override_decision.decision_id required');
83
204
  else if (!DECISION_ID_RE.test(cmd.decision_id))
@@ -118,6 +239,8 @@ function validate(input) {
118
239
  module.exports = {
119
240
  COMMAND_KINDS,
120
241
  VALID_PROFILE_NAMES,
242
+ VALID_REMOVE_STATUSES,
243
+ STORY_KEY_RE,
121
244
  validate,
122
245
  validateOne,
123
246
  };
@@ -188,12 +188,11 @@ function runPostGreenGates(ctx) {
188
188
  }
189
189
  const cp = require('node:child_process');
190
190
  const args = [scriptAbs, '--json', '--project-root', ctx.projectRoot];
191
- // Forward output_limit (v2.2.28). Pre-2.2.28 the typed-profile field
192
- // existed but lint-changed.js used its hardcoded default of 100.
191
+ // Forward output_limit so lint-changed.js honors git.lint.output_limit.
193
192
  if (typeof ctx.profile.lint_output_limit === 'number' && ctx.profile.lint_output_limit > 0) {
194
193
  args.push('--output-limit', String(ctx.profile.lint_output_limit));
195
194
  }
196
- // Forward per-language linter map (v2.2.28). Lets users reorder or
195
+ // Forward the per-language linter map. Users reorder priorities or
197
196
  // disable linters via git.lint.linters.{language}: [list].
198
197
  if (ctx.profile.lint_linters && typeof ctx.profile.lint_linters === 'object') {
199
198
  try {
@@ -459,10 +458,8 @@ function verifyDevGreen(state, out, ctx) {
459
458
  }
460
459
  // Post-GREEN gates: lint-changed + lint-test-pitfalls + ci-parity scan.
461
460
  // Composed pipeline lives in scripts/post-green-gates.js. Only fires
462
- // when profile.lint_enabled === true. Blocking vs non-blocking
463
- // governed by profile.lint_blocking. Pre-2.2.24 the script existed
464
- // and was documented as "called by the orchestrator after GREEN
465
- // verify" but nothing actually invoked it.
461
+ // when profile.lint_enabled === true. Blocking vs non-blocking is
462
+ // governed by profile.lint_blocking.
466
463
  const lintResult = runPostGreenGates(ctx);
467
464
  if (lintResult) {
468
465
  if (lintResult.failed && (ctx.profile && ctx.profile.lint_blocking)) {
@@ -476,17 +473,13 @@ function verifyDevGreen(state, out, ctx) {
476
473
 
477
474
  function verifyCodeReview(state, out, ctx) {
478
475
  const issues = [];
479
- // bmad-code-review (.claude/skills/bmad-code-review/steps/step-04-present.md)
480
- // writes findings as a "### Review Findings" subsection INSIDE the story
481
- // file's Tasks/Subtasks block — NOT a separate _bmad-output/reviews/<key>.md.
482
- // The pre-2.2.17 check for that file rejected every real run because the
483
- // skill never creates one (recurring user pain: "review artifact missing:
484
- // <path>" halts).
485
- //
486
- // Accept any of:
476
+ // bmad-code-review writes findings as a "### Review Findings"
477
+ // subsection inside the story file's Tasks/Subtasks block (see
478
+ // .claude/skills/bmad-code-review/steps/step-04-present.md). Older
479
+ // repo layouts also use a separate review file. Accept any of:
487
480
  // - story file contains a `### Review Findings` section
488
- // - legacy `_bmad-output/reviews/<key>.md` exists (older repos)
489
- // - legacy `_bmad-output/implementation-artifacts/code-review-<key>.md` exists
481
+ // - `_bmad-output/reviews/<key>.md` exists
482
+ // - `_bmad-output/implementation-artifacts/code-review-<key>.md` exists
490
483
  // Reject only when NONE of the above exist AND the LLM didn't supply
491
484
  // findings[] inline.
492
485
  const storyKey = state.story_key || 'unknown';
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 2.2.30
3
+ version: 2.3.0
4
4
  description: Sprintpilot — autopilot and multi-agent addon for BMad Method (git workflow, parallel agents, autonomous story execution)
5
5
  bmad_compatibility: ">=6.2.0"
6
6
  modules:
@@ -24,3 +24,6 @@ addon:
24
24
  - sprintpilot-migrate
25
25
  - sprintpilot-research
26
26
  - sprintpilot-party-mode
27
+ - sprintpilot-plan-sprint
28
+ - sprintpilot-sprint-progress
29
+ - sprintpilot-dependency-graph
@@ -18,10 +18,24 @@ autopilot:
18
18
  coalesce_state_writes: true # M3, PR 6 — batch non-critical writes, flush at story boundary
19
19
  conditional_boot_work: true # M4, PR 7 — skip health-check + branch reconciliation on a clean repo
20
20
  cache_shared_reads: true # M5, PR 8 — memoize sprint-status / git-status / decision-log reads per loop iteration
21
- # 2.0.2 — autopilot session infers inter-story DAG once after bmad-sprint-planning
22
- # and writes _Sprintpilot/sprints/dependencies.yaml. Hand-authored sidecars
23
- # (no AUTO-INFERRED marker) are detected and respected.
24
- auto_infer_dependencies: true
21
+ # 2.0.2 — original dependency-inference knob. Pre-v2.3.0 this triggered
22
+ # an LLM-driven inference pass once after bmad-sprint-planning and wrote
23
+ # `_Sprintpilot/sprints/dependencies.yaml`. v2.3.0 replaces that flow:
24
+ # - The plan now lives at `_bmad-output/implementation-artifacts/sprint-plan.yaml`.
25
+ # - Inference is opt-in via /sprintpilot-plan-sprint, NOT automatic.
26
+ # - Greenfield runs (no plan, no --stories/--epic) execute stories in
27
+ # sprint-status order — same as today's resolveNextStoryKey path.
28
+ # Default flipped to false; users who relied on the old behavior should
29
+ # set `autopilot.auto_plan_on_start: true` (see below).
30
+ auto_infer_dependencies: false
31
+
32
+ # v2.3.0 — when true, cmdStart emits an `invoke_skill` action for
33
+ # /sprintpilot-plan-sprint on first run if no sprint-plan.yaml exists.
34
+ # Default `false`: missing plan → fall back to sprint-status order
35
+ # (existing behavior). Once a plan exists, staleness is detected and
36
+ # re-derive runs automatically regardless of this knob — flipping back
37
+ # to false is a one-shot opt-out for net-new projects only.
38
+ auto_plan_on_start: false
25
39
 
26
40
  git:
27
41
  granularity: story # story | epic
@@ -65,13 +65,18 @@ git:
65
65
  story-title: "from story file title, fallback to story-key"
66
66
  patch-title: "from review finding title, fallback to 'code review fix'"
67
67
 
68
- # Linting
68
+ # Linting — runs after dev_green verify passes via
69
+ # scripts/post-green-gates.js (lint-changed + lint-test-pitfalls +
70
+ # ci-parity scan).
69
71
  lint:
70
- enabled: true
71
- blocking: false # true = lint errors halt the autopilot; false = warn only
72
- output_limit: 100 # max lines of lint output injected into context
73
- # Linter preference per language — first found wins.
74
- # Override to force a specific tool (e.g., set typescript: [biome] to skip eslint).
72
+ enabled: true # gate runs only when true
73
+ blocking: false # true = lint errors reject verify (LLM fix-loops); false = recorded but non-gating
74
+ output_limit: 100 # max lines of lint output injected back into context
75
+ # Per-language linter preference, first-installed wins. Empty list
76
+ # disables linting for that language. `javascript` and `typescript`
77
+ # merge into a single js-ts bucket (both share eslint/biome tooling).
78
+ # To add a new language, add an entry here AND a matching resolver
79
+ # block in scripts/lint-changed.js.
75
80
  linters:
76
81
  python: [ruff, flake8, pylint]
77
82
  javascript: [eslint, biome]
@@ -87,7 +92,6 @@ git:
87
92
  plsql: [sqlfluff]
88
93
  kotlin: [ktlint, detekt]
89
94
  php: [phpstan, phpcs]
90
- # To add a new language: add an entry here and a matching block in scripts/lint-changed.js
91
95
 
92
96
  # Push & PR
93
97
  push:
@@ -103,9 +107,11 @@ git:
103
107
  cleanup_on_merge: true # false = keep worktrees after epic completion for inspection
104
108
  health_check_on_boot: true # check for orphaned worktrees from crashed sessions
105
109
 
106
- # Lock file (.autopilot.lock — prevents concurrent autopilot sessions)
110
+ # Lock file (.autopilot.lock — prevents concurrent autopilot sessions
111
+ # on the same project). Acquired by `autopilot start`; released by
112
+ # `/sprint-autopilot-off`.
107
113
  lock:
108
- stale_timeout_minutes: 30 # auto-remove locks older than this
114
+ stale_timeout_minutes: 30 # auto-take-over locks older than this (0 disables)
109
115
 
110
116
  # Platform detection
111
117
  platform:
@@ -1,30 +1,32 @@
1
1
  # Multi-Agent Configuration
2
2
  #
3
- # Top-level key MUST be `ma:` resolve-profile.js merges this under the
3
+ # Top-level key MUST be `ma:`. resolve-profile.js merges this under the
4
4
  # `ma` namespace, and profile-rules.js reads `ma.parallel_stories` /
5
- # `ma.max_parallel_stories`. The legacy `multi_agent:` wrapper used in
6
- # pre-2.2.16 versions was silently ignored (deep-merge produced
7
- # `resolved.ma.multi_agent.*` instead of `resolved.ma.*`).
5
+ # `ma.max_parallel_stories`.
8
6
 
9
7
  ma:
10
8
  enabled: true
11
9
  max_parallel_review_layers: 3 # Always 3: blind, edge-case, acceptance
12
10
  max_parallel_research: 3 # Max concurrent research agents per batch
13
11
  max_parallel_analysis: 5 # Max concurrent codebase analysis agents
14
- # session_story_limit NOT duplicated hereuses autopilot's single authoritative limit
12
+ # session_story_limit lives under autopilot.*single source of truth.
15
13
 
16
- # PR 11: intra-epic parallel story execution.
17
- # parallel_stories: true enables dispatch-layer.js on Claude Code;
18
- # other hosts silently fall back to sequential (per agent-adapter.js
19
- # detection confidence high + supports_parallel true).
20
- # max_parallel_stories: cap on concurrent sub-agents per layer.
21
- # min_epic_duration_for_parallel_sec: skip parallelism for epics
22
- # whose estimated wall-clock is below this threshold (saves
23
- # dispatch overhead).
24
- # max_consecutive_conflicts: disable parallelism for the rest of the
25
- # session once N consecutive merge conflicts occur.
26
- # effective_parallel_floor: never drop below this mid-session even
27
- # after failure-driven concurrency reduction.
14
+ # Intra-epic parallel story execution.
15
+ #
16
+ # parallel_stories: true enables the dispatch-layer.js building blocks
17
+ # (planBatch, resolve-dag.js, merge-shards.js, agent-adapter.js). The
18
+ # state machine emits stories one at a time; the autopilot logs a
19
+ # clear notice at session start when this flag is set so behavior is
20
+ # unambiguous. Intra-epic parallel emission is planned for a future
21
+ # minor release.
22
+ #
23
+ # max_parallel_stories: cap on concurrent sub-agents per layer.
24
+ # min_epic_duration_for_parallel_sec: skip parallelism for epics whose
25
+ # estimated wall-clock is below this threshold (saves dispatch overhead).
26
+ # max_consecutive_conflicts: disable parallelism for the rest of the
27
+ # session once N consecutive merge conflicts occur.
28
+ # effective_parallel_floor: never drop below this mid-session even
29
+ # after failure-driven concurrency reduction.
28
30
  parallel_stories: false
29
31
  max_parallel_stories: 2
30
32
  min_epic_duration_for_parallel_sec: 300
@@ -32,25 +34,25 @@ ma:
32
34
  max_consecutive_conflicts: 2
33
35
  effective_parallel_floor: 1
34
36
 
35
- # EXPERIMENTAL: enable parallel_stories dispatch on Gemini CLI.
37
+ # Experimental: parallel_stories dispatch on Gemini CLI.
36
38
  #
37
39
  # Gemini CLI has a subagent primitive (invoke_subagent) but its
38
- # worktree-scoped variant is not yet shipped upstream (tracker:
39
- # github.com/google-gemini/gemini-cli#22967) and real-world parallelism
40
+ # worktree-scoped variant is tracked upstream
41
+ # (github.com/google-gemini/gemini-cli#22967). Real-world parallelism
40
42
  # reports serialization + quota throttling. Sprintpilot detects Gemini
41
43
  # CLI via GEMINI_CLI=1 (env, HIGH confidence) or parent process `gemini`
42
- # (MEDIUM), but supports_parallel stays false by default.
44
+ # (MEDIUM); supports_parallel stays false by default.
43
45
  #
44
- # Flip to true PER PROJECT once upstream worktree support lands (or to
45
- # experiment at your own risk). Workflow logs an "experimental parallel"
46
- # warning once per session when this is true AND host=gemini-cli.
46
+ # Flip to true PER PROJECT to opt into the experimental path. The
47
+ # workflow logs an "experimental parallel" warning once per session
48
+ # when this is true AND host=gemini-cli.
47
49
  experimental_parallel_on_gemini: false
48
50
 
49
- # PR 12 — cross-epic parallelism (EXPERIMENTAL).
50
- # Off by default on ALL profiles including large. Enabling requires:
51
+ # Cross-epic parallelism (experimental). Off by default on every
52
+ # profile, including large. Enabling requires:
51
53
  # 1. Both epics carry `independent: true` in dependencies.yaml.
52
54
  # 2. preflight-merge.js reports no conflicts between them.
53
- # 3. max_parallel_epics is hardcoded at 2 — no tuning knob.
55
+ # 3. max_parallel_epics is fixed at 2 — no tuning knob.
54
56
  # A single cross-epic merge conflict in a session disables parallel_epics
55
57
  # for the rest of the session.
56
58
  parallel_epics: false
@@ -79,10 +79,9 @@ function planLayer({ keys, maxParallel, projectRoot, branchPrefix, baseBranch })
79
79
  }
80
80
  const effectiveParallel = Math.max(1, Math.min(maxParallel | 0, dedupedKeys.length));
81
81
  // CAP: only dispatch the first `effectiveParallel` stories. The
82
- // remaining keys are deferred — the autopilot loop will pick them up
83
- // in the next iteration after this batch completes. Pre-2.0.8 the
84
- // script created worktrees for ALL keys regardless of the cap, then
85
- // the workflow spawned N agents anyway, fully ignoring --max-parallel.
82
+ // remaining keys are deferred — the autopilot loop picks them up
83
+ // in the next iteration after this batch completes. Honors
84
+ // --max-parallel as the upper bound on concurrent worktree creation.
86
85
  const dispatchedKeys = dedupedKeys.slice(0, effectiveParallel);
87
86
  const deferredKeys = dedupedKeys.slice(effectiveParallel);
88
87
  const worktrees = dispatchedKeys.map((key) => ({
@@ -112,10 +111,10 @@ function writePlan(projectRoot, plan) {
112
111
  }
113
112
 
114
113
  // Match git's "branch already exists" diagnostic. We retry without -b
115
- // only when the FIRST attempt failed for this specific reason
116
- // pre-2.0.8 the bare retry fired on ANY first-attempt failure and
117
- // silently checked out whatever stale branch happened to exist at the
118
- // requested name (e.g. last week's commits from an abandoned story).
114
+ // ONLY when the first attempt failed for this specific reason. A bare
115
+ // retry on any other failure would silently check out whatever stale
116
+ // branch happened to exist at the requested name (e.g. last week's
117
+ // commits from an abandoned story).
119
118
  const BRANCH_EXISTS_RE = /a branch named .* already exists/i;
120
119
 
121
120
  function createWorktree({ projectRoot, worktree, branch, baseBranch }) {
@@ -147,11 +146,9 @@ function createWorktree({ projectRoot, worktree, branch, baseBranch }) {
147
146
  };
148
147
  }
149
148
 
150
- // After a worktree is created, disable gc.auto on it. The sequential
151
- // path in workflow.md does this at line 738; pre-2.0.8 the parallel
152
- // path skipped it, so concurrent sub-agents in heavy repos could
153
- // trigger gc on each worktree mid-dispatch. Best-effort — never block
154
- // dispatch on a config write.
149
+ // After a worktree is created, disable gc.auto on it so concurrent
150
+ // sub-agents in heavy repos don't trigger gc on each worktree mid-
151
+ // dispatch. Best-effort never block dispatch on a config write.
155
152
  function disableGcAutoOnWorktree(worktree) {
156
153
  spawnSync('git', ['-C', worktree, 'config', '--local', 'gc.auto', '0'], {
157
154
  encoding: 'utf8',
@@ -197,8 +194,8 @@ function dispatch({ keys, maxParallel, projectRoot, branchPrefix, baseBranch, dr
197
194
  return results;
198
195
  }
199
196
  // Real dispatch. Track successful creates so we can roll them back if
200
- // a later create fails — leaving an orphan worktree + a plan file
201
- // claiming it succeeded was the v2.0.7 partial-failure bug.
197
+ // a later create fails — partial success would leave orphan worktrees
198
+ // alongside a plan file that claims everything succeeded.
202
199
  const succeeded = [];
203
200
  let failureIndex = -1;
204
201
  for (let i = 0; i < plan.stories.length; i++) {