@ikunin/sprintpilot 2.2.30 → 2.2.31
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/_Sprintpilot/bin/autopilot.js +21 -1
- package/_Sprintpilot/lib/orchestrator/user-command-applier.js +29 -0
- package/_Sprintpilot/lib/orchestrator/user-commands.js +10 -1
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.orchestrator.md +1 -1
- package/package.json +1 -1
|
@@ -343,6 +343,26 @@ function readSprintStatuses(projectRoot) {
|
|
|
343
343
|
// - sprint-status doesn't exist or fails to parse
|
|
344
344
|
// - no stories match the epic
|
|
345
345
|
// - all matching stories are already done
|
|
346
|
+
// Terminal statuses for sprint-status entries — stories in these states
|
|
347
|
+
// are NOT counted as "remaining" for epic-done routing. BMad's official
|
|
348
|
+
// vocabulary only has `done`, but users frequently need to mark stories
|
|
349
|
+
// out-of-scope without lying that they shipped:
|
|
350
|
+
// - skipped / wont_do / cancelled / deferred — explicit user intent
|
|
351
|
+
// - abandoned — alternate spelling seen in the wild
|
|
352
|
+
// Pre-2.2.31 only `done` counted, so any deferred 4-N story trapped the
|
|
353
|
+
// orchestrator on next-story routing instead of letting the user close
|
|
354
|
+
// out the epic with a retrospective.
|
|
355
|
+
const TERMINAL_STATUSES = new Set([
|
|
356
|
+
'done',
|
|
357
|
+
'skipped',
|
|
358
|
+
'wont_do',
|
|
359
|
+
"won't_do",
|
|
360
|
+
'cancelled',
|
|
361
|
+
'canceled',
|
|
362
|
+
'deferred',
|
|
363
|
+
'abandoned',
|
|
364
|
+
]);
|
|
365
|
+
|
|
346
366
|
function resolveStoriesForEpic(projectRoot, epicId) {
|
|
347
367
|
if (!epicId) return [];
|
|
348
368
|
const stories = readSprintStatuses(projectRoot);
|
|
@@ -354,7 +374,7 @@ function resolveStoriesForEpic(projectRoot, epicId) {
|
|
|
354
374
|
const derivedEpic = deriveEpicFromStoryKey(key);
|
|
355
375
|
if (derivedEpic !== epicId && derivedEpic !== `epic-${epicId}`) continue;
|
|
356
376
|
const status = String(stories[key].status || '').trim().toLowerCase();
|
|
357
|
-
if (status
|
|
377
|
+
if (TERMINAL_STATUSES.has(status)) continue;
|
|
358
378
|
out.push(key);
|
|
359
379
|
}
|
|
360
380
|
return out;
|
|
@@ -169,6 +169,35 @@ function applyOne(state, profile, cmd) {
|
|
|
169
169
|
});
|
|
170
170
|
break;
|
|
171
171
|
|
|
172
|
+
case 'trigger_retrospective':
|
|
173
|
+
// Force-route to RETROSPECTIVE regardless of remaining_stories_in_epic.
|
|
174
|
+
// Used when the user explicitly wants to close out the current epic
|
|
175
|
+
// with deferred stories still showing as non-terminal in sprint-status.
|
|
176
|
+
// Story-bound fields cleared so the retro skill reads from current_epic.
|
|
177
|
+
newState = {
|
|
178
|
+
...state,
|
|
179
|
+
phase: STATES.RETROSPECTIVE,
|
|
180
|
+
story_key: null,
|
|
181
|
+
story_file_path: null,
|
|
182
|
+
ac_summary: null,
|
|
183
|
+
prior_diagnosis: null,
|
|
184
|
+
patch_findings: null,
|
|
185
|
+
tests_to_rerun: null,
|
|
186
|
+
retry_count_this_phase: 0,
|
|
187
|
+
verify_reject_count: 0,
|
|
188
|
+
consecutive_test_failures: 0,
|
|
189
|
+
// current_epic intentionally preserved — retro skill needs it.
|
|
190
|
+
};
|
|
191
|
+
effects.push({
|
|
192
|
+
kind: 'state_transition',
|
|
193
|
+
from: state.phase,
|
|
194
|
+
to: STATES.RETROSPECTIVE,
|
|
195
|
+
reason: 'user_trigger_retrospective',
|
|
196
|
+
epic: state.current_epic || null,
|
|
197
|
+
details: cmd.reason || null,
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
|
|
172
201
|
default:
|
|
173
202
|
effects.push({ kind: 'state_transition', reason: 'unknown_user_command', cmd });
|
|
174
203
|
}
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
// the stored alternative as the next action and clears the pending
|
|
20
20
|
// entry. Validation rejects this kind when no alternative is pending
|
|
21
21
|
// in state — see user-command-applier.js for the runtime check.
|
|
22
|
+
// trigger_retrospective { reason?: string }
|
|
23
|
+
// Force-routes the orchestrator into RETROSPECTIVE for the current
|
|
24
|
+
// epic regardless of `remaining_stories_in_epic`. Used when the user
|
|
25
|
+
// explicitly wants to close out an epic with deferred stories still
|
|
26
|
+
// in the queue (BMad has no formal `skipped`/`deferred` status for
|
|
27
|
+
// stories, so the orchestrator otherwise counts them as remaining
|
|
28
|
+
// and routes to next-story instead of retro).
|
|
22
29
|
//
|
|
23
30
|
// Validation returns { ok: true, command } | { ok: false, errors: string[] }.
|
|
24
31
|
|
|
@@ -34,6 +41,7 @@ const COMMAND_KINDS = [
|
|
|
34
41
|
'change_profile',
|
|
35
42
|
'pause',
|
|
36
43
|
'accept_alternative',
|
|
44
|
+
'trigger_retrospective',
|
|
37
45
|
];
|
|
38
46
|
|
|
39
47
|
const STORY_KEY_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
@@ -73,7 +81,8 @@ function validateOne(cmd) {
|
|
|
73
81
|
case 'abort_sprint':
|
|
74
82
|
case 'force_continue':
|
|
75
83
|
case 'pause':
|
|
76
|
-
case 'accept_alternative':
|
|
84
|
+
case 'accept_alternative':
|
|
85
|
+
case 'trigger_retrospective': {
|
|
77
86
|
if ('reason' in cmd && cmd.reason !== undefined && typeof cmd.reason !== 'string')
|
|
78
87
|
errors.push(`${cmd.kind}.reason must be string when present`);
|
|
79
88
|
break;
|
|
@@ -83,7 +83,7 @@ Wrap everything in `{ "status": "...", ... }` and pass to
|
|
|
83
83
|
| `failure` | `reason`, `diagnosis` (first-class — fed back into next retry), `recoverable: boolean` |
|
|
84
84
|
| `blocked` | `blocker_kind` (one of the 5 TRUE BLOCKERS or recoverable kinds), `details`, `user_input_needed`, `consecutive_count?` |
|
|
85
85
|
| `propose_alternative` | `reason`, `alternative` (full Action object), `urgency_hint?` (raises impact only). Low impact → auto-accepted; medium / high → orchestrator stores the alternative in `state.pending_alternative` and emits `user_prompt`. The user accepts via `user_input` `{ kind: 'accept_alternative' }` or rejects via `force_continue` (both clear `pending_alternative`). |
|
|
86
|
-
| `user_input` | `commands: UserCommand[]` (validated server-side; see user-commands.js). Kinds: `skip_story`, `abort_sprint`, `force_continue`, `override_decision`, `change_profile`, `pause` (cleanly halts THIS session; next `/sprint-autopilot-on` resumes), `accept_alternative` (dispatches the stored `pending_alternative`). **NEVER send `pause` on your own initiative** — see "Pause is human-only" below. |
|
|
86
|
+
| `user_input` | `commands: UserCommand[]` (validated server-side; see user-commands.js). Kinds: `skip_story`, `abort_sprint`, `force_continue`, `override_decision`, `change_profile`, `pause` (cleanly halts THIS session; next `/sprint-autopilot-on` resumes), `accept_alternative` (dispatches the stored `pending_alternative`), `trigger_retrospective` (v2.2.31+ — force-routes to RETROSPECTIVE for the current epic regardless of `remaining_stories_in_epic`; use when the user explicitly says "close out epic N with retro" while non-terminal stories remain). **NEVER send `pause` on your own initiative** — see "Pause is human-only" below. |
|
|
87
87
|
| `verify_override` | `evidence: { decision_log_ref?, explanation, expected_paths? }` — used when verify.js is wrong |
|
|
88
88
|
|
|
89
89
|
## Pause is human-only
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikunin/sprintpilot",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.31",
|
|
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": {
|