@ikunin/sprintpilot 2.2.3 → 2.2.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.
@@ -158,6 +158,58 @@ function resolveNextStoryKey(projectRoot) {
158
158
  }
159
159
  }
160
160
 
161
+ // Validate a persisted current_story key against sprint-status.yaml.
162
+ // Returns null when the key is valid (the orchestrator should keep it).
163
+ // Returns a short reason string when the key is poisoned or stale (the
164
+ // orchestrator should drop it and re-resolve).
165
+ //
166
+ // NARROW filter (vs looksLikeStoryKey which is strict). Only rejects:
167
+ // - `epic-N` shape (epic-rollup header) — the v2.1.3/v2.1.4 poison
168
+ // - bare numeric `N` (legacy bare-id epic form)
169
+ // - `*-retrospective` shape
170
+ // Accepts everything else as a plausible story key (including short test
171
+ // keys like `S1`, `S1.2`, and non-BMad-canonical naming conventions).
172
+ // The orchestrator should not nuke valid state just because the key
173
+ // doesn't match the strict BMad `<epic>-<story>-<slug>` shape.
174
+ //
175
+ // Defensive: when sprint-status can't be read, returns null so the
176
+ // orchestrator preserves persisted value. The user shouldn't have their
177
+ // session reset just because the artifact is missing.
178
+ function persistedStoryRejectionReason(key, projectRoot) {
179
+ if (typeof key !== 'string' || !key) return 'not a string';
180
+ if (isObviouslyEpicHeader(key)) {
181
+ return 'matches epic-rollup header shape (epic-N or bare N) — v2.1.3/v2.1.4 poisoned state';
182
+ }
183
+ if (/-retrospective$/i.test(key)) {
184
+ return 'matches retrospective entry shape — not a story';
185
+ }
186
+ const stories = readSprintStatuses(projectRoot);
187
+ if (!stories) return null; // sprint-status absent → defer to caller; don't reject.
188
+ if (!Object.prototype.hasOwnProperty.call(stories, key)) {
189
+ return 'not present in sprint-status.yaml';
190
+ }
191
+ const status = String(stories[key].status || '').trim().toLowerCase();
192
+ if (status === 'done') {
193
+ return `sprint-status shows status='done'; story already complete`;
194
+ }
195
+ return null;
196
+ }
197
+
198
+ // Catch only the documented poisoned shapes that pre-v2.1.5 orchestrator
199
+ // versions could write to persisted.current_story:
200
+ // - `epic-N` with no further hyphen-separated segments (epic rollup
201
+ // header — composeRuntimeState in v2.1.3/v2.1.4 picked this as the
202
+ // first non-done entry before the looksLikeStoryKey filter shipped).
203
+ // - bare numeric `N` (legacy BMad bare-id epic form).
204
+ // Does NOT reject short test keys like `S1` / `S1.2` or other non-BMad
205
+ // naming conventions — those are valid persisted state.
206
+ function isObviouslyEpicHeader(key) {
207
+ if (typeof key !== 'string' || !key) return false;
208
+ if (/^epic-[A-Za-z0-9_]+$/i.test(key)) return true;
209
+ if (/^\d+$/.test(key)) return true;
210
+ return false;
211
+ }
212
+
161
213
  // Tell story keys apart from non-story bookkeeping entries in
162
214
  // sprint-status.yaml. BMad development_status: holds three kinds of
163
215
  // entries that parseStatuses returns side-by-side:
@@ -419,9 +471,45 @@ function composeRuntimeState(persisted, profile, projectRoot) {
419
471
  // failure), fall back to flowStart so the LLM gets a meaningful
420
472
  // skill invocation. PREPARE_STORY_BRANCH with no story_key would be
421
473
  // a confusing emission to act on.
422
- let resolvedStoryKey = persisted.current_story || null;
474
+ // Validate persisted.current_story against sprint-status before
475
+ // trusting it. Older orchestrator versions (v2.1.3 / v2.1.4) could
476
+ // poison this field with an epic-rollup header (e.g. `epic-4`) when
477
+ // resolveNextStoryKey scanned sprint-status without filtering. The
478
+ // v2.1.5 hotfix added looksLikeStoryKey but only at resolution time —
479
+ // already-persisted poisoned values ride forward through every
480
+ // upgrade, producing emissions like `branch: story/epic-4` on every
481
+ // session boot.
482
+ //
483
+ // Treat persisted.current_story as null when:
484
+ // - it doesn't look like a real story key (epic header, retro, garbage)
485
+ // - sprint-status.yaml exists but the key isn't in it (deleted/renamed)
486
+ // - sprint-status shows the key as 'done' (already complete; advancing
487
+ // past STORY_DONE should have cleared it, so something is stale)
488
+ //
489
+ // Defensive: if sprint-status can't be read, preserve persisted value
490
+ // (don't punish the user for a missing artifact). The warning is on
491
+ // stderr so the user sees what was rejected and why.
492
+ const persistedCurrentStory = persisted.current_story || null;
493
+ let resolvedStoryKey = persistedCurrentStory;
423
494
  let resolvedEpic = persisted.current_epic || null;
424
495
  let resolvedStoryFilePath = persisted.story_file_path || null;
496
+ if (persistedCurrentStory) {
497
+ const rejection = persistedStoryRejectionReason(
498
+ persistedCurrentStory,
499
+ projectRoot,
500
+ );
501
+ if (rejection) {
502
+ process.stderr.write(
503
+ `[autopilot] WARN persisted current_story "${persistedCurrentStory}" rejected: ${rejection}. ` +
504
+ 'Treating as null and falling through to queue / sprint-status resolution. ' +
505
+ 'This typically means state was poisoned by an older orchestrator version (v2.1.3 / v2.1.4 pre-filter); ' +
506
+ 'next emission will clean it up.\n',
507
+ );
508
+ resolvedStoryKey = null;
509
+ resolvedEpic = null;
510
+ resolvedStoryFilePath = null;
511
+ }
512
+ }
425
513
  // Explicit queue (populated by `autopilot start --stories` / `--epic`)
426
514
  // takes priority over the linear resolveNextStoryKey scan: when a
427
515
  // user specifies "start with stories 4-1, 4-2, 4-5" we honor that
@@ -438,9 +526,32 @@ function composeRuntimeState(persisted, profile, projectRoot) {
438
526
  // Forward-compat for ma.parallel_stories: the queue is the source
439
527
  // multiple workers will pull from when the parallel-batch path is
440
528
  // wired into the state machine.
441
- const persistedQueue = Array.isArray(persisted.story_queue)
529
+ // Validate persisted.story_queue entries against sprint-status. Same
530
+ // rejection rules as current_story (epic-rollup shape / retrospective
531
+ // / missing from sprint-status / marked done) — applies to every queue
532
+ // member. Without this, a legacy queue persisted by an older
533
+ // orchestrator (or after a sprint-status edit that removed entries)
534
+ // would feed garbage keys to subsequent emissions.
535
+ //
536
+ // Defensive: if sprint-status can't be read, only the shape-based
537
+ // rejections (epic-N, retrospective) apply; presence/status checks
538
+ // are skipped. Same don't-punish-missing-artifact policy as
539
+ // current_story validation.
540
+ const rawPersistedQueue = Array.isArray(persisted.story_queue)
442
541
  ? persisted.story_queue.filter((k) => typeof k === 'string' && k.length > 0)
443
542
  : [];
543
+ const persistedQueue = [];
544
+ for (const k of rawPersistedQueue) {
545
+ const reason = persistedStoryRejectionReason(k, projectRoot);
546
+ if (reason) {
547
+ process.stderr.write(
548
+ `[autopilot] WARN story_queue entry "${k}" rejected: ${reason}. ` +
549
+ 'Dropping from queue.\n',
550
+ );
551
+ continue;
552
+ }
553
+ persistedQueue.push(k);
554
+ }
444
555
  const isNewStoryStartPhase =
445
556
  phase === STATES.CREATE_STORY ||
446
557
  phase === STATES.NANO_QUICK_DEV ||
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 2.2.3
3
+ version: 2.2.5
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.3",
3
+ "version": "2.2.5",
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": {