@ikunin/sprintpilot 2.2.1 → 2.2.3

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.
@@ -426,21 +426,40 @@ function composeRuntimeState(persisted, profile, projectRoot) {
426
426
  // takes priority over the linear resolveNextStoryKey scan: when a
427
427
  // user specifies "start with stories 4-1, 4-2, 4-5" we honor that
428
428
  // order regardless of what comes first in sprint-status.yaml.
429
+ //
430
+ // Queue consumption is GATED to story-start phases. Without this gate,
431
+ // composeRuntimeState would pull the queue head as runtime.story_key
432
+ // during EPIC_BOUNDARY_CHECK / RETROSPECTIVE / STORY_LAND — phases
433
+ // where the orchestrator isn't starting a new story yet. That would
434
+ // pollute state and (via adapt.advanceState's signal-output
435
+ // propagation) overwrite current_epic with the next story's epic
436
+ // BEFORE retrospective runs.
437
+ //
429
438
  // Forward-compat for ma.parallel_stories: the queue is the source
430
439
  // multiple workers will pull from when the parallel-batch path is
431
440
  // wired into the state machine.
432
441
  const persistedQueue = Array.isArray(persisted.story_queue)
433
442
  ? persisted.story_queue.filter((k) => typeof k === 'string' && k.length > 0)
434
443
  : [];
435
- if (!resolvedStoryKey && persistedQueue.length > 0) {
444
+ const isNewStoryStartPhase =
445
+ phase === STATES.CREATE_STORY ||
446
+ phase === STATES.NANO_QUICK_DEV ||
447
+ phase === STATES.PREPARE_STORY_BRANCH;
448
+ if (isNewStoryStartPhase && !resolvedStoryKey && persistedQueue.length > 0) {
436
449
  resolvedStoryKey = persistedQueue[0];
437
- if (!resolvedEpic) resolvedEpic = deriveEpicFromStoryKey(resolvedStoryKey);
450
+ // Unconditional re-derive: when picking a new story_key, current_epic
451
+ // MUST match. A queue spanning multiple epics (e.g. [4-1, 5-1]) needs
452
+ // to update current_epic when crossing the boundary. The previous
453
+ // story's epic — preserved through EPIC_BOUNDARY_CHECK + RETROSPECTIVE
454
+ // by adapt.advanceState — would otherwise carry over and mislabel
455
+ // commits/branches.
456
+ resolvedEpic = deriveEpicFromStoryKey(resolvedStoryKey) || resolvedEpic;
438
457
  }
439
458
  if (phase === STATES.PREPARE_STORY_BRANCH && !resolvedStoryKey) {
440
459
  const next = resolveNextStoryKey(projectRoot);
441
460
  if (next) {
442
461
  resolvedStoryKey = next;
443
- if (!resolvedEpic) resolvedEpic = deriveEpicFromStoryKey(next);
462
+ resolvedEpic = deriveEpicFromStoryKey(next) || resolvedEpic;
444
463
  } else {
445
464
  process.stderr.write(
446
465
  `[autopilot] WARN PREPARE_STORY_BRANCH needs a next-story key but sprint-status.yaml has none pending — falling back to ${flowStart}. ` +
@@ -450,6 +469,23 @@ function composeRuntimeState(persisted, profile, projectRoot) {
450
469
  }
451
470
  }
452
471
 
472
+ // Count non-done stories in the current epic. state-machine.js's
473
+ // EPIC_BOUNDARY_CHECK reads this to decide between RETROSPECTIVE (end
474
+ // of epic, count === 0) and next-story-start (count > 0). Pre-2.2.2
475
+ // this field was passthrough-only — never written by the orchestrator
476
+ // — so the count stayed at 0 and EVERY story triggered a
477
+ // retrospective. Now: recompute from sprint-status.yaml each emission
478
+ // when current_epic is known.
479
+ //
480
+ // Count semantics: excludes done stories AND non-story entries (epic
481
+ // rollup headers, -retrospective entries) via the same looksLikeStoryKey
482
+ // filter resolveNextStoryKey uses.
483
+ let remainingStoriesInEpic = persisted.remaining_stories_in_epic || 0;
484
+ if (resolvedEpic && projectRoot) {
485
+ const epicStories = resolveStoriesForEpic(projectRoot, resolvedEpic);
486
+ remainingStoriesInEpic = epicStories.length;
487
+ }
488
+
453
489
  return {
454
490
  phase,
455
491
  story_key: resolvedStoryKey,
@@ -461,7 +497,7 @@ function composeRuntimeState(persisted, profile, projectRoot) {
461
497
  prior_signals_summary: persisted.prior_signals_summary || null,
462
498
  patch_findings: persisted.patch_findings || null,
463
499
  tests_to_rerun: persisted.tests_to_rerun || null,
464
- remaining_stories_in_epic: persisted.remaining_stories_in_epic || 0,
500
+ remaining_stories_in_epic: remainingStoriesInEpic,
465
501
  sprint_is_complete: !!persisted.sprint_is_complete,
466
502
  retry_count_this_phase: persisted.retry_count_this_phase || 0,
467
503
  verify_reject_count: persisted.verify_reject_count || 0,
@@ -550,18 +550,26 @@ function advanceState(state, profile, newPhase, signal) {
550
550
 
551
551
  // Story-completion boundary: STORY_DONE → EPIC_BOUNDARY_CHECK means
552
552
  // the current story is committed and pushed. Pop the explicit story
553
- // queue (if any) — its head was THIS story — and clear story_key so
554
- // composeRuntimeState picks queue[1] (now queue[0]) on the next
555
- // emission. Without this pop, composeRuntimeState would re-pick the
556
- // just-completed story (via the signal-output propagation above) and
557
- // loop. This block runs AFTER propagation so the clearing wins.
553
+ // queue (if any) — its head was THIS story — and clear story_key /
554
+ // story_file_path / ac_summary so composeRuntimeState picks
555
+ // queue[1] (now queue[0]) on the next emission. Without this pop,
556
+ // composeRuntimeState would re-pick the just-completed story (via the
557
+ // signal-output propagation above) and loop. This block runs AFTER
558
+ // propagation so the clearing wins.
559
+ //
560
+ // current_epic is intentionally NOT cleared here. EPIC_BOUNDARY_CHECK
561
+ // and (downstream) RETROSPECTIVE both need it: state-machine reads
562
+ // current_epic to compute remaining_stories_in_epic, and
563
+ // verifyRetrospective uses it to locate `_bmad-output/retrospectives/
564
+ // <epic>.md`. composeRuntimeState re-derives current_epic from the
565
+ // new story_key when the queue head changes epics on next-story-start
566
+ // (CREATE_STORY / PREPARE_STORY_BRANCH / NANO_QUICK_DEV).
558
567
  if (state.phase === STATES.STORY_DONE && newPhase === STATES.EPIC_BOUNDARY_CHECK) {
559
568
  if (Array.isArray(state.story_queue) && state.story_queue.length > 0) {
560
569
  next.story_queue = state.story_queue.slice(1);
561
570
  }
562
571
  next.story_key = null;
563
572
  next.story_file_path = null;
564
- next.current_epic = null;
565
573
  next.ac_summary = null;
566
574
  }
567
575
 
@@ -57,6 +57,12 @@ function escapeRe(s) {
57
57
  // Extract a story's status from sprint-status.yaml without pulling in a
58
58
  // full YAML parser. Supports both inline form (`<key>: <status>`) and
59
59
  // block form (`<key>:\n status: <status>\n title: ...`).
60
+ //
61
+ // Tolerates trailing `# comment` on inline status lines — the BMad
62
+ // convention is `<key>: done # PR #N merged ...` and the previous
63
+ // regex required `\s*$` immediately after the status token, rejecting
64
+ // every commented entry. The block-form inner status regex never
65
+ // anchored to end-of-line, so it always tolerated comments.
60
66
  function storyStatusFromSprintStatus(text, storyKey) {
61
67
  if (!text || !storyKey) return null;
62
68
  const k = escapeRe(storyKey);
@@ -69,7 +75,12 @@ function storyStatusFromSprintStatus(text, storyKey) {
69
75
  if (sm) return sm[1];
70
76
  }
71
77
  // Inline form: ` story-key: done` (status as scalar value).
72
- const inlineRe = new RegExp(`^\\s+${k}:\\s*["']?([\\w-]+)["']?\\s*$`, 'm');
78
+ // Optional trailing `# comment` is allowed so `done # PR #N merged`
79
+ // matches `done` instead of failing the whole line.
80
+ const inlineRe = new RegExp(
81
+ `^\\s+${k}:\\s*["']?([\\w-]+)["']?\\s*(?:#.*)?$`,
82
+ 'm',
83
+ );
73
84
  const im = text.match(inlineRe);
74
85
  return im ? im[1] : null;
75
86
  }
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 2.2.1
3
+ version: 2.2.3
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikunin/sprintpilot",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Sprintpilot — autopilot and multi-agent addon for BMad Method v6: git workflow, parallel agents, autonomous story execution",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {