@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
554
|
-
//
|
|
555
|
-
// emission. Without this pop,
|
|
556
|
-
// just-completed story (via the
|
|
557
|
-
// loop. This block runs AFTER
|
|
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
|
-
|
|
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
|
}
|
package/package.json
CHANGED