@ikunin/sprintpilot 2.1.5 → 2.2.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.
- package/README.md +11 -0
- package/_Sprintpilot/bin/autopilot.js +208 -1
- package/_Sprintpilot/lib/orchestrator/action-ledger.js +4 -0
- package/_Sprintpilot/lib/orchestrator/adapt.js +17 -0
- package/_Sprintpilot/lib/orchestrator/state-store.js +5 -0
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +84 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,17 @@ npx @ikunin/sprintpilot@latest
|
|
|
31
31
|
/sprint-autopilot-on
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
**Start at a specific story or epic** (v2.2.0+):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
/sprint-autopilot-on epic 4
|
|
38
|
+
/sprint-autopilot-on stories 3.1, 3.2, 4.5
|
|
39
|
+
/sprint-autopilot-on 4-8-realm-wide-matcher-and-session-lock
|
|
40
|
+
/sprint-autopilot-on voice identity matcher
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The skill resolves the natural-language directive against your `sprint-status.yaml` and queues the matching stories. The autopilot runs them in order, then falls back to the normal next-pending-story flow. Ambiguous matches surface a candidate list — never picks arbitrarily.
|
|
44
|
+
|
|
34
45
|
Non-interactive install:
|
|
35
46
|
|
|
36
47
|
```bash
|
|
@@ -65,6 +65,25 @@ function help() {
|
|
|
65
65
|
' --profile <nano|small|medium|large|legacy>',
|
|
66
66
|
' Override resolved profile',
|
|
67
67
|
' --help Show this help',
|
|
68
|
+
'',
|
|
69
|
+
'Story-selection flags (on `start` only):',
|
|
70
|
+
' --stories <k1,k2,...> Explicit queue of story keys to run, in',
|
|
71
|
+
' order. Keys must exist in sprint-status.yaml',
|
|
72
|
+
' and not be already done. Once the queue',
|
|
73
|
+
' exhausts, the orchestrator falls back to',
|
|
74
|
+
' its normal next-pending-story flow.',
|
|
75
|
+
' --epic <id> Queue all non-done stories of the given',
|
|
76
|
+
' epic (id matches `epic-N` or bare `N`),',
|
|
77
|
+
' in sprint-status.yaml order. --stories',
|
|
78
|
+
' takes priority when both are given.',
|
|
79
|
+
' --force Overwrite an in-flight queue. Without',
|
|
80
|
+
' this, --stories/--epic refuses to run',
|
|
81
|
+
' when current_story is set or a queue',
|
|
82
|
+
' already exists.',
|
|
83
|
+
'',
|
|
84
|
+
'Natural-language entry: `/sprint-autopilot-on epic 4` /',
|
|
85
|
+
'`/sprint-autopilot-on stories 3.1, 4.5` — the skill resolves the NL',
|
|
86
|
+
'directive to canonical keys and invokes `autopilot start --stories`.',
|
|
68
87
|
].join('\n'),
|
|
69
88
|
);
|
|
70
89
|
}
|
|
@@ -169,6 +188,125 @@ function looksLikeStoryKey(key) {
|
|
|
169
188
|
return withoutEpicPrefix.includes('-');
|
|
170
189
|
}
|
|
171
190
|
|
|
191
|
+
// Build an explicit story queue from CLI opts (--stories / --epic).
|
|
192
|
+
// Returns { queue: [], error?: string }. Either or both flags can be
|
|
193
|
+
// provided; --stories is the canonical list and --epic expands to all
|
|
194
|
+
// non-done stories under that epic. When both are given, --stories
|
|
195
|
+
// takes priority. When neither is given, returns an empty queue
|
|
196
|
+
// (orchestrator falls back to resolveNextStoryKey).
|
|
197
|
+
//
|
|
198
|
+
// Validation:
|
|
199
|
+
// - Every key listed in --stories must exist in sprint-status.yaml.
|
|
200
|
+
// - Every key must NOT have status 'done'.
|
|
201
|
+
// - For --epic, the epic must have at least one non-done story.
|
|
202
|
+
function buildExplicitQueueFromOpts(opts, projectRoot) {
|
|
203
|
+
const rawStories = typeof opts.stories === 'string' ? opts.stories : null;
|
|
204
|
+
const rawEpic = opts.epic !== undefined && opts.epic !== null ? String(opts.epic) : null;
|
|
205
|
+
if (!rawStories && !rawEpic) return { queue: [] };
|
|
206
|
+
|
|
207
|
+
const sprintStories = readSprintStatuses(projectRoot);
|
|
208
|
+
if (!sprintStories || Object.keys(sprintStories).length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
queue: [],
|
|
211
|
+
error:
|
|
212
|
+
'--stories / --epic given but sprint-status.yaml is missing or empty. ' +
|
|
213
|
+
'Run BMad sprint-planning to populate it before queuing stories.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (rawStories) {
|
|
218
|
+
const requested = rawStories
|
|
219
|
+
.split(',')
|
|
220
|
+
.map((s) => s.trim())
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
if (requested.length === 0) {
|
|
223
|
+
return { queue: [], error: '--stories was empty after parsing the comma-separated list.' };
|
|
224
|
+
}
|
|
225
|
+
const missing = [];
|
|
226
|
+
const alreadyDone = [];
|
|
227
|
+
const queue = [];
|
|
228
|
+
for (const key of requested) {
|
|
229
|
+
if (!Object.prototype.hasOwnProperty.call(sprintStories, key)) {
|
|
230
|
+
missing.push(key);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const status = String(sprintStories[key].status || '').trim().toLowerCase();
|
|
234
|
+
if (status === 'done') {
|
|
235
|
+
alreadyDone.push(key);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
queue.push(key);
|
|
239
|
+
}
|
|
240
|
+
if (missing.length > 0 || alreadyDone.length > 0) {
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (missing.length > 0) {
|
|
243
|
+
parts.push(
|
|
244
|
+
`not in sprint-status.yaml: ${missing.join(', ')}. ` +
|
|
245
|
+
'Use canonical keys (e.g. 4-8-realm-wide-matcher), not story numbers (e.g. 4.8).',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (alreadyDone.length > 0) {
|
|
249
|
+
parts.push(`already done: ${alreadyDone.join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
return { queue: [], error: `--stories rejected: ${parts.join(' | ')}` };
|
|
252
|
+
}
|
|
253
|
+
return { queue };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --epic only
|
|
257
|
+
const expanded = resolveStoriesForEpic(projectRoot, rawEpic);
|
|
258
|
+
if (expanded.length === 0) {
|
|
259
|
+
return {
|
|
260
|
+
queue: [],
|
|
261
|
+
error: `--epic ${rawEpic}: no non-done stories found in sprint-status.yaml`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { queue: expanded };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Read and parse sprint-status.yaml. Returns { stories } where stories
|
|
268
|
+
// is a map of {key: {status: string|null}}. Returns null on any failure
|
|
269
|
+
// (missing file, parse error). Callers handle null gracefully.
|
|
270
|
+
function readSprintStatuses(projectRoot) {
|
|
271
|
+
if (!projectRoot) return null;
|
|
272
|
+
const p = path.join(
|
|
273
|
+
projectRoot,
|
|
274
|
+
'_bmad-output',
|
|
275
|
+
'implementation-artifacts',
|
|
276
|
+
'sprint-status.yaml',
|
|
277
|
+
);
|
|
278
|
+
if (!safeExistsSync(p)) return null;
|
|
279
|
+
try {
|
|
280
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
281
|
+
return parseSprintStatuses(raw);
|
|
282
|
+
} catch (_e) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Resolve all non-done story keys for the given epic id, in
|
|
288
|
+
// sprint-status.yaml insertion order. Used by `autopilot start
|
|
289
|
+
// --epic <id>` to expand into an explicit queue. Returns [] when:
|
|
290
|
+
// - sprint-status doesn't exist or fails to parse
|
|
291
|
+
// - no stories match the epic
|
|
292
|
+
// - all matching stories are already done
|
|
293
|
+
function resolveStoriesForEpic(projectRoot, epicId) {
|
|
294
|
+
if (!epicId) return [];
|
|
295
|
+
const stories = readSprintStatuses(projectRoot);
|
|
296
|
+
if (!stories) return [];
|
|
297
|
+
const keys = Object.keys(stories);
|
|
298
|
+
const out = [];
|
|
299
|
+
for (const key of keys) {
|
|
300
|
+
if (!looksLikeStoryKey(key)) continue;
|
|
301
|
+
const derivedEpic = deriveEpicFromStoryKey(key);
|
|
302
|
+
if (derivedEpic !== epicId && derivedEpic !== `epic-${epicId}`) continue;
|
|
303
|
+
const status = String(stories[key].status || '').trim().toLowerCase();
|
|
304
|
+
if (status === 'done') continue;
|
|
305
|
+
out.push(key);
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
172
310
|
// Derive the epic identifier from a BMad story key. Convention:
|
|
173
311
|
// `epic-N-slug` → `epic-N`; `<epic>-<story>-<slug>` → `<epic>`.
|
|
174
312
|
// Returns null when the key doesn't parse cleanly. Kept in sync with
|
|
@@ -284,6 +422,20 @@ function composeRuntimeState(persisted, profile, projectRoot) {
|
|
|
284
422
|
let resolvedStoryKey = persisted.current_story || null;
|
|
285
423
|
let resolvedEpic = persisted.current_epic || null;
|
|
286
424
|
let resolvedStoryFilePath = persisted.story_file_path || null;
|
|
425
|
+
// Explicit queue (populated by `autopilot start --stories` / `--epic`)
|
|
426
|
+
// takes priority over the linear resolveNextStoryKey scan: when a
|
|
427
|
+
// user specifies "start with stories 4-1, 4-2, 4-5" we honor that
|
|
428
|
+
// order regardless of what comes first in sprint-status.yaml.
|
|
429
|
+
// Forward-compat for ma.parallel_stories: the queue is the source
|
|
430
|
+
// multiple workers will pull from when the parallel-batch path is
|
|
431
|
+
// wired into the state machine.
|
|
432
|
+
const persistedQueue = Array.isArray(persisted.story_queue)
|
|
433
|
+
? persisted.story_queue.filter((k) => typeof k === 'string' && k.length > 0)
|
|
434
|
+
: [];
|
|
435
|
+
if (!resolvedStoryKey && persistedQueue.length > 0) {
|
|
436
|
+
resolvedStoryKey = persistedQueue[0];
|
|
437
|
+
if (!resolvedEpic) resolvedEpic = deriveEpicFromStoryKey(resolvedStoryKey);
|
|
438
|
+
}
|
|
287
439
|
if (phase === STATES.PREPARE_STORY_BRANCH && !resolvedStoryKey) {
|
|
288
440
|
const next = resolveNextStoryKey(projectRoot);
|
|
289
441
|
if (next) {
|
|
@@ -317,6 +469,10 @@ function composeRuntimeState(persisted, profile, projectRoot) {
|
|
|
317
469
|
escalation_note: persisted.escalation_note || null,
|
|
318
470
|
// Branch reuse: persisted across resumes once detected on first boot.
|
|
319
471
|
user_branch: persisted.user_branch || null,
|
|
472
|
+
// Explicit story queue from `autopilot start --stories` / `--epic`.
|
|
473
|
+
// Head is the current pick; adapt.advanceState pops on story
|
|
474
|
+
// completion. Empty array means "no override; use resolveNextStoryKey."
|
|
475
|
+
story_queue: persistedQueue,
|
|
320
476
|
// Land-as-you-go: pending land state survives rebase-conflict halts.
|
|
321
477
|
land_pending: persisted.land_pending || null,
|
|
322
478
|
// Pending alternative (propose_alternative → user_prompt) survives
|
|
@@ -348,6 +504,7 @@ function persistRuntimeState(runtime, profile, projectRoot) {
|
|
|
348
504
|
verify_reject_count: runtime.verify_reject_count,
|
|
349
505
|
consecutive_test_failures: runtime.consecutive_test_failures,
|
|
350
506
|
user_branch: runtime.user_branch,
|
|
507
|
+
story_queue: Array.isArray(runtime.story_queue) ? runtime.story_queue : [],
|
|
351
508
|
land_pending: runtime.land_pending,
|
|
352
509
|
pending_alternative: runtime.pending_alternative || null,
|
|
353
510
|
};
|
|
@@ -654,6 +811,37 @@ function cmdStart(opts) {
|
|
|
654
811
|
const { typed: profile } = resolveProfile(projectRoot, opts.profile);
|
|
655
812
|
const persisted = loadState(projectRoot);
|
|
656
813
|
|
|
814
|
+
// Build an explicit story queue from --stories / --epic flags. The
|
|
815
|
+
// user (or the LLM via /sprint-autopilot-on natural-language args)
|
|
816
|
+
// tells the orchestrator EXACTLY which stories to run, and in what
|
|
817
|
+
// order. Queue head is consumed first; resolveNextStoryKey takes
|
|
818
|
+
// over once the queue exhausts.
|
|
819
|
+
const queueBuildResult = buildExplicitQueueFromOpts(opts, projectRoot);
|
|
820
|
+
if (queueBuildResult.error) {
|
|
821
|
+
log.error(queueBuildResult.error);
|
|
822
|
+
process.stdout.write(
|
|
823
|
+
`${JSON.stringify({ error: queueBuildResult.error, kind: 'queue_validation_error' }, null, 2)}\n`,
|
|
824
|
+
);
|
|
825
|
+
return 2;
|
|
826
|
+
}
|
|
827
|
+
const explicitQueue = queueBuildResult.queue; // may be []
|
|
828
|
+
|
|
829
|
+
// Mid-sprint guard: refuse to overwrite an in-flight queue without
|
|
830
|
+
// --force. The user almost certainly wants to finish what's running
|
|
831
|
+
// before pivoting; a silent overwrite would lose state.
|
|
832
|
+
if (
|
|
833
|
+
explicitQueue.length > 0 &&
|
|
834
|
+
(persisted.current_story || (persisted.story_queue || []).length > 0) &&
|
|
835
|
+
!opts.force
|
|
836
|
+
) {
|
|
837
|
+
const err =
|
|
838
|
+
`Sprint already in progress (current_story=${persisted.current_story || '<queue head>'}). ` +
|
|
839
|
+
`Pass --force to overwrite the queue, or finish the current story first.`;
|
|
840
|
+
log.error(err);
|
|
841
|
+
process.stdout.write(`${JSON.stringify({ error: err, kind: 'mid_sprint_queue_overwrite' }, null, 2)}\n`);
|
|
842
|
+
return 2;
|
|
843
|
+
}
|
|
844
|
+
|
|
657
845
|
// Resume detection: if a prior session left a fingerprint, diff.
|
|
658
846
|
const lastHalt = ledger.last({ projectRoot }, 'halt');
|
|
659
847
|
if (lastHalt && lastHalt.fingerprint) {
|
|
@@ -670,6 +858,25 @@ function cmdStart(opts) {
|
|
|
670
858
|
}
|
|
671
859
|
}
|
|
672
860
|
|
|
861
|
+
// Persist the new queue BEFORE composing runtime state so the queue
|
|
862
|
+
// head is visible to composeRuntimeState's resolver.
|
|
863
|
+
if (explicitQueue.length > 0) {
|
|
864
|
+
persisted.story_queue = explicitQueue;
|
|
865
|
+
// --force overwrite also clears the prior story identity so the
|
|
866
|
+
// queue head is selected cleanly. Without this, persisted.current_
|
|
867
|
+
// story would short-circuit the queue read.
|
|
868
|
+
if (opts.force) {
|
|
869
|
+
persisted.current_story = null;
|
|
870
|
+
persisted.story_file_path = null;
|
|
871
|
+
persisted.current_epic = null;
|
|
872
|
+
persisted.current_bmad_step = null;
|
|
873
|
+
}
|
|
874
|
+
ledger.append(
|
|
875
|
+
{ kind: 'story_queue_set', queue: explicitQueue, force: !!opts.force },
|
|
876
|
+
{ projectRoot },
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
673
880
|
// Fresh start or clean resume. `composeRuntimeState` applies the
|
|
674
881
|
// profile-aware initial phase when persisted state is empty.
|
|
675
882
|
const runtime = composeRuntimeState(persisted, profile, projectRoot);
|
|
@@ -886,7 +1093,7 @@ function cmdStatus(opts) {
|
|
|
886
1093
|
// ------------------------------------------------------------ main
|
|
887
1094
|
|
|
888
1095
|
function main(argv) {
|
|
889
|
-
const { opts, positional } = parseArgs(argv, { booleanFlags: ['help'] });
|
|
1096
|
+
const { opts, positional } = parseArgs(argv, { booleanFlags: ['help', 'force'] });
|
|
890
1097
|
if (opts.help) {
|
|
891
1098
|
help();
|
|
892
1099
|
return 0;
|
|
@@ -35,6 +35,10 @@ const VALID_KINDS = [
|
|
|
35
35
|
'lock_acquired',
|
|
36
36
|
'lock_released',
|
|
37
37
|
'flush',
|
|
38
|
+
// Explicit story queue installed via `autopilot start --stories` /
|
|
39
|
+
// `--epic`. Logged once per start invocation so resume/audit can see
|
|
40
|
+
// why a queue head differs from sprint-status's natural order.
|
|
41
|
+
'story_queue_set',
|
|
38
42
|
];
|
|
39
43
|
|
|
40
44
|
function isPlainObject(v) {
|
|
@@ -548,6 +548,23 @@ function advanceState(state, profile, newPhase, signal) {
|
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
// Story-completion boundary: STORY_DONE → EPIC_BOUNDARY_CHECK means
|
|
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.
|
|
558
|
+
if (state.phase === STATES.STORY_DONE && newPhase === STATES.EPIC_BOUNDARY_CHECK) {
|
|
559
|
+
if (Array.isArray(state.story_queue) && state.story_queue.length > 0) {
|
|
560
|
+
next.story_queue = state.story_queue.slice(1);
|
|
561
|
+
}
|
|
562
|
+
next.story_key = null;
|
|
563
|
+
next.story_file_path = null;
|
|
564
|
+
next.current_epic = null;
|
|
565
|
+
next.ac_summary = null;
|
|
566
|
+
}
|
|
567
|
+
|
|
551
568
|
return next;
|
|
552
569
|
}
|
|
553
570
|
|
|
@@ -27,6 +27,11 @@ const CRITICAL_KEYS = new Set([
|
|
|
27
27
|
'current_bmad_step',
|
|
28
28
|
'in_worktree',
|
|
29
29
|
'patch_commits',
|
|
30
|
+
// Explicit story queue populated by `autopilot start --stories <csv>`
|
|
31
|
+
// / `--epic <id>`. composeRuntimeState reads queue[0] as the next
|
|
32
|
+
// story_key; adapt.advanceState pops the head when a story completes.
|
|
33
|
+
// When empty, the orchestrator falls back to resolveNextStoryKey.
|
|
34
|
+
'story_queue',
|
|
30
35
|
]);
|
|
31
36
|
|
|
32
37
|
// In-memory pending buffer. Process-scoped — flushed at story boundary or
|
|
@@ -26,3 +26,87 @@ Follow **`./workflow.orchestrator.md`** verbatim. Flow control lives in
|
|
|
26
26
|
|
|
27
27
|
`workflow.orchestrator.md` is the **sole authority** for the rest of the
|
|
28
28
|
session.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Natural-language entry: starting at a specific story or epic
|
|
33
|
+
|
|
34
|
+
The user may invoke this skill with extra arguments specifying which
|
|
35
|
+
story / epic to run, e.g.:
|
|
36
|
+
|
|
37
|
+
- `/sprint-autopilot-on epic 4`
|
|
38
|
+
- `/sprint-autopilot-on stories 3.1, 3.2, 4.5`
|
|
39
|
+
- `/sprint-autopilot-on 4-8-realm-wide-matcher`
|
|
40
|
+
- `/sprint-autopilot-on voice identity matcher`
|
|
41
|
+
- `/sprint-autopilot-on starting from 4.5`
|
|
42
|
+
|
|
43
|
+
Translate the natural-language directive into an explicit queue of
|
|
44
|
+
canonical story keys, then call `autopilot start --stories <csv>` (or
|
|
45
|
+
`--epic <id>`). The orchestrator validates the keys before running.
|
|
46
|
+
|
|
47
|
+
**Resolution procedure** — do this BEFORE the first `autopilot next`:
|
|
48
|
+
|
|
49
|
+
1. **Read sprint-status.yaml** at `_bmad-output/implementation-artifacts/sprint-status.yaml`.
|
|
50
|
+
If it's missing, tell the user to run BMad sprint-planning first and
|
|
51
|
+
stop — don't invoke `autopilot start`.
|
|
52
|
+
|
|
53
|
+
2. **Parse the user's directive.** Match against these forms:
|
|
54
|
+
|
|
55
|
+
| User says | Resolve to |
|
|
56
|
+
|---|---|
|
|
57
|
+
| "epic 4", "epic-4", "all of epic 4" | `autopilot start --epic 4` |
|
|
58
|
+
| "stories 3.1, 3.2, 4.5", "3.1 3.2 4.5" | Match each `<epic>.<story>` to canonical keys (`3-1-*`, `3-2-*`, `4-5-*`) in sprint-status order; `autopilot start --stories <csv>` |
|
|
59
|
+
| "4-8-realm-wide-matcher" (already canonical) | `autopilot start --stories 4-8-realm-wide-matcher` |
|
|
60
|
+
| "voice identity", "matcher" (name fragment) | Search story slugs in sprint-status for fuzzy matches |
|
|
61
|
+
| "starting from 4.5" (everything from here) | Resolve `4.5` to canonical key, then queue it + every subsequent non-done story in sprint-status order: `autopilot start --stories <key1>,<key2>,...` |
|
|
62
|
+
| (no extra args) | Plain `autopilot start` — orchestrator picks the next pending story |
|
|
63
|
+
|
|
64
|
+
3. **Ambiguity handling.** If a name fragment or number matches more
|
|
65
|
+
than one story, **do not pick arbitrarily**. List the candidates and
|
|
66
|
+
ask the user to disambiguate. Example:
|
|
67
|
+
|
|
68
|
+
> "voice identity" matches 3 stories:
|
|
69
|
+
> - 3-2-speaker-enrollment
|
|
70
|
+
> - 4-2b-voice-identity-matcher
|
|
71
|
+
> - 4-8-realm-wide-matcher-and-session-lock
|
|
72
|
+
> Which one(s) do you mean?
|
|
73
|
+
|
|
74
|
+
Do not invoke `autopilot start` with a guess.
|
|
75
|
+
|
|
76
|
+
4. **Validation.** Every resolved key must exist in sprint-status.yaml
|
|
77
|
+
and not be `done`. The orchestrator double-checks this and errors
|
|
78
|
+
out otherwise — but verifying ahead of time gives the user clearer
|
|
79
|
+
feedback. If a key resolves to a `done` entry, mention that and ask
|
|
80
|
+
whether they meant something else.
|
|
81
|
+
|
|
82
|
+
5. **Mid-sprint overwrite.** If `autopilot state` shows a sprint already
|
|
83
|
+
in progress (`current_story` is set or `story_queue` is non-empty)
|
|
84
|
+
AND the user is asking to start something different, the orchestrator
|
|
85
|
+
will refuse without `--force`. Confirm with the user before adding
|
|
86
|
+
`--force` — it discards the current story identity.
|
|
87
|
+
|
|
88
|
+
6. **Continuation behavior.** Once the explicit queue exhausts, the
|
|
89
|
+
orchestrator falls back to its normal next-pending-story resolver
|
|
90
|
+
(so a user who says "epic 4" gets epic 4 done, then continues with
|
|
91
|
+
whatever comes next in sprint-status — including epic 5+). Tell the
|
|
92
|
+
user this if they ask.
|
|
93
|
+
|
|
94
|
+
7. **Then proceed normally.** After `autopilot start ...` returns
|
|
95
|
+
successfully, follow `workflow.orchestrator.md` from `autopilot next`
|
|
96
|
+
onward.
|
|
97
|
+
|
|
98
|
+
### Examples
|
|
99
|
+
|
|
100
|
+
| Input | Resolved invocation |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `/sprint-autopilot-on` | `autopilot start --project-root <root>` |
|
|
103
|
+
| `/sprint-autopilot-on epic 4` | `autopilot start --epic 4 --project-root <root>` |
|
|
104
|
+
| `/sprint-autopilot-on stories 3.1, 3.2` (after matching `3-1-game-engine`, `3-2-input-parser`) | `autopilot start --stories 3-1-game-engine,3-2-input-parser --project-root <root>` |
|
|
105
|
+
| `/sprint-autopilot-on 4-8-realm-wide-matcher-and-session-lock` | `autopilot start --stories 4-8-realm-wide-matcher-and-session-lock --project-root <root>` |
|
|
106
|
+
| `/sprint-autopilot-on starting from 4.5` (resolved + all-subsequent) | `autopilot start --stories 4-5-realm-config,4-8-realm-wide-matcher --project-root <root>` |
|
|
107
|
+
|
|
108
|
+
Failure cases that should stop you (do NOT invoke autopilot):
|
|
109
|
+
|
|
110
|
+
- `sprint-status.yaml` missing → ask the user to run sprint-planning.
|
|
111
|
+
- Ambiguous match → list candidates, ask which.
|
|
112
|
+
- Every resolved key is `done` → tell the user there's nothing to run.
|
package/package.json
CHANGED