@slope-dev/slope 1.60.0 → 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 (115) hide show
  1. package/README.md +6 -0
  2. package/dist/cli/commands/agent.js +2 -2
  3. package/dist/cli/commands/agent.js.map +1 -1
  4. package/dist/cli/commands/commit-ready.js +1 -1
  5. package/dist/cli/commands/commit-ready.js.map +1 -1
  6. package/dist/cli/commands/next.d.ts +1 -1
  7. package/dist/cli/commands/next.d.ts.map +1 -1
  8. package/dist/cli/commands/next.js +31 -2
  9. package/dist/cli/commands/next.js.map +1 -1
  10. package/dist/cli/commands/now.d.ts +3 -0
  11. package/dist/cli/commands/now.d.ts.map +1 -1
  12. package/dist/cli/commands/now.js +14 -2
  13. package/dist/cli/commands/now.js.map +1 -1
  14. package/dist/cli/commands/review-state.d.ts.map +1 -1
  15. package/dist/cli/commands/review-state.js +45 -4
  16. package/dist/cli/commands/review-state.js.map +1 -1
  17. package/dist/cli/commands/review.d.ts +9 -1
  18. package/dist/cli/commands/review.d.ts.map +1 -1
  19. package/dist/cli/commands/review.js +67 -39
  20. package/dist/cli/commands/review.js.map +1 -1
  21. package/dist/cli/commands/roadmap.d.ts.map +1 -1
  22. package/dist/cli/commands/roadmap.js +274 -5
  23. package/dist/cli/commands/roadmap.js.map +1 -1
  24. package/dist/cli/commands/sprint.d.ts.map +1 -1
  25. package/dist/cli/commands/sprint.js +353 -54
  26. package/dist/cli/commands/sprint.js.map +1 -1
  27. package/dist/cli/guards/claim-required.d.ts.map +1 -1
  28. package/dist/cli/guards/claim-required.js +19 -4
  29. package/dist/cli/guards/claim-required.js.map +1 -1
  30. package/dist/cli/guards/next-action.js +1 -1
  31. package/dist/cli/guards/next-action.js.map +1 -1
  32. package/dist/cli/guards/phase-boundary.d.ts.map +1 -1
  33. package/dist/cli/guards/phase-boundary.js +8 -2
  34. package/dist/cli/guards/phase-boundary.js.map +1 -1
  35. package/dist/cli/guards/post-push.js +2 -2
  36. package/dist/cli/guards/post-push.js.map +1 -1
  37. package/dist/cli/guards/review-tier.d.ts.map +1 -1
  38. package/dist/cli/guards/review-tier.js +2 -3
  39. package/dist/cli/guards/review-tier.js.map +1 -1
  40. package/dist/cli/guards/roadmap-edit-shipped.d.ts.map +1 -1
  41. package/dist/cli/guards/roadmap-edit-shipped.js +143 -7
  42. package/dist/cli/guards/roadmap-edit-shipped.js.map +1 -1
  43. package/dist/cli/guards/scope-drift.d.ts.map +1 -1
  44. package/dist/cli/guards/scope-drift.js +8 -2
  45. package/dist/cli/guards/scope-drift.js.map +1 -1
  46. package/dist/cli/guards/session-briefing.js +6 -2
  47. package/dist/cli/guards/session-briefing.js.map +1 -1
  48. package/dist/cli/guards/sprint-completion.d.ts.map +1 -1
  49. package/dist/cli/guards/sprint-completion.js +94 -18
  50. package/dist/cli/guards/sprint-completion.js.map +1 -1
  51. package/dist/cli/index.js +4 -2
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/registry.d.ts.map +1 -1
  54. package/dist/cli/registry.js +25 -0
  55. package/dist/cli/registry.js.map +1 -1
  56. package/dist/cli/roadmap-source-store.d.ts +37 -0
  57. package/dist/cli/roadmap-source-store.d.ts.map +1 -0
  58. package/dist/cli/roadmap-source-store.js +298 -0
  59. package/dist/cli/roadmap-source-store.js.map +1 -0
  60. package/dist/cli/sprint-inference.js +1 -1
  61. package/dist/cli/sprint-inference.js.map +1 -1
  62. package/dist/cli/sprint-rollover.d.ts +102 -0
  63. package/dist/cli/sprint-rollover.d.ts.map +1 -0
  64. package/dist/cli/sprint-rollover.js +558 -0
  65. package/dist/cli/sprint-rollover.js.map +1 -0
  66. package/dist/cli/sprint-state.d.ts +72 -1
  67. package/dist/cli/sprint-state.d.ts.map +1 -1
  68. package/dist/cli/sprint-state.js +203 -5
  69. package/dist/cli/sprint-state.js.map +1 -1
  70. package/dist/cli/template-generator.d.ts.map +1 -1
  71. package/dist/cli/template-generator.js +45 -32
  72. package/dist/cli/template-generator.js.map +1 -1
  73. package/dist/cli/workflow-resync.d.ts.map +1 -1
  74. package/dist/cli/workflow-resync.js +9 -8
  75. package/dist/cli/workflow-resync.js.map +1 -1
  76. package/dist/core/briefing.d.ts.map +1 -1
  77. package/dist/core/briefing.js +254 -14
  78. package/dist/core/briefing.js.map +1 -1
  79. package/dist/core/builder.d.ts +16 -2
  80. package/dist/core/builder.d.ts.map +1 -1
  81. package/dist/core/builder.js +82 -5
  82. package/dist/core/builder.js.map +1 -1
  83. package/dist/core/index.d.ts +7 -3
  84. package/dist/core/index.d.ts.map +1 -1
  85. package/dist/core/index.js +3 -1
  86. package/dist/core/index.js.map +1 -1
  87. package/dist/core/roadmap-focus.d.ts +101 -0
  88. package/dist/core/roadmap-focus.d.ts.map +1 -0
  89. package/dist/core/roadmap-focus.js +293 -0
  90. package/dist/core/roadmap-focus.js.map +1 -0
  91. package/dist/core/roadmap-sources.d.ts +52 -0
  92. package/dist/core/roadmap-sources.d.ts.map +1 -0
  93. package/dist/core/roadmap-sources.js +483 -0
  94. package/dist/core/roadmap-sources.js.map +1 -0
  95. package/dist/core/roadmap.d.ts +25 -1
  96. package/dist/core/roadmap.d.ts.map +1 -1
  97. package/dist/core/roadmap.js +62 -45
  98. package/dist/core/roadmap.js.map +1 -1
  99. package/dist/core/workflow-engine.d.ts.map +1 -1
  100. package/dist/core/workflow-engine.js +17 -2
  101. package/dist/core/workflow-engine.js.map +1 -1
  102. package/dist/mcp/registry.d.ts +1 -1
  103. package/dist/mcp/registry.d.ts.map +1 -1
  104. package/dist/mcp/registry.js +5 -4
  105. package/dist/mcp/registry.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/core/workflows/sprint-standard.yaml +1 -1
  108. package/templates/claude-code/CLAUDE.md +3 -1
  109. package/templates/claude-code/commands/start-sprint.md +9 -5
  110. package/templates/claude-code/rules/sprint-checklist.md +13 -10
  111. package/templates/codex/plugins/slope/.codex-plugin/plugin.json +1 -1
  112. package/templates/codex/plugins/slope/skills/slope-sprint/SKILL.md +5 -4
  113. package/templates/cursor/rules/slope-sprint-checklist.mdc +11 -8
  114. package/templates/generic/SLOPE-CHECKLIST.md +10 -7
  115. package/templates/pi/skills/start-sprint.md +9 -5
@@ -1,7 +1,8 @@
1
- import { loadSprintState, saveSprintState, createSprintState, mutateSprintState, updateGate, updateSprintPhase, clearSprintState, isSprintComplete, isReviewGateSatisfied, pendingGateNames, pendingGates, isSprintPhase, isReviewGateName, validateReviewGateCompletion, 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
4
  import { formatActorName, formatActorSource, formatConflictSummary, resolveActor } from '../actor.js';
5
+ import { inspectSprintRollover, performSprintRollover, SprintRolloverError, verifySprintRolloverLineage, } from '../sprint-rollover.js';
5
6
  /** Get workflow definition from execution snapshot (preferred) or disk (fallback for old executions) */
6
7
  function getDefinition(exec, cwd) {
7
8
  // Prefer snapshot from execution
@@ -21,7 +22,7 @@ function getDefinition(exec, cwd) {
21
22
  return { def: loadWorkflow(exec.workflow_name, cwd), drifted: false };
22
23
  }
23
24
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
24
- import { join, dirname, basename, isAbsolute } from 'node:path';
25
+ import { join, dirname, basename, isAbsolute, relative } from 'node:path';
25
26
  import { createStore } from '../../store/index.js';
26
27
  import { formatCliError } from '../error-reporter.js';
27
28
  import { buildSprintResumePointer, planPortableSprintResume, writeSprintResumePointer, } from '../sprint-resume.js';
@@ -98,6 +99,7 @@ function parseGateOptions(args) {
98
99
  let reviewer;
99
100
  let prReview;
100
101
  let selfReview = false;
102
+ let waiveIndependentReview = false;
101
103
  let notes;
102
104
  let reason;
103
105
  let overrideReason;
@@ -139,6 +141,10 @@ function parseGateOptions(args) {
139
141
  selfReview = true;
140
142
  continue;
141
143
  }
144
+ if (arg === '--waive-independent-review') {
145
+ waiveIndependentReview = true;
146
+ continue;
147
+ }
142
148
  const reasonValue = optionValue(args, i, '--reason');
143
149
  if (reasonValue) {
144
150
  if (!reasonValue.value || reasonValue.value.startsWith('--'))
@@ -170,11 +176,12 @@ function parseGateOptions(args) {
170
176
  }
171
177
  const hasPrReview = Boolean(prReview);
172
178
  const hasSelfReview = selfReview;
179
+ const hasWaiver = waiveIndependentReview;
173
180
  const hasOverride = Boolean(overrideReason);
174
- const hasIndependent = !hasPrReview && !hasSelfReview && !hasOverride && (Boolean(reviewer) || evidence.length > 0);
175
- const modeCount = [hasPrReview, hasSelfReview, hasOverride, hasIndependent].filter(Boolean).length;
181
+ const hasIndependent = !hasPrReview && !hasSelfReview && !hasWaiver && !hasOverride && (Boolean(reviewer) || evidence.length > 0);
182
+ const modeCount = [hasPrReview, hasSelfReview, hasWaiver, hasOverride, hasIndependent].filter(Boolean).length;
176
183
  if (modeCount > 1) {
177
- errors.push('Use only one review provenance mode: --reviewer/--evidence, --pr-review, --self-review, or --override.');
184
+ errors.push('Use only one review provenance mode: --reviewer/--evidence, --pr-review, --self-review, --waive-independent-review, or --override.');
178
185
  }
179
186
  let review;
180
187
  if (modeCount === 1) {
@@ -184,6 +191,9 @@ function parseGateOptions(args) {
184
191
  else if (hasSelfReview) {
185
192
  review = { provenance: 'self_review', evidence, reviewer, notes: reason ?? notes };
186
193
  }
194
+ else if (hasWaiver) {
195
+ review = { provenance: 'independent_review_waived', evidence, reviewer, notes: reason ?? notes };
196
+ }
187
197
  else if (hasOverride) {
188
198
  review = { provenance: 'manual_override', evidence, reviewer, notes: overrideReason };
189
199
  }
@@ -201,15 +211,216 @@ function printGateUsage(gateName) {
201
211
  console.error(` slope sprint gate ${gate} --reviewer=<agent-or-person> --evidence=<transcript-or-output>`);
202
212
  console.error(` slope sprint gate ${gate} --pr-review=<url-or-id>`);
203
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"`);
204
215
  console.error(` slope sprint gate ${gate} --override="manual override reason"`);
205
216
  console.error('');
206
217
  console.error('Review provenance modes:');
207
218
  console.error(' independent_review: requires --reviewer and --evidence');
208
219
  console.error(' pr_review: records external PR review evidence from --pr-review');
209
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');
210
222
  console.error(' manual_override (weaker): requires --override');
211
223
  console.error('');
212
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
+ }
213
424
  /**
214
425
  * `slope sprint begin --sprint=N --ticket=T` — bundled start-of-work flow.
215
426
  *
@@ -222,32 +433,34 @@ function printGateUsage(gateName) {
222
433
  * 6. Print recommended next implementation step
223
434
  */
224
435
  async function beginCommand(args, cwd) {
225
- const sprintArg = args.find(a => a.startsWith('--sprint='));
226
- const ticketArg = args.find(a => a.startsWith('--ticket='));
227
- if (!sprintArg || !ticketArg) {
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}`);
228
442
  console.error('\nUsage: slope sprint begin --sprint=N --ticket=KEY [--actor=<name>]');
229
443
  console.error('Bundles: sprint start + claim + briefing + prep --lite\n');
230
444
  process.exit(1);
231
445
  }
232
- const sprint = parseSprintNumber(sprintArg.split('=')[1]);
233
- const ticket = ticketArg.split('=')[1];
446
+ const sprint = parseSprintNumber(sprintValue);
447
+ const ticket = ticketValue;
234
448
  if (!sprint || !ticket) {
235
449
  console.error('Error: --sprint must be a positive sprint id; --ticket must be a non-empty key');
236
450
  process.exit(1);
237
451
  }
238
- // Step 1: sprint state
239
- let state = loadSprintState(cwd);
240
- if (state && state.sprint === sprint) {
241
- console.log(`Sprint ${formatSprintNumber(sprint)}: already started (phase: ${state.phase}).`);
242
- }
243
- else if (state && state.sprint !== sprint) {
244
- console.error(`Refusing to begin S${formatSprintNumber(sprint)} — sprint-state.json is for S${formatSprintNumber(state.sprint)}.`);
245
- 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)}.`);
246
456
  process.exit(1);
247
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
+ }
248
463
  else {
249
- state = createSprintState(sprint, 'planning');
250
- saveSprintState(cwd, state);
251
464
  console.log(`Sprint ${formatSprintNumber(sprint)}: started (phase: planning).`);
252
465
  }
253
466
  // Step 2: claim
@@ -340,30 +553,47 @@ async function beginCommand(args, cwd) {
340
553
  console.log('');
341
554
  }
342
555
  async function startCommand(args, cwd) {
343
- const numberArg = args.find(a => a.startsWith('--number='));
344
- 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}`);
345
561
  console.error('Error: --number=N is required. Usage: slope sprint start --number=22');
346
562
  process.exit(1);
347
563
  }
348
- const sprint = parseSprintNumber(numberArg.slice('--number='.length));
564
+ const sprint = parseSprintNumber(numberValue);
349
565
  if (!sprint) {
350
566
  console.error('Error: --number must be a positive sprint id, e.g. 114 or 114.5.');
351
567
  process.exit(1);
352
568
  }
353
- const phaseArg = args.find(a => a.startsWith('--phase='));
354
- const phaseInput = phaseArg?.slice('--phase='.length) ?? 'planning';
569
+ const phaseValue = findOptionValue(args, '--phase');
570
+ const phaseInput = phaseValue ?? 'planning';
355
571
  if (!isSprintPhase(phaseInput)) {
356
572
  console.error(`Error: invalid phase "${phaseInput}". Valid phases: ${SPRINT_PHASES.join(', ')}`);
357
573
  process.exit(1);
358
574
  }
359
575
  const phase = phaseInput;
360
576
  const force = args.includes('--force');
361
- const touchesArg = args.find(a => a.startsWith('--touches='));
362
- const touchedPaths = parseTouchedPaths(touchesArg?.slice('--touches='.length));
363
- const existing = loadSprintState(cwd);
364
- if (existing && existing.sprint === sprint) {
365
- if (phaseArg && existing.phase !== phase) {
366
- 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
+ }
367
597
  console.log(`Sprint ${formatSprintNumber(sprint)} phase updated: ${phase}.`);
368
598
  return;
369
599
  }
@@ -393,8 +623,16 @@ async function startCommand(args, cwd) {
393
623
  console.error('Resolve the overlap, choose a non-overlapping sprint, or rerun with --force if this is intentional.');
394
624
  process.exit(1);
395
625
  }
396
- const state = createSprintState(sprint, phase);
397
- saveSprintState(cwd, state);
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
+ }
398
636
  const autoClaim = await autoClaimSprint(cwd, sprint, actorOverride(args));
399
637
  console.log(`Sprint ${formatSprintNumber(sprint)} started (phase: ${phase}). Use 'slope sprint gate <name>' to mark gates; review gates require evidence options.`);
400
638
  if (autoClaim)
@@ -471,7 +709,25 @@ function gateCommand(args, cwd) {
471
709
  console.error('Error: review evidence options only apply to code_review and architect_review gates.');
472
710
  process.exit(1);
473
711
  }
712
+ const state = loadSprintState(cwd);
713
+ if (!state) {
714
+ console.error("No active sprint. Run 'slope sprint start --number=N' first.");
715
+ process.exit(1);
716
+ }
474
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
+ }
475
731
  const validation = validateReviewGateCompletion(options.review);
476
732
  if (validation) {
477
733
  console.error(`Error: ${validation}.`);
@@ -479,11 +735,6 @@ function gateCommand(args, cwd) {
479
735
  process.exit(1);
480
736
  }
481
737
  }
482
- const state = loadSprintState(cwd);
483
- if (!state) {
484
- console.error("No active sprint. Run 'slope sprint start --number=N' first.");
485
- process.exit(1);
486
- }
487
738
  if (state.gates[gateName] && (!isReviewGateName(gateName) || !options.review)) {
488
739
  console.log(`Gate '${gateName}' is already complete.`);
489
740
  return;
@@ -508,8 +759,12 @@ function formatReviewEvidence(review) {
508
759
  }
509
760
  function formatReviewGateStatus(state, gate) {
510
761
  const review = state.review_gates[gate];
762
+ const requirement = state.review_requirements?.[gate];
763
+ const requiredLabel = requirement?.priority === 'required' ? 'required independent review' : null;
511
764
  if (!state.gates[gate] || review.provenance === 'pending') {
512
- return 'pending review evidence';
765
+ return requiredLabel
766
+ ? `${requiredLabel} pending; choose reviewer evidence, PR review evidence, or an explicit waiver`
767
+ : 'pending review evidence';
513
768
  }
514
769
  const validation = validateReviewGateCompletion({
515
770
  provenance: review.provenance,
@@ -527,6 +782,8 @@ function formatReviewGateStatus(state, gate) {
527
782
  return `pr_review; evidence=${formatReviewEvidence(review)}`;
528
783
  case 'self_review':
529
784
  return `self_review (weaker); reason=${review.notes}`;
785
+ case 'independent_review_waived':
786
+ return `required independent review WAIVED; reason=${review.notes}`;
530
787
  case 'manual_override':
531
788
  return `manual_override (weaker); reason=${review.notes}`;
532
789
  }
@@ -538,7 +795,10 @@ function statusCommand(cwd) {
538
795
  return;
539
796
  }
540
797
  const complete = isSprintComplete(state);
541
- const derivedStatus = complete ? 'ready_for_pr' : state.phase;
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;
542
802
  const phaseContext = complete ? ` (phase: ${state.phase}, all gates complete)` : ` (phase: ${state.phase})`;
543
803
  console.log(`Sprint ${formatSprintNumber(state.sprint)} - status: ${derivedStatus}${phaseContext}`);
544
804
  console.log(`Started: ${state.started_at}`);
@@ -548,7 +808,9 @@ function statusCommand(cwd) {
548
808
  for (const [gate, done] of Object.entries(state.gates)) {
549
809
  if (isReviewGateName(gate)) {
550
810
  const satisfied = isReviewGateSatisfied(state, gate);
551
- const marker = satisfied ? '[x]' : done ? '[!]' : '[ ]';
811
+ const marker = satisfied
812
+ ? state.review_gates[gate].provenance === 'independent_review_waived' ? '[~]' : '[x]'
813
+ : done ? '[!]' : '[ ]';
552
814
  console.log(` ${marker} ${gate} (${formatReviewGateStatus(state, gate)})`);
553
815
  }
554
816
  else {
@@ -568,7 +830,13 @@ function statusCommand(cwd) {
568
830
  console.log(`\nRemaining: ${pending.join(', ')}`);
569
831
  }
570
832
  else {
571
- console.log('\nNext: create PR for this branch; after merge, run post-merge retro.');
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
+ }
572
840
  }
573
841
  }
574
842
  function resetCommand(cwd) {
@@ -644,6 +912,11 @@ function applyWorkflowVariableDefaults(def, vars, cwd, sprintId) {
644
912
  if (tickets.length > 0) {
645
913
  vars.tickets = tickets.join(',');
646
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
+ }
647
920
  }
648
921
  }
649
922
  function positionalSprintArg(args) {
@@ -699,7 +972,7 @@ function syncSprintStateWithWorkflow(cwd, sprintId, workflowPhase) {
699
972
  return;
700
973
  const existing = loadSprintState(cwd);
701
974
  if (!existing) {
702
- saveSprintState(cwd, createSprintState(sprint, nextPhase));
975
+ initializeSprintState(cwd, createSprintState(sprint, nextPhase));
703
976
  return;
704
977
  }
705
978
  if (existing.sprint !== sprint || existing.phase === 'complete')
@@ -936,6 +1209,12 @@ function parsePortableResumeFlags(args) {
936
1209
  async function portableResumeCommand(args, cwd) {
937
1210
  const flags = parsePortableResumeFlags(args);
938
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
+ }
939
1218
  if (flags.invalidSprint) {
940
1219
  console.error(`Error: invalid sprint "${flags.invalidSprint}". Use --sprint=N, e.g. --sprint=177.`);
941
1220
  process.exit(1);
@@ -947,7 +1226,7 @@ async function portableResumeCommand(args, cwd) {
947
1226
  return;
948
1227
  }
949
1228
  if (flags.writePointer) {
950
- const current = loadSprintState(cwd);
1229
+ const current = loadedState.status === 'valid' ? loadedState.state : null;
951
1230
  const sprint = flags.sprint ?? current?.sprint;
952
1231
  if (!sprint) {
953
1232
  console.error('Usage: slope sprint resume --write-pointer --sprint=N [--phase=<phase>] [--output=path]');
@@ -977,21 +1256,34 @@ async function portableResumeCommand(args, cwd) {
977
1256
  process.exit(1);
978
1257
  return;
979
1258
  }
980
- const existing = loadSprintState(cwd);
981
- if (existing && existing.sprint !== plan.sprint && !flags.force) {
982
- console.error(`\nPortable resume refused. Local sprint-state is ${formatSprintLabel(existing.sprint)}, but resume target is ${formatSprintLabel(plan.sprint)}.`);
983
- console.error('Run `slope sprint reset` or rerun with --force if replacing local state is intentional.');
984
- process.exit(1);
985
- 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);
986
1269
  }
987
1270
  if (flags.dryRun) {
988
1271
  console.log('\nDry run: local sprint-state and claims were not changed.');
989
1272
  return;
990
1273
  }
991
- 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
+ }
992
1282
  const restoredClaims = await restoreResumeClaims(cwd, plan);
993
- console.log(`\nPortable sprint resume complete: ${formatSprintLabel(plan.sprint)} (${plan.phase}).`);
994
- 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');
995
1287
  console.log(` Resume claims restored: ${restoredClaims}`);
996
1288
  console.log(' Local DB/locks/metrics were not imported from another machine.');
997
1289
  }
@@ -1330,6 +1622,9 @@ export async function sprintCommand(args) {
1330
1622
  case 'begin':
1331
1623
  await beginCommand(args.slice(1), cwd);
1332
1624
  break;
1625
+ case 'rollover':
1626
+ rolloverCommand(args.slice(1), cwd);
1627
+ break;
1333
1628
  case 'plan': {
1334
1629
  const { sprintPlanCommand } = await import('./sprint-plan.js');
1335
1630
  await sprintPlanCommand(args.slice(1));
@@ -1392,12 +1687,15 @@ Legacy commands:
1392
1687
  slope sprint start --number=N [--phase=<phase>] [--touches=<paths>] [--actor=<name>] [--force]
1393
1688
  Start sprint state tracking with pre-sprint reality checks
1394
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
1395
1692
  slope sprint plan --sprint=N [--output=path] Generate markdown sprint plan (#312)
1396
1693
  slope sprint phase <phase> Update current sprint phase
1397
1694
  slope sprint gate <name> [review evidence options]
1398
1695
  Mark a gate as complete
1399
1696
  Review gates require independent evidence, PR review evidence,
1400
- or explicit weaker-mode self_review/manual_override provenance
1697
+ required reviews need independent evidence or an explicit
1698
+ --waive-independent-review downgrade; optional reviews may use self_review
1401
1699
  slope sprint status Show sprint state and gates
1402
1700
  slope sprint resume --portable [--from=path] [--force] [--dry-run]
1403
1701
  Reconstruct local sprint state from tracked artifacts
@@ -1407,6 +1705,7 @@ Legacy commands:
1407
1705
 
1408
1706
  Workflow commands:
1409
1707
  slope sprint run <id> --workflow=<name> [--var k=v ...] Start workflow execution
1708
+ tickets accepts a list/JSON array or count (tickets=3)
1410
1709
  slope sprint status [sprint_id] Show workflow execution progress
1411
1710
  slope sprint resume <sprint_id> Resume a paused workflow execution
1412
1711
  slope sprint pause <sprint_id> Pause a running workflow execution