@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.
@@ -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 === 'done') continue;
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;
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 2.2.30
3
+ version: 2.2.31
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:
@@ -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.30",
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": {