@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.
- package/README.md +6 -0
- package/dist/cli/actor.d.ts +18 -0
- package/dist/cli/actor.d.ts.map +1 -0
- package/dist/cli/actor.js +133 -0
- package/dist/cli/actor.js.map +1 -0
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +10 -8
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/briefing.d.ts.map +1 -1
- package/dist/cli/commands/briefing.js +12 -1
- package/dist/cli/commands/briefing.js.map +1 -1
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +10 -6
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/commit-ready.d.ts.map +1 -1
- package/dist/cli/commands/commit-ready.js +4 -2
- package/dist/cli/commands/commit-ready.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/interview.d.ts.map +1 -1
- package/dist/cli/commands/interview.js +5 -0
- package/dist/cli/commands/interview.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/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +7 -3
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/commands/retro.d.ts.map +1 -1
- package/dist/cli/commands/retro.js +27 -6
- package/dist/cli/commands/retro.js.map +1 -1
- package/dist/cli/commands/review-run.d.ts +2 -0
- package/dist/cli/commands/review-run.d.ts.map +1 -1
- package/dist/cli/commands/review-run.js +38 -8
- package/dist/cli/commands/review-run.js.map +1 -1
- package/dist/cli/commands/review-state.d.ts +2 -5
- package/dist/cli/commands/review-state.d.ts.map +1 -1
- package/dist/cli/commands/review-state.js +211 -47
- 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 +291 -9
- 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 +594 -68
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +20 -4
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/ticket.d.ts.map +1 -1
- package/dist/cli/commands/ticket.js +31 -9
- package/dist/cli/commands/ticket.js.map +1 -1
- package/dist/cli/commands/version.d.ts +12 -0
- package/dist/cli/commands/version.d.ts.map +1 -1
- package/dist/cli/commands/version.js +169 -4
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/git-preflight.d.ts +9 -0
- package/dist/cli/git-preflight.d.ts.map +1 -0
- package/dist/cli/git-preflight.js +36 -0
- package/dist/cli/git-preflight.js.map +1 -0
- 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.js +60 -10
- package/dist/cli/guards/phase-boundary.js.map +1 -1
- package/dist/cli/guards/post-push.d.ts.map +1 -1
- package/dist/cli/guards/post-push.js +8 -2
- package/dist/cli/guards/post-push.js.map +1 -1
- package/dist/cli/guards/pr-review.d.ts +6 -0
- package/dist/cli/guards/pr-review.d.ts.map +1 -1
- package/dist/cli/guards/pr-review.js +22 -11
- package/dist/cli/guards/pr-review.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.d.ts.map +1 -1
- package/dist/cli/guards/session-briefing.js +24 -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 +107 -18
- package/dist/cli/guards/sprint-completion.js.map +1 -1
- package/dist/cli/guards/workflow-step-gate.d.ts.map +1 -1
- package/dist/cli/guards/workflow-step-gate.js +25 -6
- package/dist/cli/guards/workflow-step-gate.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 +45 -3
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/reviewer-agents.d.ts +22 -0
- package/dist/cli/reviewer-agents.d.ts.map +1 -0
- package/dist/cli/reviewer-agents.js +141 -0
- package/dist/cli/reviewer-agents.js.map +1 -0
- package/dist/cli/roadmap-source-store.d.ts +37 -0
- package/dist/cli/roadmap-source-store.d.ts.map +1 -0
- package/dist/cli/roadmap-source-store.js +298 -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 +99 -2
- package/dist/cli/sprint-state.d.ts.map +1 -1
- package/dist/cli/sprint-state.js +349 -14
- 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 +2 -0
- package/dist/cli/workflow-resync.d.ts.map +1 -1
- package/dist/cli/workflow-resync.js +34 -12
- package/dist/cli/workflow-resync.js.map +1 -1
- package/dist/core/analyzers/git.d.ts.map +1 -1
- package/dist/core/analyzers/git.js +95 -5
- package/dist/core/analyzers/git.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/constants.d.ts.map +1 -1
- package/dist/core/constants.js +1 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/findings.d.ts +20 -0
- package/dist/core/findings.d.ts.map +1 -0
- package/dist/core/findings.js +85 -0
- package/dist/core/findings.js.map +1 -0
- package/dist/core/generators/first-sprint.js +1 -1
- package/dist/core/generators/first-sprint.js.map +1 -1
- package/dist/core/generators/roadmap.js +3 -3
- package/dist/core/generators/roadmap.js.map +1 -1
- package/dist/core/index.d.ts +10 -4
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/interview-engine.d.ts.map +1 -1
- package/dist/core/interview-engine.js +5 -4
- package/dist/core/interview-engine.js.map +1 -1
- package/dist/core/interview-metaphor.d.ts +5 -0
- package/dist/core/interview-metaphor.d.ts.map +1 -0
- package/dist/core/interview-metaphor.js +20 -0
- package/dist/core/interview-metaphor.js.map +1 -0
- package/dist/core/interview-steps.d.ts.map +1 -1
- package/dist/core/interview-steps.js +3 -6
- package/dist/core/interview-steps.js.map +1 -1
- package/dist/core/interview.d.ts.map +1 -1
- package/dist/core/interview.js +5 -6
- package/dist/core/interview.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-sources.d.ts +52 -0
- package/dist/core/roadmap-sources.d.ts.map +1 -0
- package/dist/core/roadmap-sources.js +483 -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/types.d.ts +11 -1
- package/dist/core/types.d.ts.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/dist/store/index.d.ts +13 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +60 -7
- package/dist/store/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/workflows/sprint-standard.yaml +7 -1
- package/templates/claude-code/CLAUDE.md +3 -1
- package/templates/claude-code/commands/post-sprint.md +2 -0
- package/templates/claude-code/commands/start-sprint.md +10 -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-retro/SKILL.md +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 +13 -9
- package/templates/pi/skills/post-sprint.md +2 -0
- package/templates/pi/skills/start-sprint.md +10 -5
|
@@ -1,6 +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
|
+
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
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
90
|
-
const ticket =
|
|
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:
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
console.
|
|
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
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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
|
|
199
|
-
|
|
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(
|
|
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
|
|
209
|
-
const phaseInput =
|
|
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
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
758
|
-
if (existing
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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>
|
|
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
|