@slope-dev/slope 1.60.0 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli/commands/agent.js +2 -2
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/commit-ready.js +1 -1
- package/dist/cli/commands/commit-ready.js.map +1 -1
- package/dist/cli/commands/next.d.ts +1 -1
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +31 -2
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/now.d.ts +3 -0
- package/dist/cli/commands/now.d.ts.map +1 -1
- package/dist/cli/commands/now.js +14 -2
- package/dist/cli/commands/now.js.map +1 -1
- package/dist/cli/commands/pr.d.ts +11 -3
- package/dist/cli/commands/pr.d.ts.map +1 -1
- package/dist/cli/commands/pr.js +81 -31
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/commands/review-run.d.ts +40 -6
- package/dist/cli/commands/review-run.d.ts.map +1 -1
- package/dist/cli/commands/review-run.js +235 -105
- package/dist/cli/commands/review-run.js.map +1 -1
- package/dist/cli/commands/review-state.d.ts.map +1 -1
- package/dist/cli/commands/review-state.js +61 -7
- package/dist/cli/commands/review-state.js.map +1 -1
- package/dist/cli/commands/review.d.ts +9 -1
- package/dist/cli/commands/review.d.ts.map +1 -1
- package/dist/cli/commands/review.js +67 -39
- package/dist/cli/commands/review.js.map +1 -1
- package/dist/cli/commands/roadmap.d.ts.map +1 -1
- package/dist/cli/commands/roadmap.js +364 -5
- package/dist/cli/commands/roadmap.js.map +1 -1
- package/dist/cli/commands/sprint.d.ts.map +1 -1
- package/dist/cli/commands/sprint.js +353 -54
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/guards/claim-required.d.ts.map +1 -1
- package/dist/cli/guards/claim-required.js +19 -4
- package/dist/cli/guards/claim-required.js.map +1 -1
- package/dist/cli/guards/next-action.js +1 -1
- package/dist/cli/guards/next-action.js.map +1 -1
- package/dist/cli/guards/phase-boundary.d.ts.map +1 -1
- package/dist/cli/guards/phase-boundary.js +8 -2
- package/dist/cli/guards/phase-boundary.js.map +1 -1
- package/dist/cli/guards/post-push.js +2 -2
- package/dist/cli/guards/post-push.js.map +1 -1
- package/dist/cli/guards/review-tier.d.ts.map +1 -1
- package/dist/cli/guards/review-tier.js +2 -3
- package/dist/cli/guards/review-tier.js.map +1 -1
- package/dist/cli/guards/roadmap-edit-shipped.d.ts.map +1 -1
- package/dist/cli/guards/roadmap-edit-shipped.js +143 -7
- package/dist/cli/guards/roadmap-edit-shipped.js.map +1 -1
- package/dist/cli/guards/scope-drift.d.ts.map +1 -1
- package/dist/cli/guards/scope-drift.js +8 -2
- package/dist/cli/guards/scope-drift.js.map +1 -1
- package/dist/cli/guards/session-briefing.js +6 -2
- package/dist/cli/guards/session-briefing.js.map +1 -1
- package/dist/cli/guards/sprint-completion.d.ts.map +1 -1
- package/dist/cli/guards/sprint-completion.js +94 -18
- package/dist/cli/guards/sprint-completion.js.map +1 -1
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +31 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/review-diff.d.ts +72 -0
- package/dist/cli/review-diff.d.ts.map +1 -0
- package/dist/cli/review-diff.js +416 -0
- package/dist/cli/review-diff.js.map +1 -0
- package/dist/cli/roadmap-source-migration.d.ts +113 -0
- package/dist/cli/roadmap-source-migration.d.ts.map +1 -0
- package/dist/cli/roadmap-source-migration.js +721 -0
- package/dist/cli/roadmap-source-migration.js.map +1 -0
- package/dist/cli/roadmap-source-store.d.ts +44 -0
- package/dist/cli/roadmap-source-store.d.ts.map +1 -0
- package/dist/cli/roadmap-source-store.js +307 -0
- package/dist/cli/roadmap-source-store.js.map +1 -0
- package/dist/cli/sprint-inference.js +1 -1
- package/dist/cli/sprint-inference.js.map +1 -1
- package/dist/cli/sprint-rollover.d.ts +102 -0
- package/dist/cli/sprint-rollover.d.ts.map +1 -0
- package/dist/cli/sprint-rollover.js +558 -0
- package/dist/cli/sprint-rollover.js.map +1 -0
- package/dist/cli/sprint-state.d.ts +72 -1
- package/dist/cli/sprint-state.d.ts.map +1 -1
- package/dist/cli/sprint-state.js +203 -5
- package/dist/cli/sprint-state.js.map +1 -1
- package/dist/cli/template-generator.d.ts.map +1 -1
- package/dist/cli/template-generator.js +45 -32
- package/dist/cli/template-generator.js.map +1 -1
- package/dist/cli/workflow-resync.d.ts.map +1 -1
- package/dist/cli/workflow-resync.js +9 -8
- package/dist/cli/workflow-resync.js.map +1 -1
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +254 -14
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/builder.d.ts +16 -2
- package/dist/core/builder.d.ts.map +1 -1
- package/dist/core/builder.js +82 -5
- package/dist/core/builder.js.map +1 -1
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/roadmap-focus.d.ts +101 -0
- package/dist/core/roadmap-focus.d.ts.map +1 -0
- package/dist/core/roadmap-focus.js +293 -0
- package/dist/core/roadmap-focus.js.map +1 -0
- package/dist/core/roadmap-migration.d.ts +101 -0
- package/dist/core/roadmap-migration.d.ts.map +1 -0
- package/dist/core/roadmap-migration.js +630 -0
- package/dist/core/roadmap-migration.js.map +1 -0
- package/dist/core/roadmap-sources.d.ts +52 -0
- package/dist/core/roadmap-sources.d.ts.map +1 -0
- package/dist/core/roadmap-sources.js +487 -0
- package/dist/core/roadmap-sources.js.map +1 -0
- package/dist/core/roadmap.d.ts +25 -1
- package/dist/core/roadmap.d.ts.map +1 -1
- package/dist/core/roadmap.js +62 -45
- package/dist/core/roadmap.js.map +1 -1
- package/dist/core/workflow-engine.d.ts.map +1 -1
- package/dist/core/workflow-engine.js +17 -2
- package/dist/core/workflow-engine.js.map +1 -1
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +5 -4
- package/dist/mcp/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/core/workflows/sprint-standard.yaml +1 -1
- package/templates/claude-code/CLAUDE.md +3 -1
- package/templates/claude-code/commands/start-sprint.md +9 -5
- package/templates/claude-code/rules/sprint-checklist.md +13 -10
- package/templates/codex/plugins/slope/.codex-plugin/plugin.json +1 -1
- package/templates/codex/plugins/slope/skills/slope-sprint/SKILL.md +5 -4
- package/templates/cursor/rules/slope-sprint-checklist.mdc +11 -8
- package/templates/generic/SLOPE-CHECKLIST.md +10 -7
- package/templates/pi/skills/start-sprint.md +9 -5
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { loadSprintState,
|
|
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
|
|
226
|
-
const
|
|
227
|
-
|
|
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(
|
|
233
|
-
const ticket =
|
|
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:
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
console.
|
|
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
|
|
344
|
-
|
|
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(
|
|
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
|
|
354
|
-
const phaseInput =
|
|
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
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
397
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
981
|
-
if (existing
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|