@slope-dev/slope 1.59.2 → 1.61.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.
Files changed (207) hide show
  1. package/README.md +6 -0
  2. package/dist/cli/actor.d.ts +18 -0
  3. package/dist/cli/actor.d.ts.map +1 -0
  4. package/dist/cli/actor.js +133 -0
  5. package/dist/cli/actor.js.map +1 -0
  6. package/dist/cli/commands/agent.d.ts.map +1 -1
  7. package/dist/cli/commands/agent.js +10 -8
  8. package/dist/cli/commands/agent.js.map +1 -1
  9. package/dist/cli/commands/briefing.d.ts.map +1 -1
  10. package/dist/cli/commands/briefing.js +12 -1
  11. package/dist/cli/commands/briefing.js.map +1 -1
  12. package/dist/cli/commands/claim.d.ts.map +1 -1
  13. package/dist/cli/commands/claim.js +10 -6
  14. package/dist/cli/commands/claim.js.map +1 -1
  15. package/dist/cli/commands/commit-ready.d.ts.map +1 -1
  16. package/dist/cli/commands/commit-ready.js +4 -2
  17. package/dist/cli/commands/commit-ready.js.map +1 -1
  18. package/dist/cli/commands/init.d.ts.map +1 -1
  19. package/dist/cli/commands/init.js +6 -0
  20. package/dist/cli/commands/init.js.map +1 -1
  21. package/dist/cli/commands/interview.d.ts.map +1 -1
  22. package/dist/cli/commands/interview.js +5 -0
  23. package/dist/cli/commands/interview.js.map +1 -1
  24. package/dist/cli/commands/next.d.ts +1 -1
  25. package/dist/cli/commands/next.d.ts.map +1 -1
  26. package/dist/cli/commands/next.js +31 -2
  27. package/dist/cli/commands/next.js.map +1 -1
  28. package/dist/cli/commands/now.d.ts +3 -0
  29. package/dist/cli/commands/now.d.ts.map +1 -1
  30. package/dist/cli/commands/now.js +14 -2
  31. package/dist/cli/commands/now.js.map +1 -1
  32. package/dist/cli/commands/release.d.ts.map +1 -1
  33. package/dist/cli/commands/release.js +7 -3
  34. package/dist/cli/commands/release.js.map +1 -1
  35. package/dist/cli/commands/retro.d.ts.map +1 -1
  36. package/dist/cli/commands/retro.js +27 -6
  37. package/dist/cli/commands/retro.js.map +1 -1
  38. package/dist/cli/commands/review-run.d.ts +2 -0
  39. package/dist/cli/commands/review-run.d.ts.map +1 -1
  40. package/dist/cli/commands/review-run.js +38 -8
  41. package/dist/cli/commands/review-run.js.map +1 -1
  42. package/dist/cli/commands/review-state.d.ts +2 -5
  43. package/dist/cli/commands/review-state.d.ts.map +1 -1
  44. package/dist/cli/commands/review-state.js +211 -47
  45. package/dist/cli/commands/review-state.js.map +1 -1
  46. package/dist/cli/commands/review.d.ts +9 -1
  47. package/dist/cli/commands/review.d.ts.map +1 -1
  48. package/dist/cli/commands/review.js +67 -39
  49. package/dist/cli/commands/review.js.map +1 -1
  50. package/dist/cli/commands/roadmap.d.ts.map +1 -1
  51. package/dist/cli/commands/roadmap.js +291 -9
  52. package/dist/cli/commands/roadmap.js.map +1 -1
  53. package/dist/cli/commands/sprint.d.ts.map +1 -1
  54. package/dist/cli/commands/sprint.js +594 -68
  55. package/dist/cli/commands/sprint.js.map +1 -1
  56. package/dist/cli/commands/start.d.ts.map +1 -1
  57. package/dist/cli/commands/start.js +20 -4
  58. package/dist/cli/commands/start.js.map +1 -1
  59. package/dist/cli/commands/ticket.d.ts.map +1 -1
  60. package/dist/cli/commands/ticket.js +31 -9
  61. package/dist/cli/commands/ticket.js.map +1 -1
  62. package/dist/cli/commands/version.d.ts +12 -0
  63. package/dist/cli/commands/version.d.ts.map +1 -1
  64. package/dist/cli/commands/version.js +169 -4
  65. package/dist/cli/commands/version.js.map +1 -1
  66. package/dist/cli/git-preflight.d.ts +9 -0
  67. package/dist/cli/git-preflight.d.ts.map +1 -0
  68. package/dist/cli/git-preflight.js +36 -0
  69. package/dist/cli/git-preflight.js.map +1 -0
  70. package/dist/cli/guards/claim-required.d.ts.map +1 -1
  71. package/dist/cli/guards/claim-required.js +19 -4
  72. package/dist/cli/guards/claim-required.js.map +1 -1
  73. package/dist/cli/guards/next-action.js +1 -1
  74. package/dist/cli/guards/next-action.js.map +1 -1
  75. package/dist/cli/guards/phase-boundary.js +60 -10
  76. package/dist/cli/guards/phase-boundary.js.map +1 -1
  77. package/dist/cli/guards/post-push.d.ts.map +1 -1
  78. package/dist/cli/guards/post-push.js +8 -2
  79. package/dist/cli/guards/post-push.js.map +1 -1
  80. package/dist/cli/guards/pr-review.d.ts +6 -0
  81. package/dist/cli/guards/pr-review.d.ts.map +1 -1
  82. package/dist/cli/guards/pr-review.js +22 -11
  83. package/dist/cli/guards/pr-review.js.map +1 -1
  84. package/dist/cli/guards/review-tier.d.ts.map +1 -1
  85. package/dist/cli/guards/review-tier.js +2 -3
  86. package/dist/cli/guards/review-tier.js.map +1 -1
  87. package/dist/cli/guards/roadmap-edit-shipped.d.ts.map +1 -1
  88. package/dist/cli/guards/roadmap-edit-shipped.js +143 -7
  89. package/dist/cli/guards/roadmap-edit-shipped.js.map +1 -1
  90. package/dist/cli/guards/scope-drift.d.ts.map +1 -1
  91. package/dist/cli/guards/scope-drift.js +8 -2
  92. package/dist/cli/guards/scope-drift.js.map +1 -1
  93. package/dist/cli/guards/session-briefing.d.ts.map +1 -1
  94. package/dist/cli/guards/session-briefing.js +24 -2
  95. package/dist/cli/guards/session-briefing.js.map +1 -1
  96. package/dist/cli/guards/sprint-completion.d.ts.map +1 -1
  97. package/dist/cli/guards/sprint-completion.js +107 -18
  98. package/dist/cli/guards/sprint-completion.js.map +1 -1
  99. package/dist/cli/guards/workflow-step-gate.d.ts.map +1 -1
  100. package/dist/cli/guards/workflow-step-gate.js +25 -6
  101. package/dist/cli/guards/workflow-step-gate.js.map +1 -1
  102. package/dist/cli/index.js +4 -2
  103. package/dist/cli/index.js.map +1 -1
  104. package/dist/cli/registry.d.ts.map +1 -1
  105. package/dist/cli/registry.js +45 -3
  106. package/dist/cli/registry.js.map +1 -1
  107. package/dist/cli/reviewer-agents.d.ts +22 -0
  108. package/dist/cli/reviewer-agents.d.ts.map +1 -0
  109. package/dist/cli/reviewer-agents.js +141 -0
  110. package/dist/cli/reviewer-agents.js.map +1 -0
  111. package/dist/cli/roadmap-source-store.d.ts +37 -0
  112. package/dist/cli/roadmap-source-store.d.ts.map +1 -0
  113. package/dist/cli/roadmap-source-store.js +298 -0
  114. package/dist/cli/roadmap-source-store.js.map +1 -0
  115. package/dist/cli/sprint-inference.js +1 -1
  116. package/dist/cli/sprint-inference.js.map +1 -1
  117. package/dist/cli/sprint-rollover.d.ts +102 -0
  118. package/dist/cli/sprint-rollover.d.ts.map +1 -0
  119. package/dist/cli/sprint-rollover.js +558 -0
  120. package/dist/cli/sprint-rollover.js.map +1 -0
  121. package/dist/cli/sprint-state.d.ts +99 -2
  122. package/dist/cli/sprint-state.d.ts.map +1 -1
  123. package/dist/cli/sprint-state.js +349 -14
  124. package/dist/cli/sprint-state.js.map +1 -1
  125. package/dist/cli/template-generator.d.ts.map +1 -1
  126. package/dist/cli/template-generator.js +45 -32
  127. package/dist/cli/template-generator.js.map +1 -1
  128. package/dist/cli/workflow-resync.d.ts +2 -0
  129. package/dist/cli/workflow-resync.d.ts.map +1 -1
  130. package/dist/cli/workflow-resync.js +34 -12
  131. package/dist/cli/workflow-resync.js.map +1 -1
  132. package/dist/core/analyzers/git.d.ts.map +1 -1
  133. package/dist/core/analyzers/git.js +95 -5
  134. package/dist/core/analyzers/git.js.map +1 -1
  135. package/dist/core/briefing.d.ts.map +1 -1
  136. package/dist/core/briefing.js +254 -14
  137. package/dist/core/briefing.js.map +1 -1
  138. package/dist/core/builder.d.ts +16 -2
  139. package/dist/core/builder.d.ts.map +1 -1
  140. package/dist/core/builder.js +82 -5
  141. package/dist/core/builder.js.map +1 -1
  142. package/dist/core/constants.d.ts.map +1 -1
  143. package/dist/core/constants.js +1 -0
  144. package/dist/core/constants.js.map +1 -1
  145. package/dist/core/findings.d.ts +20 -0
  146. package/dist/core/findings.d.ts.map +1 -0
  147. package/dist/core/findings.js +85 -0
  148. package/dist/core/findings.js.map +1 -0
  149. package/dist/core/generators/first-sprint.js +1 -1
  150. package/dist/core/generators/first-sprint.js.map +1 -1
  151. package/dist/core/generators/roadmap.js +3 -3
  152. package/dist/core/generators/roadmap.js.map +1 -1
  153. package/dist/core/index.d.ts +10 -4
  154. package/dist/core/index.d.ts.map +1 -1
  155. package/dist/core/index.js +5 -1
  156. package/dist/core/index.js.map +1 -1
  157. package/dist/core/interview-engine.d.ts.map +1 -1
  158. package/dist/core/interview-engine.js +5 -4
  159. package/dist/core/interview-engine.js.map +1 -1
  160. package/dist/core/interview-metaphor.d.ts +5 -0
  161. package/dist/core/interview-metaphor.d.ts.map +1 -0
  162. package/dist/core/interview-metaphor.js +20 -0
  163. package/dist/core/interview-metaphor.js.map +1 -0
  164. package/dist/core/interview-steps.d.ts.map +1 -1
  165. package/dist/core/interview-steps.js +3 -6
  166. package/dist/core/interview-steps.js.map +1 -1
  167. package/dist/core/interview.d.ts.map +1 -1
  168. package/dist/core/interview.js +5 -6
  169. package/dist/core/interview.js.map +1 -1
  170. package/dist/core/roadmap-focus.d.ts +101 -0
  171. package/dist/core/roadmap-focus.d.ts.map +1 -0
  172. package/dist/core/roadmap-focus.js +293 -0
  173. package/dist/core/roadmap-focus.js.map +1 -0
  174. package/dist/core/roadmap-sources.d.ts +52 -0
  175. package/dist/core/roadmap-sources.d.ts.map +1 -0
  176. package/dist/core/roadmap-sources.js +483 -0
  177. package/dist/core/roadmap-sources.js.map +1 -0
  178. package/dist/core/roadmap.d.ts +25 -1
  179. package/dist/core/roadmap.d.ts.map +1 -1
  180. package/dist/core/roadmap.js +62 -45
  181. package/dist/core/roadmap.js.map +1 -1
  182. package/dist/core/types.d.ts +11 -1
  183. package/dist/core/types.d.ts.map +1 -1
  184. package/dist/core/workflow-engine.d.ts.map +1 -1
  185. package/dist/core/workflow-engine.js +17 -2
  186. package/dist/core/workflow-engine.js.map +1 -1
  187. package/dist/mcp/registry.d.ts +1 -1
  188. package/dist/mcp/registry.d.ts.map +1 -1
  189. package/dist/mcp/registry.js +5 -4
  190. package/dist/mcp/registry.js.map +1 -1
  191. package/dist/store/index.d.ts +13 -0
  192. package/dist/store/index.d.ts.map +1 -1
  193. package/dist/store/index.js +60 -7
  194. package/dist/store/index.js.map +1 -1
  195. package/package.json +1 -1
  196. package/src/core/workflows/sprint-standard.yaml +7 -1
  197. package/templates/claude-code/CLAUDE.md +3 -1
  198. package/templates/claude-code/commands/post-sprint.md +2 -0
  199. package/templates/claude-code/commands/start-sprint.md +10 -5
  200. package/templates/claude-code/rules/sprint-checklist.md +13 -10
  201. package/templates/codex/plugins/slope/.codex-plugin/plugin.json +1 -1
  202. package/templates/codex/plugins/slope/skills/slope-retro/SKILL.md +1 -1
  203. package/templates/codex/plugins/slope/skills/slope-sprint/SKILL.md +5 -4
  204. package/templates/cursor/rules/slope-sprint-checklist.mdc +11 -8
  205. package/templates/generic/SLOPE-CHECKLIST.md +13 -9
  206. package/templates/pi/skills/post-sprint.md +2 -0
  207. package/templates/pi/skills/start-sprint.md +10 -5
@@ -1,6 +1,8 @@
1
- import { loadSprintState, saveSprintState, createSprintState, mutateSprintState, updateGate, updateSprintPhase, clearSprintState, isSprintComplete, pendingGates, isSprintPhase, SPRINT_PHASES, } from '../sprint-state.js';
1
+ import { loadSprintState, loadSprintStateResult, createSprintState, initializeSprintState, mutateSprintState, updateGate, updateSprintPhase, updateSprintPhaseForSprint, clearSprintState, isSprintComplete, isReviewGateSatisfied, isRequiredReviewGate, waivedReviewGateNames, pendingGateNames, pendingGates, isSprintPhase, isReviewGateName, validateReviewGateCompletion, SPRINT_PHASES, } from '../sprint-state.js';
2
2
  import { WorkflowEngine, loadWorkflow, resolveVariables, validateWorkflow, loadConfig, parseRoadmap, castRoadmapStructure, formatSprintLabel, formatSprintNumber, parseSprintNumber } from '../../core/index.js';
3
3
  import { createHash } from 'node:crypto';
4
+ import { formatActorName, formatActorSource, formatConflictSummary, resolveActor } from '../actor.js';
5
+ import { inspectSprintRollover, performSprintRollover, SprintRolloverError, verifySprintRolloverLineage, } from '../sprint-rollover.js';
4
6
  /** Get workflow definition from execution snapshot (preferred) or disk (fallback for old executions) */
5
7
  function getDefinition(exec, cwd) {
6
8
  // Prefer snapshot from execution
@@ -20,12 +22,12 @@ function getDefinition(exec, cwd) {
20
22
  return { def: loadWorkflow(exec.workflow_name, cwd), drifted: false };
21
23
  }
22
24
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
23
- import { join, dirname, basename, isAbsolute } from 'node:path';
25
+ import { join, dirname, basename, isAbsolute, relative } from 'node:path';
24
26
  import { createStore } from '../../store/index.js';
25
27
  import { formatCliError } from '../error-reporter.js';
26
28
  import { buildSprintResumePointer, planPortableSprintResume, writeSprintResumePointer, } from '../sprint-resume.js';
27
29
  import { blockingRoadmapIssuesForSprint, collectSiblingWorktreeReality, findWorktreeOverlaps, formatWorktreeRealitySection, loadRoadmapReality, parseTouchedPaths, } from '../pre-sprint-reality.js';
28
- import { findStaleWorkflowExecutions, reconcileWorkflowExecutions } from '../workflow-resync.js';
30
+ import { findStaleWorkflowExecutions, reconcileWorkflowExecutions, sprintLabelForExecution } from '../workflow-resync.js';
29
31
  /**
30
32
  * Check completion_conditions for a step before allowing completion/skip.
31
33
  * Returns null if all conditions met, or an error message string.
@@ -67,6 +69,358 @@ function checkCompletionConditions(stepId, def, currentPhase, cwd) {
67
69
  return null;
68
70
  }
69
71
  const VALID_GATES = ['tests', 'code_review', 'architect_review', 'scorecard', 'review_md'];
72
+ function optionValue(args, index, flag) {
73
+ const arg = args[index];
74
+ if (arg.startsWith(`${flag}=`))
75
+ return { value: arg.slice(flag.length + 1), next: index };
76
+ if (arg === flag)
77
+ return { value: args[index + 1] ?? null, next: index + 1 };
78
+ return null;
79
+ }
80
+ function findOptionValue(args, flag) {
81
+ for (let i = 0; i < args.length; i++) {
82
+ const value = optionValue(args, i, flag);
83
+ if (value)
84
+ return value.value ?? undefined;
85
+ }
86
+ return undefined;
87
+ }
88
+ function actorOverride(args) {
89
+ for (const flag of ['--actor', '--player']) {
90
+ const value = findOptionValue(args, flag);
91
+ if (value && !value.startsWith('--'))
92
+ return value;
93
+ }
94
+ return undefined;
95
+ }
96
+ function parseGateOptions(args) {
97
+ const errors = [];
98
+ const evidence = [];
99
+ let reviewer;
100
+ let prReview;
101
+ let selfReview = false;
102
+ let waiveIndependentReview = false;
103
+ let notes;
104
+ let reason;
105
+ let overrideReason;
106
+ let help = false;
107
+ for (let i = 0; i < args.length; i++) {
108
+ const arg = args[i];
109
+ if (arg === '--help' || arg === '-h') {
110
+ help = true;
111
+ continue;
112
+ }
113
+ const evidenceValue = optionValue(args, i, '--evidence');
114
+ if (evidenceValue) {
115
+ if (!evidenceValue.value || evidenceValue.value.startsWith('--'))
116
+ errors.push('--evidence requires a value');
117
+ else
118
+ evidence.push(evidenceValue.value);
119
+ i = evidenceValue.next;
120
+ continue;
121
+ }
122
+ const reviewerValue = optionValue(args, i, '--reviewer');
123
+ if (reviewerValue) {
124
+ if (!reviewerValue.value || reviewerValue.value.startsWith('--'))
125
+ errors.push('--reviewer requires a value');
126
+ else
127
+ reviewer = reviewerValue.value;
128
+ i = reviewerValue.next;
129
+ continue;
130
+ }
131
+ const prReviewValue = optionValue(args, i, '--pr-review');
132
+ if (prReviewValue) {
133
+ if (!prReviewValue.value || prReviewValue.value.startsWith('--'))
134
+ errors.push('--pr-review requires a value');
135
+ else
136
+ prReview = prReviewValue.value;
137
+ i = prReviewValue.next;
138
+ continue;
139
+ }
140
+ if (arg === '--self-review') {
141
+ selfReview = true;
142
+ continue;
143
+ }
144
+ if (arg === '--waive-independent-review') {
145
+ waiveIndependentReview = true;
146
+ continue;
147
+ }
148
+ const reasonValue = optionValue(args, i, '--reason');
149
+ if (reasonValue) {
150
+ if (!reasonValue.value || reasonValue.value.startsWith('--'))
151
+ errors.push('--reason requires a value');
152
+ else
153
+ reason = reasonValue.value;
154
+ i = reasonValue.next;
155
+ continue;
156
+ }
157
+ const notesValue = optionValue(args, i, '--notes');
158
+ if (notesValue) {
159
+ if (!notesValue.value || notesValue.value.startsWith('--'))
160
+ errors.push('--notes requires a value');
161
+ else
162
+ notes = notesValue.value;
163
+ i = notesValue.next;
164
+ continue;
165
+ }
166
+ const overrideValue = optionValue(args, i, '--override') ?? optionValue(args, i, '--manual-override');
167
+ if (overrideValue) {
168
+ if (!overrideValue.value || overrideValue.value.startsWith('--'))
169
+ errors.push('--override requires a value');
170
+ else
171
+ overrideReason = overrideValue.value;
172
+ i = overrideValue.next;
173
+ continue;
174
+ }
175
+ errors.push(`Unknown gate option: ${arg}`);
176
+ }
177
+ const hasPrReview = Boolean(prReview);
178
+ const hasSelfReview = selfReview;
179
+ const hasWaiver = waiveIndependentReview;
180
+ const hasOverride = Boolean(overrideReason);
181
+ const hasIndependent = !hasPrReview && !hasSelfReview && !hasWaiver && !hasOverride && (Boolean(reviewer) || evidence.length > 0);
182
+ const modeCount = [hasPrReview, hasSelfReview, hasWaiver, hasOverride, hasIndependent].filter(Boolean).length;
183
+ if (modeCount > 1) {
184
+ errors.push('Use only one review provenance mode: --reviewer/--evidence, --pr-review, --self-review, --waive-independent-review, or --override.');
185
+ }
186
+ let review;
187
+ if (modeCount === 1) {
188
+ if (hasPrReview) {
189
+ review = { provenance: 'pr_review', evidence: [prReview, ...evidence], reviewer, notes: notes ?? reason };
190
+ }
191
+ else if (hasSelfReview) {
192
+ review = { provenance: 'self_review', evidence, reviewer, notes: reason ?? notes };
193
+ }
194
+ else if (hasWaiver) {
195
+ review = { provenance: 'independent_review_waived', evidence, reviewer, notes: reason ?? notes };
196
+ }
197
+ else if (hasOverride) {
198
+ review = { provenance: 'manual_override', evidence, reviewer, notes: overrideReason };
199
+ }
200
+ else {
201
+ review = { provenance: 'independent_review', evidence, reviewer, notes: notes ?? reason };
202
+ }
203
+ }
204
+ return { review, errors, help };
205
+ }
206
+ function printGateUsage(gateName) {
207
+ const gate = gateName && isReviewGateName(gateName) ? gateName : 'code_review';
208
+ console.error('');
209
+ console.error('Usage:');
210
+ console.error(' slope sprint gate <name>');
211
+ console.error(` slope sprint gate ${gate} --reviewer=<agent-or-person> --evidence=<transcript-or-output>`);
212
+ console.error(` slope sprint gate ${gate} --pr-review=<url-or-id>`);
213
+ console.error(` slope sprint gate ${gate} --self-review --reason="why self-review is acceptable"`);
214
+ console.error(` slope sprint gate ${gate} --waive-independent-review --reason="why the required independent review is being waived"`);
215
+ console.error(` slope sprint gate ${gate} --override="manual override reason"`);
216
+ console.error('');
217
+ console.error('Review provenance modes:');
218
+ console.error(' independent_review: requires --reviewer and --evidence');
219
+ console.error(' pr_review: records external PR review evidence from --pr-review');
220
+ console.error(' self_review (weaker): requires --self-review and --reason');
221
+ console.error(' independent_review_waived: explicit waiver for a required review; requires --waive-independent-review and --reason');
222
+ console.error(' manual_override (weaker): requires --override');
223
+ console.error('');
224
+ }
225
+ function commandValue(value) {
226
+ if (/^[A-Za-z0-9._:/-]+$/.test(value))
227
+ return value;
228
+ return process.platform === 'win32'
229
+ ? `'${value.replaceAll("'", "''")}'`
230
+ : `'${value.replaceAll("'", `'"'"'`)}`;
231
+ }
232
+ function actorRetryOption(args) {
233
+ for (const flag of ['--actor', '--player']) {
234
+ const value = findOptionValue(args, flag);
235
+ if (value)
236
+ return `${flag}=${commandValue(value)}`;
237
+ }
238
+ return null;
239
+ }
240
+ function commandOptionErrors(args, valueFlags, booleanFlags = []) {
241
+ const errors = [];
242
+ const seen = new Set();
243
+ for (let index = 0; index < args.length; index++) {
244
+ const arg = args[index];
245
+ let matched = false;
246
+ for (const flag of valueFlags) {
247
+ const value = optionValue(args, index, flag);
248
+ if (!value)
249
+ continue;
250
+ matched = true;
251
+ if (seen.has(flag))
252
+ errors.push(`${flag} may only be provided once`);
253
+ seen.add(flag);
254
+ if (!value.value || value.value.startsWith('--'))
255
+ errors.push(`${flag} requires a value`);
256
+ index = value.next;
257
+ break;
258
+ }
259
+ if (matched)
260
+ continue;
261
+ if (booleanFlags.includes(arg)) {
262
+ if (seen.has(arg))
263
+ errors.push(`${arg} may only be provided once`);
264
+ seen.add(arg);
265
+ continue;
266
+ }
267
+ errors.push(`Unknown option: ${arg}`);
268
+ }
269
+ if (seen.has('--actor') && seen.has('--player')) {
270
+ errors.push('Use only one actor identity flag: --actor or --player');
271
+ }
272
+ return errors;
273
+ }
274
+ function rolloverFlagValue(label) {
275
+ return label.startsWith('S') ? label.slice(1) : label;
276
+ }
277
+ function rolloverBaseCommand(assessment) {
278
+ return `slope sprint rollover --from=${rolloverFlagValue(assessment.from_label)} --to=${rolloverFlagValue(assessment.to_label)}`;
279
+ }
280
+ /**
281
+ * Return true when the requested sprint is the existing sprint, including a
282
+ * roadmap-aware legacy alias. Otherwise print bounded recovery guidance and
283
+ * terminate without mutating local state.
284
+ */
285
+ function requireMatchingSprintOrRollover(cwd, state, requestedSprint, action, retryCommand) {
286
+ try {
287
+ verifySprintRolloverLineage(cwd, state);
288
+ }
289
+ catch (error) {
290
+ console.error(`Refusing to ${action}: ${error.message}`);
291
+ console.error(`After restoring the tracked rollover audit, retry: ${retryCommand}`);
292
+ process.exit(1);
293
+ }
294
+ if (state.sprint === requestedSprint)
295
+ return true;
296
+ let assessment;
297
+ try {
298
+ assessment = inspectSprintRollover(cwd, { from: state.sprint, to: requestedSprint });
299
+ }
300
+ catch (error) {
301
+ console.error(`Refusing to ${action}: ${error.message}`);
302
+ console.error(`After resolving the sprint state, retry: ${retryCommand}`);
303
+ process.exit(1);
304
+ }
305
+ if (assessment.from_sprint && assessment.to_sprint && assessment.from === assessment.to)
306
+ return true;
307
+ console.error(`Refusing to ${action} — sprint-state.json is for ${assessment.from_label}, not ${assessment.to_label}.`);
308
+ const eligibilityIssues = assessment.issues.filter(issue => issue.code !== 'from_not_terminal');
309
+ if (eligibilityIssues.length === 0) {
310
+ const command = assessment.from_terminal
311
+ ? rolloverBaseCommand(assessment)
312
+ : `${rolloverBaseCommand(assessment)} --force --reason="<why>"`;
313
+ console.error(`Record the prior state with: ${command}`);
314
+ }
315
+ else {
316
+ console.error('Rollover is not currently eligible:');
317
+ for (const issue of eligibilityIssues)
318
+ console.error(` - ${issue.message}`);
319
+ }
320
+ console.error(`After resolving the rollover, retry: ${retryCommand}`);
321
+ process.exit(1);
322
+ }
323
+ function failOnCorruptSprintState(cwd) {
324
+ const loaded = loadSprintStateResult(cwd);
325
+ if (loaded.status !== 'corrupt')
326
+ return;
327
+ console.error(`Refusing to change sprint state: corrupt evidence was preserved at ${relative(cwd, loaded.path)}.`);
328
+ process.exit(1);
329
+ }
330
+ function printRolloverUsage() {
331
+ console.error('');
332
+ console.error('Usage: slope sprint rollover --from=N --to=N [--force --reason="<why>"]');
333
+ console.error('Safely archives the prior local state in a tracked audit before creating the next planning state.');
334
+ console.error('');
335
+ }
336
+ function rolloverCommand(args, cwd) {
337
+ if (args.includes('--help') || args.includes('-h')) {
338
+ printRolloverUsage();
339
+ return;
340
+ }
341
+ let fromValue;
342
+ let toValue;
343
+ let reason;
344
+ let force = false;
345
+ const errors = [];
346
+ const seen = new Set();
347
+ for (let index = 0; index < args.length; index++) {
348
+ const arg = args[index];
349
+ const from = optionValue(args, index, '--from');
350
+ if (from) {
351
+ if (seen.has('--from'))
352
+ errors.push('--from may only be provided once');
353
+ seen.add('--from');
354
+ if (!from.value || from.value.startsWith('--'))
355
+ errors.push('--from requires a sprint id');
356
+ else
357
+ fromValue = from.value;
358
+ index = from.next;
359
+ continue;
360
+ }
361
+ const to = optionValue(args, index, '--to');
362
+ if (to) {
363
+ if (seen.has('--to'))
364
+ errors.push('--to may only be provided once');
365
+ seen.add('--to');
366
+ if (!to.value || to.value.startsWith('--'))
367
+ errors.push('--to requires a sprint id');
368
+ else
369
+ toValue = to.value;
370
+ index = to.next;
371
+ continue;
372
+ }
373
+ const parsedReason = optionValue(args, index, '--reason');
374
+ if (parsedReason) {
375
+ if (seen.has('--reason'))
376
+ errors.push('--reason may only be provided once');
377
+ seen.add('--reason');
378
+ if (!parsedReason.value || parsedReason.value.startsWith('--'))
379
+ errors.push('--reason requires a non-empty value');
380
+ else
381
+ reason = parsedReason.value;
382
+ index = parsedReason.next;
383
+ continue;
384
+ }
385
+ if (arg === '--force') {
386
+ if (seen.has('--force'))
387
+ errors.push('--force may only be provided once');
388
+ seen.add('--force');
389
+ force = true;
390
+ continue;
391
+ }
392
+ errors.push(`Unknown rollover option: ${arg}`);
393
+ }
394
+ const from = fromValue ? parseSprintNumber(fromValue) : null;
395
+ const to = toValue ? parseSprintNumber(toValue) : null;
396
+ if (from == null)
397
+ errors.push('--from must be a positive sprint id');
398
+ if (to == null)
399
+ errors.push('--to must be a positive sprint id');
400
+ if (reason && !force)
401
+ errors.push('--reason is only valid with --force');
402
+ if (errors.length > 0) {
403
+ for (const error of errors)
404
+ console.error(`Error: ${error}`);
405
+ printRolloverUsage();
406
+ process.exit(1);
407
+ }
408
+ try {
409
+ const result = performSprintRollover(cwd, { from: from, to: to, force, reason }, resolveActor(cwd));
410
+ console.log(`${result.already_applied ? 'Rollover already recorded' : 'Rollover recorded'}: ${result.record.from_label} -> ${result.record.to_label}.`);
411
+ console.log(`Audit: ${result.audit_path}`);
412
+ console.log(`Sprint ${rolloverFlagValue(result.record.to_label)} state is ready (phase: ${result.state.phase}).`);
413
+ console.log('Prior state archived; claims and sessions unchanged.');
414
+ console.log(`Next: slope sprint begin --sprint=${rolloverFlagValue(result.record.to_label)} --ticket=<key>`);
415
+ }
416
+ catch (error) {
417
+ if (error instanceof SprintRolloverError) {
418
+ console.error(`Rollover refused: ${error.message}`);
419
+ process.exit(1);
420
+ }
421
+ throw error;
422
+ }
423
+ }
70
424
  /**
71
425
  * `slope sprint begin --sprint=N --ticket=T` — bundled start-of-work flow.
72
426
  *
@@ -79,44 +433,48 @@ const VALID_GATES = ['tests', 'code_review', 'architect_review', 'scorecard', 'r
79
433
  * 6. Print recommended next implementation step
80
434
  */
81
435
  async function beginCommand(args, cwd) {
82
- const sprintArg = args.find(a => a.startsWith('--sprint='));
83
- const ticketArg = args.find(a => a.startsWith('--ticket='));
84
- if (!sprintArg || !ticketArg) {
85
- console.error('\nUsage: slope sprint begin --sprint=N --ticket=KEY');
436
+ const optionErrors = commandOptionErrors(args, ['--sprint', '--ticket', '--actor', '--player']);
437
+ const sprintValue = findOptionValue(args, '--sprint');
438
+ const ticketValue = findOptionValue(args, '--ticket');
439
+ if (optionErrors.length > 0 || !sprintValue || !ticketValue) {
440
+ for (const error of optionErrors)
441
+ console.error(`Error: ${error}`);
442
+ console.error('\nUsage: slope sprint begin --sprint=N --ticket=KEY [--actor=<name>]');
86
443
  console.error('Bundles: sprint start + claim + briefing + prep --lite\n');
87
444
  process.exit(1);
88
445
  }
89
- const sprint = parseSprintNumber(sprintArg.split('=')[1]);
90
- const ticket = ticketArg.split('=')[1];
446
+ const sprint = parseSprintNumber(sprintValue);
447
+ const ticket = ticketValue;
91
448
  if (!sprint || !ticket) {
92
449
  console.error('Error: --sprint must be a positive sprint id; --ticket must be a non-empty key');
93
450
  process.exit(1);
94
451
  }
95
- // Step 1: sprint state
96
- let state = loadSprintState(cwd);
97
- if (state && state.sprint === sprint) {
98
- console.log(`Sprint ${formatSprintNumber(sprint)}: already started (phase: ${state.phase}).`);
99
- }
100
- else if (state && state.sprint !== sprint) {
101
- console.error(`Refusing to begin S${formatSprintNumber(sprint)} — sprint-state.json is for S${formatSprintNumber(state.sprint)}.`);
102
- console.error('Run `slope sprint reset` first if the previous sprint is done.');
452
+ // Step 1: initialize without overwriting existing or corrupt local state.
453
+ const initialized = initializeSprintState(cwd, createSprintState(sprint, 'planning'));
454
+ if (initialized.status === 'corrupt') {
455
+ console.error(`Refusing to begin: corrupt sprint evidence was preserved at ${relative(cwd, initialized.path)}.`);
103
456
  process.exit(1);
104
457
  }
458
+ let state = initialized.state;
459
+ const retryActor = actorRetryOption(args);
460
+ if (initialized.status === 'existing' && requireMatchingSprintOrRollover(cwd, state, sprint, `begin ${formatSprintLabel(sprint)}`, `slope sprint begin --sprint=${formatSprintNumber(sprint)} --ticket=${commandValue(ticket)}${retryActor ? ` ${retryActor}` : ''}`)) {
461
+ console.log(`Sprint ${formatSprintNumber(sprint)}: already started (phase: ${state.phase}).`);
462
+ }
105
463
  else {
106
- state = createSprintState(sprint, 'planning');
107
- saveSprintState(cwd, state);
108
464
  console.log(`Sprint ${formatSprintNumber(sprint)}: started (phase: planning).`);
109
465
  }
110
466
  // Step 2: claim
111
467
  const { resolveStore } = await import('../store.js');
112
468
  const { checkConflicts } = await import('../../core/index.js');
113
- const player = process.env.USER || 'unknown';
469
+ const actor = resolveActor(cwd, { explicitActor: actorOverride(args) });
470
+ const player = actor.name;
471
+ const playerDisplay = formatActorName(actor);
114
472
  const store = await resolveStore(cwd);
115
473
  try {
116
474
  const existing = await store.list(sprint);
117
475
  const ownClaim = existing.find(c => c.target === ticket && c.player === player);
118
476
  if (ownClaim) {
119
- console.log(`Ticket ${ticket}: already claimed by ${player}.`);
477
+ console.log(`Ticket ${ticket}: already claimed by ${playerDisplay} (actor source: ${formatActorSource(actor)}).`);
120
478
  }
121
479
  else {
122
480
  // Detect overlap conflicts via core check
@@ -132,12 +490,12 @@ async function beginCommand(args, cwd) {
132
490
  if (overlaps.length > 0) {
133
491
  console.error(`\nClaim blocked — overlap conflict(s) detected:`);
134
492
  for (const c of overlaps)
135
- console.error(` [!!] ${c.reason}`);
493
+ console.error(` [!!] ${formatConflictSummary(c)}`);
136
494
  console.error(`\nResolve conflicts or run \`slope claim --target=${ticket} --sprint=${formatSprintNumber(sprint)} --force\` to override.`);
137
495
  process.exit(1);
138
496
  }
139
497
  const claim = await store.claim({ sprint_number: sprint, player, target: ticket, scope: 'ticket' });
140
- console.log(`Ticket ${ticket}: claimed (id ${claim.id.slice(0, 8)}, player ${player}).`);
498
+ console.log(`Ticket ${ticket}: claimed (id ${claim.id.slice(0, 8)}, player ${playerDisplay}, actor source: ${formatActorSource(actor)}).`);
141
499
  }
142
500
  }
143
501
  finally {
@@ -195,30 +553,47 @@ async function beginCommand(args, cwd) {
195
553
  console.log('');
196
554
  }
197
555
  async function startCommand(args, cwd) {
198
- const numberArg = args.find(a => a.startsWith('--number='));
199
- if (!numberArg) {
556
+ const optionErrors = commandOptionErrors(args, ['--number', '--phase', '--touches', '--actor', '--player'], ['--force']);
557
+ const numberValue = findOptionValue(args, '--number');
558
+ if (optionErrors.length > 0 || !numberValue) {
559
+ for (const error of optionErrors)
560
+ console.error(`Error: ${error}`);
200
561
  console.error('Error: --number=N is required. Usage: slope sprint start --number=22');
201
562
  process.exit(1);
202
563
  }
203
- const sprint = parseSprintNumber(numberArg.slice('--number='.length));
564
+ const sprint = parseSprintNumber(numberValue);
204
565
  if (!sprint) {
205
566
  console.error('Error: --number must be a positive sprint id, e.g. 114 or 114.5.');
206
567
  process.exit(1);
207
568
  }
208
- const phaseArg = args.find(a => a.startsWith('--phase='));
209
- const phaseInput = phaseArg?.slice('--phase='.length) ?? 'planning';
569
+ const phaseValue = findOptionValue(args, '--phase');
570
+ const phaseInput = phaseValue ?? 'planning';
210
571
  if (!isSprintPhase(phaseInput)) {
211
572
  console.error(`Error: invalid phase "${phaseInput}". Valid phases: ${SPRINT_PHASES.join(', ')}`);
212
573
  process.exit(1);
213
574
  }
214
575
  const phase = phaseInput;
215
576
  const force = args.includes('--force');
216
- const touchesArg = args.find(a => a.startsWith('--touches='));
217
- const touchedPaths = parseTouchedPaths(touchesArg?.slice('--touches='.length));
218
- const existing = loadSprintState(cwd);
219
- if (existing && existing.sprint === sprint) {
220
- if (phaseArg && existing.phase !== phase) {
221
- updateSprintPhase(cwd, phase);
577
+ const touchedPaths = parseTouchedPaths(findOptionValue(args, '--touches'));
578
+ const retryActor = actorRetryOption(args);
579
+ const retryTouches = findOptionValue(args, '--touches');
580
+ const retryExtras = [
581
+ ...(retryTouches ? [`--touches=${commandValue(retryTouches)}`] : []),
582
+ ...(retryActor ? [retryActor] : []),
583
+ ...(force ? ['--force'] : []),
584
+ ];
585
+ const retryStart = `slope sprint start --number=${formatSprintNumber(sprint)} --phase=${phase}${retryExtras.length > 0 ? ` ${retryExtras.join(' ')}` : ''}`;
586
+ failOnCorruptSprintState(cwd);
587
+ const existingResult = loadSprintStateResult(cwd);
588
+ const existing = existingResult.status === 'valid' ? existingResult.state : null;
589
+ if (existing) {
590
+ requireMatchingSprintOrRollover(cwd, existing, sprint, `start ${formatSprintLabel(sprint)}`, retryStart);
591
+ if (phaseValue && existing.phase !== phase) {
592
+ const updated = updateSprintPhaseForSprint(cwd, existing.sprint, phase);
593
+ if (!updated.matched) {
594
+ console.error('Refusing to update phase because sprint state changed concurrently; retry the start command.');
595
+ process.exit(1);
596
+ }
222
597
  console.log(`Sprint ${formatSprintNumber(sprint)} phase updated: ${phase}.`);
223
598
  return;
224
599
  }
@@ -248,23 +623,33 @@ async function startCommand(args, cwd) {
248
623
  console.error('Resolve the overlap, choose a non-overlapping sprint, or rerun with --force if this is intentional.');
249
624
  process.exit(1);
250
625
  }
251
- const state = createSprintState(sprint, phase);
252
- saveSprintState(cwd, state);
253
- const autoClaim = await autoClaimSprint(cwd, sprint);
254
- console.log(`Sprint ${formatSprintNumber(sprint)} started (phase: ${phase}). Use 'slope sprint gate <name>' to mark gates.`);
626
+ const initialized = initializeSprintState(cwd, createSprintState(sprint, phase));
627
+ if (initialized.status === 'corrupt') {
628
+ console.error(`Refusing to start: corrupt sprint evidence was preserved at ${relative(cwd, initialized.path)}.`);
629
+ process.exit(1);
630
+ }
631
+ if (initialized.status === 'existing') {
632
+ requireMatchingSprintOrRollover(cwd, initialized.state, sprint, `start ${formatSprintLabel(sprint)}`, retryStart);
633
+ console.log(`Sprint ${formatSprintNumber(sprint)} state already exists (phase: ${initialized.state.phase}).`);
634
+ return;
635
+ }
636
+ const autoClaim = await autoClaimSprint(cwd, sprint, actorOverride(args));
637
+ console.log(`Sprint ${formatSprintNumber(sprint)} started (phase: ${phase}). Use 'slope sprint gate <name>' to mark gates; review gates require evidence options.`);
255
638
  if (autoClaim)
256
639
  console.log(autoClaim);
257
640
  }
258
- async function autoClaimSprint(cwd, sprint) {
641
+ async function autoClaimSprint(cwd, sprint, explicitActor) {
259
642
  const { resolveStore } = await import('../store.js');
260
- const player = process.env.USER || 'unknown';
643
+ const actor = resolveActor(cwd, { explicitActor });
644
+ const player = actor.name;
645
+ const playerDisplay = formatActorName(actor);
261
646
  const target = `sprint:${formatSprintLabel(sprint)}`;
262
647
  try {
263
648
  const store = await resolveStore(cwd);
264
649
  try {
265
650
  const existing = await store.list(sprint);
266
651
  if (existing.some(c => c.player === player && c.target === target)) {
267
- return `Claim: ${target} already held by ${player}.`;
652
+ return `Claim: ${target} already held by ${playerDisplay} (actor source: ${formatActorSource(actor)}).`;
268
653
  }
269
654
  const claim = await store.claim({
270
655
  sprint_number: sprint,
@@ -273,7 +658,7 @@ async function autoClaimSprint(cwd, sprint) {
273
658
  scope: 'area',
274
659
  notes: 'auto-claimed by slope sprint start',
275
660
  });
276
- return `Claim: ${claim.target} (${claim.scope}) auto-claimed for ${player}.`;
661
+ return `Claim: ${claim.target} (${claim.scope}) auto-claimed for ${playerDisplay} (actor source: ${formatActorSource(actor)}).`;
277
662
  }
278
663
  finally {
279
664
  store.close();
@@ -306,6 +691,22 @@ function gateCommand(args, cwd) {
306
691
  const gateName = args[0];
307
692
  if (!gateName || !VALID_GATES.includes(gateName)) {
308
693
  console.error(`Error: gate name required. Valid gates: ${VALID_GATES.join(', ')}`);
694
+ printGateUsage();
695
+ process.exit(1);
696
+ }
697
+ const options = parseGateOptions(args.slice(1));
698
+ if (options.help) {
699
+ printGateUsage(gateName);
700
+ return;
701
+ }
702
+ if (options.errors.length > 0) {
703
+ for (const error of options.errors)
704
+ console.error(`Error: ${error}`);
705
+ printGateUsage(gateName);
706
+ process.exit(1);
707
+ }
708
+ if (!isReviewGateName(gateName) && options.review) {
709
+ console.error('Error: review evidence options only apply to code_review and architect_review gates.');
309
710
  process.exit(1);
310
711
  }
311
712
  const state = loadSprintState(cwd);
@@ -313,18 +714,78 @@ function gateCommand(args, cwd) {
313
714
  console.error("No active sprint. Run 'slope sprint start --number=N' first.");
314
715
  process.exit(1);
315
716
  }
316
- if (state.gates[gateName]) {
717
+ if (isReviewGateName(gateName)) {
718
+ const required = isRequiredReviewGate(state, gateName);
719
+ const provenance = options.review?.provenance;
720
+ if (required && (provenance === 'self_review' || provenance === 'manual_override')) {
721
+ console.error(`Error: ${gateName} is a required independent review; --self-review/--override cannot silently satisfy it.`);
722
+ console.error(`Choose independent evidence, PR review evidence, or --waive-independent-review --reason="...".`);
723
+ printGateUsage(gateName);
724
+ process.exit(1);
725
+ }
726
+ if (!required && provenance === 'independent_review_waived') {
727
+ console.error(`Error: ${gateName} is not recorded as required; use --self-review --reason="..." for a weaker optional/recommended review.`);
728
+ printGateUsage(gateName);
729
+ process.exit(1);
730
+ }
731
+ const validation = validateReviewGateCompletion(options.review);
732
+ if (validation) {
733
+ console.error(`Error: ${validation}.`);
734
+ printGateUsage(gateName);
735
+ process.exit(1);
736
+ }
737
+ }
738
+ if (state.gates[gateName] && (!isReviewGateName(gateName) || !options.review)) {
317
739
  console.log(`Gate '${gateName}' is already complete.`);
318
740
  return;
319
741
  }
320
- updateGate(cwd, gateName, true);
742
+ const changed = updateGate(cwd, gateName, true, { review: options.review });
743
+ if (!changed) {
744
+ console.error(`Error: could not update gate '${gateName}'.`);
745
+ process.exit(1);
746
+ }
321
747
  const updated = loadSprintState(cwd);
322
748
  const remaining = pendingGates(updated);
749
+ const provenance = isReviewGateName(gateName) ? ` (${updated.review_gates[gateName].provenance})` : '';
323
750
  if (remaining.length === 0) {
324
- console.log(`Gate '${gateName}' marked complete. All gates done ready for PR!`);
751
+ console.log(`Gate '${gateName}' marked complete${provenance}. All gates done - ready for PR!`);
325
752
  }
326
753
  else {
327
- console.log(`Gate '${gateName}' marked complete. Remaining: ${remaining.join(', ')}`);
754
+ console.log(`Gate '${gateName}' marked complete${provenance}. Remaining: ${remaining.join(', ')}`);
755
+ }
756
+ }
757
+ function formatReviewEvidence(review) {
758
+ return review.evidence.length > 0 ? review.evidence.join(', ') : '(missing)';
759
+ }
760
+ function formatReviewGateStatus(state, gate) {
761
+ const review = state.review_gates[gate];
762
+ const requirement = state.review_requirements?.[gate];
763
+ const requiredLabel = requirement?.priority === 'required' ? 'required independent review' : null;
764
+ if (!state.gates[gate] || review.provenance === 'pending') {
765
+ return requiredLabel
766
+ ? `${requiredLabel} pending; choose reviewer evidence, PR review evidence, or an explicit waiver`
767
+ : 'pending review evidence';
768
+ }
769
+ const validation = validateReviewGateCompletion({
770
+ provenance: review.provenance,
771
+ evidence: review.evidence,
772
+ reviewer: review.reviewer,
773
+ notes: review.notes,
774
+ });
775
+ if (validation) {
776
+ return `invalid ${review.provenance}: ${validation}`;
777
+ }
778
+ switch (review.provenance) {
779
+ case 'independent_review':
780
+ return `independent_review; reviewer=${review.reviewer}; evidence=${formatReviewEvidence(review)}`;
781
+ case 'pr_review':
782
+ return `pr_review; evidence=${formatReviewEvidence(review)}`;
783
+ case 'self_review':
784
+ return `self_review (weaker); reason=${review.notes}`;
785
+ case 'independent_review_waived':
786
+ return `required independent review WAIVED; reason=${review.notes}`;
787
+ case 'manual_override':
788
+ return `manual_override (weaker); reason=${review.notes}`;
328
789
  }
329
790
  }
330
791
  function statusCommand(cwd) {
@@ -334,19 +795,49 @@ function statusCommand(cwd) {
334
795
  return;
335
796
  }
336
797
  const complete = isSprintComplete(state);
337
- console.log(`Sprint ${formatSprintNumber(state.sprint)} phase: ${state.phase}${complete ? ' (all gates complete)' : ''}`);
798
+ const waivedReviews = waivedReviewGateNames(state);
799
+ const derivedStatus = complete
800
+ ? waivedReviews.length > 0 ? 'ready_for_pr_with_review_waiver' : 'ready_for_pr'
801
+ : state.phase;
802
+ const phaseContext = complete ? ` (phase: ${state.phase}, all gates complete)` : ` (phase: ${state.phase})`;
803
+ console.log(`Sprint ${formatSprintNumber(state.sprint)} - status: ${derivedStatus}${phaseContext}`);
338
804
  console.log(`Started: ${state.started_at}`);
339
805
  console.log(`Updated: ${state.updated_at}`);
340
806
  console.log('');
341
807
  console.log('Gates:');
342
808
  for (const [gate, done] of Object.entries(state.gates)) {
343
- const marker = done ? '[x]' : '[ ]';
344
- console.log(` ${marker} ${gate}`);
809
+ if (isReviewGateName(gate)) {
810
+ const satisfied = isReviewGateSatisfied(state, gate);
811
+ const marker = satisfied
812
+ ? state.review_gates[gate].provenance === 'independent_review_waived' ? '[~]' : '[x]'
813
+ : done ? '[!]' : '[ ]';
814
+ console.log(` ${marker} ${gate} (${formatReviewGateStatus(state, gate)})`);
815
+ }
816
+ else {
817
+ const marker = done ? '[x]' : '[ ]';
818
+ console.log(` ${marker} ${gate}`);
819
+ }
345
820
  }
346
821
  if (!complete) {
347
- const pending = pendingGates(state);
822
+ const pendingNames = pendingGateNames(state);
823
+ const pendingLabels = pendingGates(state);
824
+ const pending = pendingNames.map((gate, index) => {
825
+ if (isReviewGateName(gate) && state.gates[gate]) {
826
+ return `${gate} (review evidence incomplete)`;
827
+ }
828
+ return pendingLabels[index] ?? gate;
829
+ });
348
830
  console.log(`\nRemaining: ${pending.join(', ')}`);
349
831
  }
832
+ else {
833
+ if (waivedReviews.length > 0) {
834
+ console.log(`\nNext: ${waivedReviews.join(', ')} has an explicit independent-review waiver.`);
835
+ console.log('Attach independent reviewer/PR evidence to replace the waiver, or proceed only with the recorded downgrade visible in PR guidance.');
836
+ }
837
+ else {
838
+ console.log('\nNext: create PR for this branch; after merge, run post-merge retro.');
839
+ }
840
+ }
350
841
  }
351
842
  function resetCommand(cwd) {
352
843
  clearSprintState(cwd);
@@ -421,6 +912,11 @@ function applyWorkflowVariableDefaults(def, vars, cwd, sprintId) {
421
912
  if (tickets.length > 0) {
422
913
  vars.tickets = tickets.join(',');
423
914
  }
915
+ else {
916
+ const sprintHint = sprintId ? ` for ${sprintId}` : '';
917
+ throw new Error(`Required variable "tickets" not provided and no roadmap tickets were found${sprintHint}. ` +
918
+ 'Provide a comma-separated/JSON ticket list or a count, for example `--var tickets=T1,T2` or `--var tickets=3`.');
919
+ }
424
920
  }
425
921
  }
426
922
  function positionalSprintArg(args) {
@@ -476,7 +972,7 @@ function syncSprintStateWithWorkflow(cwd, sprintId, workflowPhase) {
476
972
  return;
477
973
  const existing = loadSprintState(cwd);
478
974
  if (!existing) {
479
- saveSprintState(cwd, createSprintState(sprint, nextPhase));
975
+ initializeSprintState(cwd, createSprintState(sprint, nextPhase));
480
976
  return;
481
977
  }
482
978
  if (existing.sprint !== sprint || existing.phase === 'complete')
@@ -614,10 +1110,10 @@ async function workflowCleanupCommand(args, cwd) {
614
1110
  try {
615
1111
  const result = await reconcileWorkflowExecutions(cwd, store);
616
1112
  for (const { exec, reason } of result.paused) {
617
- console.log(`Paused ${exec.sprint_id ?? exec.id} (${exec.workflow_name}) at ${exec.current_phase}/${exec.current_step} — ${reason}`);
1113
+ console.log(`Paused ${sprintLabelForExecution(exec)} (${exec.workflow_name}) at ${exec.current_phase}/${exec.current_step} — ${reason}`);
618
1114
  }
619
1115
  for (const item of result.fastForwarded) {
620
- console.log(`Fast-forwarded ${item.exec.sprint_id ?? item.exec.id} (${item.exec.workflow_name}) to ${item.phase}/${item.step} — ${item.reason}`);
1116
+ console.log(`Fast-forwarded ${sprintLabelForExecution(item.exec)} (${item.exec.workflow_name}) to ${item.phase}/${item.step} — ${item.reason}`);
621
1117
  }
622
1118
  if (result.paused.length === 0 && result.fastForwarded.length === 0) {
623
1119
  console.log('Workflow state already matches git/roadmap reality.');
@@ -644,7 +1140,7 @@ async function workflowCleanupCommand(args, cwd) {
644
1140
  }
645
1141
  const engine = new WorkflowEngine();
646
1142
  for (const { exec, reason } of stale) {
647
- const label = exec.sprint_id ?? exec.id;
1143
+ const label = sprintLabelForExecution(exec);
648
1144
  if (dryRun) {
649
1145
  console.log(`[dry-run] Would pause ${label} (${exec.workflow_name}) at ${exec.current_phase}/${exec.current_step} — ${reason}`);
650
1146
  }
@@ -713,6 +1209,12 @@ function parsePortableResumeFlags(args) {
713
1209
  async function portableResumeCommand(args, cwd) {
714
1210
  const flags = parsePortableResumeFlags(args);
715
1211
  const config = loadConfig(cwd);
1212
+ const loadedState = loadSprintStateResult(cwd);
1213
+ if (loadedState.status === 'corrupt') {
1214
+ console.error(`Portable resume refused: corrupt sprint evidence was preserved at ${relative(cwd, loadedState.path)}.`);
1215
+ process.exit(1);
1216
+ return;
1217
+ }
716
1218
  if (flags.invalidSprint) {
717
1219
  console.error(`Error: invalid sprint "${flags.invalidSprint}". Use --sprint=N, e.g. --sprint=177.`);
718
1220
  process.exit(1);
@@ -724,7 +1226,7 @@ async function portableResumeCommand(args, cwd) {
724
1226
  return;
725
1227
  }
726
1228
  if (flags.writePointer) {
727
- const current = loadSprintState(cwd);
1229
+ const current = loadedState.status === 'valid' ? loadedState.state : null;
728
1230
  const sprint = flags.sprint ?? current?.sprint;
729
1231
  if (!sprint) {
730
1232
  console.error('Usage: slope sprint resume --write-pointer --sprint=N [--phase=<phase>] [--output=path]');
@@ -754,21 +1256,34 @@ async function portableResumeCommand(args, cwd) {
754
1256
  process.exit(1);
755
1257
  return;
756
1258
  }
757
- const existing = loadSprintState(cwd);
758
- if (existing && existing.sprint !== plan.sprint && !flags.force) {
759
- console.error(`\nPortable resume refused. Local sprint-state is ${formatSprintLabel(existing.sprint)}, but resume target is ${formatSprintLabel(plan.sprint)}.`);
760
- console.error('Run `slope sprint reset` or rerun with --force if replacing local state is intentional.');
761
- process.exit(1);
762
- return;
1259
+ const existing = loadedState.status === 'valid' ? loadedState.state : null;
1260
+ if (existing) {
1261
+ const retryResume = [
1262
+ 'slope sprint resume --portable',
1263
+ ...(flags.sprint ? [`--sprint=${formatSprintNumber(flags.sprint)}`] : []),
1264
+ ...(flags.phase ? [`--phase=${flags.phase}`] : []),
1265
+ ...(flags.from ? [`--from=${commandValue(flags.from)}`] : []),
1266
+ ...(flags.force ? ['--force'] : []),
1267
+ ].join(' ');
1268
+ requireMatchingSprintOrRollover(cwd, existing, plan.sprint, `resume ${formatSprintLabel(plan.sprint)}`, retryResume);
763
1269
  }
764
1270
  if (flags.dryRun) {
765
1271
  console.log('\nDry run: local sprint-state and claims were not changed.');
766
1272
  return;
767
1273
  }
768
- saveSprintState(cwd, createSprintState(plan.sprint, plan.phase));
1274
+ if (!existing) {
1275
+ const initialized = initializeSprintState(cwd, createSprintState(plan.sprint, plan.phase));
1276
+ if (initialized.status !== 'created') {
1277
+ console.error('Portable resume refused because sprint state changed concurrently; retry the command.');
1278
+ process.exit(1);
1279
+ return;
1280
+ }
1281
+ }
769
1282
  const restoredClaims = await restoreResumeClaims(cwd, plan);
770
- console.log(`\nPortable sprint resume complete: ${formatSprintLabel(plan.sprint)} (${plan.phase}).`);
771
- console.log(` Fresh local sprint-state written to .slope/sprint-state.json`);
1283
+ console.log(`\nPortable sprint resume complete: ${formatSprintLabel(plan.sprint)} (${existing?.phase ?? plan.phase}).`);
1284
+ console.log(existing
1285
+ ? ' Existing local sprint-state and rollover lineage preserved'
1286
+ : ' Fresh local sprint-state written to .slope/sprint-state.json');
772
1287
  console.log(` Resume claims restored: ${restoredClaims}`);
773
1288
  console.log(' Local DB/locks/metrics were not imported from another machine.');
774
1289
  }
@@ -823,7 +1338,8 @@ async function restoreResumeClaims(cwd, plan) {
823
1338
  return 0;
824
1339
  const { resolveStore } = await import('../store.js');
825
1340
  const store = await resolveStore(cwd);
826
- const player = process.env.USER || 'unknown';
1341
+ const actor = resolveActor(cwd);
1342
+ const player = actor.name;
827
1343
  let restored = 0;
828
1344
  try {
829
1345
  const existing = await store.list(plan.sprint);
@@ -1106,6 +1622,9 @@ export async function sprintCommand(args) {
1106
1622
  case 'begin':
1107
1623
  await beginCommand(args.slice(1), cwd);
1108
1624
  break;
1625
+ case 'rollover':
1626
+ rolloverCommand(args.slice(1), cwd);
1627
+ break;
1109
1628
  case 'plan': {
1110
1629
  const { sprintPlanCommand } = await import('./sprint-plan.js');
1111
1630
  await sprintPlanCommand(args.slice(1));
@@ -1165,12 +1684,18 @@ function printSprintUsage() {
1165
1684
  slope sprint — Sprint lifecycle management
1166
1685
 
1167
1686
  Legacy commands:
1168
- slope sprint start --number=N [--phase=<phase>] [--touches=<paths>] [--force]
1687
+ slope sprint start --number=N [--phase=<phase>] [--touches=<paths>] [--actor=<name>] [--force]
1169
1688
  Start sprint state tracking with pre-sprint reality checks
1170
- slope sprint begin --sprint=N --ticket=T Bundled start + claim + briefing + prep (#311)
1689
+ slope sprint begin --sprint=N --ticket=T [--actor=<name>] Bundled start + claim + briefing + prep (#311)
1690
+ slope sprint rollover --from=N --to=N [--force --reason="<why>"]
1691
+ Archive prior state and create the next planning state
1171
1692
  slope sprint plan --sprint=N [--output=path] Generate markdown sprint plan (#312)
1172
1693
  slope sprint phase <phase> Update current sprint phase
1173
- slope sprint gate <name> Mark a gate as complete
1694
+ slope sprint gate <name> [review evidence options]
1695
+ Mark a gate as complete
1696
+ Review gates require independent evidence, PR review evidence,
1697
+ required reviews need independent evidence or an explicit
1698
+ --waive-independent-review downgrade; optional reviews may use self_review
1174
1699
  slope sprint status Show sprint state and gates
1175
1700
  slope sprint resume --portable [--from=path] [--force] [--dry-run]
1176
1701
  Reconstruct local sprint state from tracked artifacts
@@ -1180,6 +1705,7 @@ Legacy commands:
1180
1705
 
1181
1706
  Workflow commands:
1182
1707
  slope sprint run <id> --workflow=<name> [--var k=v ...] Start workflow execution
1708
+ tickets accepts a list/JSON array or count (tickets=3)
1183
1709
  slope sprint status [sprint_id] Show workflow execution progress
1184
1710
  slope sprint resume <sprint_id> Resume a paused workflow execution
1185
1711
  slope sprint pause <sprint_id> Pause a running workflow execution