@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
|
-
|
|
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
|
-
|
|
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 ||
|
package/package.json
CHANGED