@boyingliu01/xp-gate 0.15.5 → 0.18.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/adapter-common.sh +0 -0
- package/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/go.sh +0 -0
- package/adapters/iac.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +29 -1
- package/gate-3.sh +0 -0
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +0 -0
- package/hooks/adapter-common.sh +0 -0
- package/hooks/gate-3.sh +0 -0
- package/hooks/gate-4.sh +0 -0
- package/hooks/gate-7.sh +0 -0
- package/hooks/gate-8.sh +0 -0
- package/hooks/gate-9.sh +0 -0
- package/hooks/lib/now-ms.sh +33 -0
- package/hooks/pre-commit +63 -47
- package/hooks/pre-push +0 -0
- package/hooks/sprint-gate.sh +291 -0
- package/lib/__tests__/check-version.test.js +122 -0
- package/lib/__tests__/doctor.test.js +13 -15
- package/lib/__tests__/gate-audit.test.ts +38 -0
- package/lib/__tests__/phase-transition.test.js +512 -272
- package/lib/__tests__/retro.test.js +131 -0
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/__tests__/sprint-status-rework.test.js +184 -0
- package/lib/check-version.js +9 -0
- package/lib/doctor-tui.js +268 -198
- package/lib/doctor.js +15 -3
- package/lib/gate-audit.ts +18 -7
- package/lib/gate-runner.js +77 -30
- package/lib/init.js +10 -0
- package/lib/language-tools.js +833 -833
- package/lib/mattpocock-license-verdict.json +7 -0
- package/lib/phase-transition.js +499 -249
- package/lib/retro.js +222 -0
- package/lib/shared-utils.js +7 -0
- package/lib/sprint-audit.js +235 -235
- package/lib/sprint-init.js +207 -0
- package/lib/sprint-state-migrator.js +1 -0
- package/lib/sprint-status.js +111 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +3 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
- package/plugins/claude-code/bin/xp-gate-check +0 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
- package/plugins/opencode/index.ts +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
- package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/opencode/skills/grilling/SKILL.md +12 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
- package/plugins/qoder/agents/delphi-architecture.md +48 -48
- package/plugins/qoder/agents/delphi-feasibility.md +48 -48
- package/plugins/qoder/agents/delphi-technical.md +48 -48
- package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
- package/plugins/qoder/bin/xp-gate-check +0 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
- package/scripts/__tests__/sync-package-content.test.js +50 -0
- package/scripts/check-mattpocock-license.cjs +302 -0
- package/scripts/delphi-external-review.cjs +432 -432
- package/scripts/migrate-audit-anomalies.mjs +78 -0
- package/scripts/sync-package-content.js +52 -5
- package/skills/batch-grill-me/SKILL.md +15 -0
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +59 -4
- package/skills/delphi-review/references/requirements.md +390 -0
- package/skills/delphi-review/references/round-templates.md +67 -0
- package/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/SKILL.md +74 -0
- package/skills/grill-with-docs/SKILL.md +7 -0
- package/skills/grilling/SKILL.md +12 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +168 -43
- package/skills/sprint-flow/references/components/memory.md +1 -1
- package/skills/sprint-flow/references/components/middleware.md +1 -1
- package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/skills/sprint-flow/references/phase-overview.md +58 -53
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/skills/test-specification-alignment/SKILL.md +8 -3
- package/skills/to-issues/SKILL.md +1 -1
- package/adapters/gate-3.sh +0 -68
- package/adapters/gate-4.sh +0 -80
- package/adapters/gate-7.sh +0 -45
- package/adapters/gate-8.sh +0 -56
- package/adapters/gate-9.sh +0 -104
package/lib/sprint-audit.js
CHANGED
|
@@ -1,235 +1,235 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* xp-gate sprint-audit — Layer 2: Final sprint completeness audit.
|
|
3
|
-
*
|
|
4
|
-
* Checks phase_history coverage, time records, artifact records,
|
|
5
|
-
* and state consistency at Phase 6 CLOSE.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* xp-gate sprint-audit [--dir <path>] [--json]
|
|
9
|
-
*
|
|
10
|
-
* Verdict rules:
|
|
11
|
-
* 6/6 completed + 0 errors → PASS (exit 0)
|
|
12
|
-
* >= 4/6 completed + 0 errors → PASS_WITH_WARNINGS (exit 0)
|
|
13
|
-
* < 4/6 completed or any errors → FAIL (exit 1)
|
|
14
|
-
* No sprint-state.json → SKIP (exit 0)
|
|
15
|
-
*
|
|
16
|
-
* @module sprint-audit
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
'use strict';
|
|
20
|
-
|
|
21
|
-
const fs = require('fs');
|
|
22
|
-
const path = require('path');
|
|
23
|
-
const { PHASE_NAMES } = require('./phase-transition');
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Run the sprint audit.
|
|
27
|
-
* @param {string[]} args - CLI arguments
|
|
28
|
-
* @returns {Promise<number>} Exit code
|
|
29
|
-
*/
|
|
30
|
-
async function handleSprintAudit(args = []) {
|
|
31
|
-
if (args.includes('--help')) {
|
|
32
|
-
console.log('Usage: xp-gate sprint-audit [options]');
|
|
33
|
-
console.log('');
|
|
34
|
-
console.log('Options:');
|
|
35
|
-
console.log(' --dir <path> Project directory (default: cwd)');
|
|
36
|
-
console.log(' --json Output as JSON instead of human-readable');
|
|
37
|
-
console.log('');
|
|
38
|
-
console.log('Runs final sprint completeness audit at Phase 6 CLOSE.');
|
|
39
|
-
console.log('Checks phase coverage, time records, artifact records,');
|
|
40
|
-
console.log('and state consistency.');
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const dirIdx = args.indexOf('--dir');
|
|
45
|
-
const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
|
|
46
|
-
const jsonFlag = args.includes('--json');
|
|
47
|
-
|
|
48
|
-
const stateFile = path.join(projectDir, '.sprint-state', 'sprint-state.json');
|
|
49
|
-
|
|
50
|
-
// No sprint-state.json → SKIP
|
|
51
|
-
if (!fs.existsSync(stateFile)) {
|
|
52
|
-
if (jsonFlag) {
|
|
53
|
-
console.log(JSON.stringify({ verdict: 'SKIP', reason: 'No sprint-state.json found' }));
|
|
54
|
-
} else {
|
|
55
|
-
console.log('⏭️ No sprint-state.json found — skipping audit.');
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let state;
|
|
61
|
-
try {
|
|
62
|
-
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
63
|
-
} catch (err) {
|
|
64
|
-
const result = {
|
|
65
|
-
verdict: 'FAIL',
|
|
66
|
-
errors: [`Failed to parse sprint-state.json: ${err.message}`],
|
|
67
|
-
};
|
|
68
|
-
if (jsonFlag) {
|
|
69
|
-
console.log(JSON.stringify(result, null, 2));
|
|
70
|
-
} else {
|
|
71
|
-
console.error(`❌ ${result.errors[0]}`);
|
|
72
|
-
}
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── Run checks ──
|
|
77
|
-
const warnings = [];
|
|
78
|
-
const errors = [];
|
|
79
|
-
const phaseHistory = Array.isArray(state.phase_history) ? state.phase_history : [];
|
|
80
|
-
|
|
81
|
-
// Build lookup
|
|
82
|
-
const historyLookup = {};
|
|
83
|
-
for (const entry of phaseHistory) {
|
|
84
|
-
historyLookup[entry.phase] = entry;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check 1: Phase coverage
|
|
88
|
-
const completedPhases = [];
|
|
89
|
-
const missingPhases = [];
|
|
90
|
-
const incompletePhases = [];
|
|
91
|
-
|
|
92
|
-
for (let i = 1; i <= 6; i++) {
|
|
93
|
-
const entry = historyLookup[i];
|
|
94
|
-
if (!entry) {
|
|
95
|
-
missingPhases.push(i);
|
|
96
|
-
} else if (entry.status === 'completed' || entry.status === 'skipped') {
|
|
97
|
-
completedPhases.push(i);
|
|
98
|
-
} else {
|
|
99
|
-
incompletePhases.push({ phase: i, name: PHASE_NAMES[i] || `Phase ${i}`, status: entry.status });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Check 2: Time records for completed phases
|
|
104
|
-
for (const phaseNum of completedPhases) {
|
|
105
|
-
const entry = historyLookup[phaseNum];
|
|
106
|
-
if (entry.status === 'completed' && (entry.duration_seconds == null || entry.duration_seconds === 0)) {
|
|
107
|
-
warnings.push(`Phase ${phaseNum} (${PHASE_NAMES[phaseNum]}) completed but has no duration recorded`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Check 3: Artifact records (phase-{N}-summary.md)
|
|
112
|
-
const outputsDir = path.join(projectDir, '.sprint-state', 'phase-outputs');
|
|
113
|
-
for (const phaseNum of completedPhases) {
|
|
114
|
-
if (historyLookup[phaseNum].status === 'completed') {
|
|
115
|
-
const summaryFile = path.join(outputsDir, `phase-${phaseNum}-summary.md`);
|
|
116
|
-
if (!fs.existsSync(summaryFile)) {
|
|
117
|
-
warnings.push(`Phase ${phaseNum} summary missing: .sprint-state/phase-outputs/phase-${phaseNum}-summary.md`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Check 4: State consistency — phase field vs phase_history last entry
|
|
123
|
-
if (phaseHistory.length > 0) {
|
|
124
|
-
const lastEntry = phaseHistory[phaseHistory.length - 1];
|
|
125
|
-
if (state.phase !== undefined && state.phase !== lastEntry.phase) {
|
|
126
|
-
errors.push(
|
|
127
|
-
`State inconsistency: sprint-state.phase=${state.phase} but phase_history last entry is phase ${lastEntry.phase}`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ── Determine verdict ──
|
|
133
|
-
const completedCount = completedPhases.length;
|
|
134
|
-
let verdict;
|
|
135
|
-
if (errors.length > 0 || completedCount < 4) {
|
|
136
|
-
verdict = 'FAIL';
|
|
137
|
-
} else if (completedCount >= 6 && errors.length === 0) {
|
|
138
|
-
verdict = 'PASS';
|
|
139
|
-
} else {
|
|
140
|
-
verdict = 'PASS_WITH_WARNINGS';
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── Build result ──
|
|
144
|
-
const result = {
|
|
145
|
-
sprint_id: state.id || 'unknown',
|
|
146
|
-
branch: state.isolation?.branch || 'unknown',
|
|
147
|
-
coverage: {
|
|
148
|
-
completed: completedCount,
|
|
149
|
-
total: 6,
|
|
150
|
-
pct: Math.round((completedCount / 6) * 100),
|
|
151
|
-
},
|
|
152
|
-
missing_phases: missingPhases.map(p => ({ phase: p, name: PHASE_NAMES[p] || `Phase ${p}` })),
|
|
153
|
-
incomplete_phases: incompletePhases,
|
|
154
|
-
warnings,
|
|
155
|
-
errors,
|
|
156
|
-
verdict,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// ── Persist report (latest overwrites — expected behavior) ──
|
|
160
|
-
const reportPath = path.join(projectDir, '.sprint-state', 'audit-report.json');
|
|
161
|
-
try {
|
|
162
|
-
fs.writeFileSync(reportPath, JSON.stringify(result, null, 2), 'utf8');
|
|
163
|
-
} catch (err) {
|
|
164
|
-
// Report write failure is WARNING, not BLOCK
|
|
165
|
-
warnings.push(`Failed to write audit report: ${err.message}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── Output ──
|
|
169
|
-
if (jsonFlag) {
|
|
170
|
-
console.log(JSON.stringify(result, null, 2));
|
|
171
|
-
} else {
|
|
172
|
-
printHumanReadable(result, state);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Exit code: FAIL → 1, others → 0
|
|
176
|
-
return verdict === 'FAIL' ? 1 : 0;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Print human-readable audit report.
|
|
181
|
-
* @param {object} result - Audit result object
|
|
182
|
-
* @param {object} state - Sprint state object
|
|
183
|
-
*/
|
|
184
|
-
function printHumanReadable(result, state) {
|
|
185
|
-
const lines = [];
|
|
186
|
-
lines.push('');
|
|
187
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
188
|
-
lines.push(` Sprint Audit Report — ${result.sprint_id}`);
|
|
189
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
190
|
-
lines.push('');
|
|
191
|
-
lines.push(` Branch: ${result.branch}`);
|
|
192
|
-
lines.push(` Phase Coverage: ${result.coverage.completed}/${result.coverage.total} (${result.coverage.pct}%)`);
|
|
193
|
-
lines.push('');
|
|
194
|
-
lines.push(' Phase History:');
|
|
195
|
-
|
|
196
|
-
for (let i = 1; i <= 6; i++) {
|
|
197
|
-
const entry = result.incomplete_phases.find(p => p.phase === i);
|
|
198
|
-
const missing = result.missing_phases.find(p => p.phase === i);
|
|
199
|
-
const name = (PHASE_NAMES[i] || `Phase ${i}`).padEnd(10);
|
|
200
|
-
|
|
201
|
-
if (missing) {
|
|
202
|
-
lines.push(` ❌ Phase ${i}/6 ${name} — not recorded`);
|
|
203
|
-
} else if (entry) {
|
|
204
|
-
lines.push(` ⚠️ Phase ${i}/6 ${name} ${entry.status} ← not yet completed`);
|
|
205
|
-
} else {
|
|
206
|
-
lines.push(` ✅ Phase ${i}/6 ${name} completed`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (result.warnings.length > 0) {
|
|
211
|
-
lines.push('');
|
|
212
|
-
lines.push(' Warnings:');
|
|
213
|
-
for (const w of result.warnings) {
|
|
214
|
-
lines.push(` ⚠️ ${w}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (result.errors.length > 0) {
|
|
219
|
-
lines.push('');
|
|
220
|
-
lines.push(' Errors:');
|
|
221
|
-
for (const e of result.errors) {
|
|
222
|
-
lines.push(` ❌ ${e}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
lines.push('');
|
|
227
|
-
const issueCount = result.warnings.length + result.errors.length;
|
|
228
|
-
lines.push(` Overall: ${result.warnings.length} warning(s), ${result.errors.length} error(s)`);
|
|
229
|
-
lines.push(` Verdict: ${result.verdict}`);
|
|
230
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
231
|
-
|
|
232
|
-
console.log(lines.join('\n'));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
module.exports = { handleSprintAudit };
|
|
1
|
+
/**
|
|
2
|
+
* xp-gate sprint-audit — Layer 2: Final sprint completeness audit.
|
|
3
|
+
*
|
|
4
|
+
* Checks phase_history coverage, time records, artifact records,
|
|
5
|
+
* and state consistency at Phase 6 CLOSE.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* xp-gate sprint-audit [--dir <path>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* Verdict rules:
|
|
11
|
+
* 6/6 completed + 0 errors → PASS (exit 0)
|
|
12
|
+
* >= 4/6 completed + 0 errors → PASS_WITH_WARNINGS (exit 0)
|
|
13
|
+
* < 4/6 completed or any errors → FAIL (exit 1)
|
|
14
|
+
* No sprint-state.json → SKIP (exit 0)
|
|
15
|
+
*
|
|
16
|
+
* @module sprint-audit
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { PHASE_NAMES } = require('./phase-transition');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run the sprint audit.
|
|
27
|
+
* @param {string[]} args - CLI arguments
|
|
28
|
+
* @returns {Promise<number>} Exit code
|
|
29
|
+
*/
|
|
30
|
+
async function handleSprintAudit(args = []) {
|
|
31
|
+
if (args.includes('--help')) {
|
|
32
|
+
console.log('Usage: xp-gate sprint-audit [options]');
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log('Options:');
|
|
35
|
+
console.log(' --dir <path> Project directory (default: cwd)');
|
|
36
|
+
console.log(' --json Output as JSON instead of human-readable');
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log('Runs final sprint completeness audit at Phase 6 CLOSE.');
|
|
39
|
+
console.log('Checks phase coverage, time records, artifact records,');
|
|
40
|
+
console.log('and state consistency.');
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dirIdx = args.indexOf('--dir');
|
|
45
|
+
const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
|
|
46
|
+
const jsonFlag = args.includes('--json');
|
|
47
|
+
|
|
48
|
+
const stateFile = path.join(projectDir, '.sprint-state', 'sprint-state.json');
|
|
49
|
+
|
|
50
|
+
// No sprint-state.json → SKIP
|
|
51
|
+
if (!fs.existsSync(stateFile)) {
|
|
52
|
+
if (jsonFlag) {
|
|
53
|
+
console.log(JSON.stringify({ verdict: 'SKIP', reason: 'No sprint-state.json found' }));
|
|
54
|
+
} else {
|
|
55
|
+
console.log('⏭️ No sprint-state.json found — skipping audit.');
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let state;
|
|
61
|
+
try {
|
|
62
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const result = {
|
|
65
|
+
verdict: 'FAIL',
|
|
66
|
+
errors: [`Failed to parse sprint-state.json: ${err.message}`],
|
|
67
|
+
};
|
|
68
|
+
if (jsonFlag) {
|
|
69
|
+
console.log(JSON.stringify(result, null, 2));
|
|
70
|
+
} else {
|
|
71
|
+
console.error(`❌ ${result.errors[0]}`);
|
|
72
|
+
}
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Run checks ──
|
|
77
|
+
const warnings = [];
|
|
78
|
+
const errors = [];
|
|
79
|
+
const phaseHistory = Array.isArray(state.phase_history) ? state.phase_history : [];
|
|
80
|
+
|
|
81
|
+
// Build lookup
|
|
82
|
+
const historyLookup = {};
|
|
83
|
+
for (const entry of phaseHistory) {
|
|
84
|
+
historyLookup[entry.phase] = entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check 1: Phase coverage
|
|
88
|
+
const completedPhases = [];
|
|
89
|
+
const missingPhases = [];
|
|
90
|
+
const incompletePhases = [];
|
|
91
|
+
|
|
92
|
+
for (let i = 1; i <= 6; i++) {
|
|
93
|
+
const entry = historyLookup[i];
|
|
94
|
+
if (!entry) {
|
|
95
|
+
missingPhases.push(i);
|
|
96
|
+
} else if (entry.status === 'completed' || entry.status === 'skipped') {
|
|
97
|
+
completedPhases.push(i);
|
|
98
|
+
} else {
|
|
99
|
+
incompletePhases.push({ phase: i, name: PHASE_NAMES[i] || `Phase ${i}`, status: entry.status });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check 2: Time records for completed phases
|
|
104
|
+
for (const phaseNum of completedPhases) {
|
|
105
|
+
const entry = historyLookup[phaseNum];
|
|
106
|
+
if (entry.status === 'completed' && (entry.duration_seconds == null || entry.duration_seconds === 0)) {
|
|
107
|
+
warnings.push(`Phase ${phaseNum} (${PHASE_NAMES[phaseNum]}) completed but has no duration recorded`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check 3: Artifact records (phase-{N}-summary.md)
|
|
112
|
+
const outputsDir = path.join(projectDir, '.sprint-state', 'phase-outputs');
|
|
113
|
+
for (const phaseNum of completedPhases) {
|
|
114
|
+
if (historyLookup[phaseNum].status === 'completed') {
|
|
115
|
+
const summaryFile = path.join(outputsDir, `phase-${phaseNum}-summary.md`);
|
|
116
|
+
if (!fs.existsSync(summaryFile)) {
|
|
117
|
+
warnings.push(`Phase ${phaseNum} summary missing: .sprint-state/phase-outputs/phase-${phaseNum}-summary.md`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check 4: State consistency — phase field vs phase_history last entry
|
|
123
|
+
if (phaseHistory.length > 0) {
|
|
124
|
+
const lastEntry = phaseHistory[phaseHistory.length - 1];
|
|
125
|
+
if (state.phase !== undefined && state.phase !== lastEntry.phase) {
|
|
126
|
+
errors.push(
|
|
127
|
+
`State inconsistency: sprint-state.phase=${state.phase} but phase_history last entry is phase ${lastEntry.phase}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Determine verdict ──
|
|
133
|
+
const completedCount = completedPhases.length;
|
|
134
|
+
let verdict;
|
|
135
|
+
if (errors.length > 0 || completedCount < 4) {
|
|
136
|
+
verdict = 'FAIL';
|
|
137
|
+
} else if (completedCount >= 6 && errors.length === 0) {
|
|
138
|
+
verdict = 'PASS';
|
|
139
|
+
} else {
|
|
140
|
+
verdict = 'PASS_WITH_WARNINGS';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Build result ──
|
|
144
|
+
const result = {
|
|
145
|
+
sprint_id: state.id || 'unknown',
|
|
146
|
+
branch: state.isolation?.branch || 'unknown',
|
|
147
|
+
coverage: {
|
|
148
|
+
completed: completedCount,
|
|
149
|
+
total: 6,
|
|
150
|
+
pct: Math.round((completedCount / 6) * 100),
|
|
151
|
+
},
|
|
152
|
+
missing_phases: missingPhases.map(p => ({ phase: p, name: PHASE_NAMES[p] || `Phase ${p}` })),
|
|
153
|
+
incomplete_phases: incompletePhases,
|
|
154
|
+
warnings,
|
|
155
|
+
errors,
|
|
156
|
+
verdict,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ── Persist report (latest overwrites — expected behavior) ──
|
|
160
|
+
const reportPath = path.join(projectDir, '.sprint-state', 'audit-report.json');
|
|
161
|
+
try {
|
|
162
|
+
fs.writeFileSync(reportPath, JSON.stringify(result, null, 2), 'utf8');
|
|
163
|
+
} catch (err) {
|
|
164
|
+
// Report write failure is WARNING, not BLOCK
|
|
165
|
+
warnings.push(`Failed to write audit report: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Output ──
|
|
169
|
+
if (jsonFlag) {
|
|
170
|
+
console.log(JSON.stringify(result, null, 2));
|
|
171
|
+
} else {
|
|
172
|
+
printHumanReadable(result, state);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Exit code: FAIL → 1, others → 0
|
|
176
|
+
return verdict === 'FAIL' ? 1 : 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Print human-readable audit report.
|
|
181
|
+
* @param {object} result - Audit result object
|
|
182
|
+
* @param {object} state - Sprint state object
|
|
183
|
+
*/
|
|
184
|
+
function printHumanReadable(result, state) {
|
|
185
|
+
const lines = [];
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
188
|
+
lines.push(` Sprint Audit Report — ${result.sprint_id}`);
|
|
189
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push(` Branch: ${result.branch}`);
|
|
192
|
+
lines.push(` Phase Coverage: ${result.coverage.completed}/${result.coverage.total} (${result.coverage.pct}%)`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push(' Phase History:');
|
|
195
|
+
|
|
196
|
+
for (let i = 1; i <= 6; i++) {
|
|
197
|
+
const entry = result.incomplete_phases.find(p => p.phase === i);
|
|
198
|
+
const missing = result.missing_phases.find(p => p.phase === i);
|
|
199
|
+
const name = (PHASE_NAMES[i] || `Phase ${i}`).padEnd(10);
|
|
200
|
+
|
|
201
|
+
if (missing) {
|
|
202
|
+
lines.push(` ❌ Phase ${i}/6 ${name} — not recorded`);
|
|
203
|
+
} else if (entry) {
|
|
204
|
+
lines.push(` ⚠️ Phase ${i}/6 ${name} ${entry.status} ← not yet completed`);
|
|
205
|
+
} else {
|
|
206
|
+
lines.push(` ✅ Phase ${i}/6 ${name} completed`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (result.warnings.length > 0) {
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(' Warnings:');
|
|
213
|
+
for (const w of result.warnings) {
|
|
214
|
+
lines.push(` ⚠️ ${w}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (result.errors.length > 0) {
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push(' Errors:');
|
|
221
|
+
for (const e of result.errors) {
|
|
222
|
+
lines.push(` ❌ ${e}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
lines.push('');
|
|
227
|
+
const issueCount = result.warnings.length + result.errors.length;
|
|
228
|
+
lines.push(` Overall: ${result.warnings.length} warning(s), ${result.errors.length} error(s)`);
|
|
229
|
+
lines.push(` Verdict: ${result.verdict}`);
|
|
230
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
231
|
+
|
|
232
|
+
console.log(lines.join('\n'));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { handleSprintAudit };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xp-gate sprint-init — Programmatic sprint initialization.
|
|
3
|
+
*
|
|
4
|
+
* Resolves #366 (phase-transition never called by orchestrator).
|
|
5
|
+
* Layer 1 of the three-layer enforcement mechanism.
|
|
6
|
+
*
|
|
7
|
+
* Creates .sprint-state/sprint-state.json as the single entry point
|
|
8
|
+
* for sprint initialization. Replaces manual state file creation.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* xp-gate sprint-init "<task_description>" [--issues "<#123,#456>"] [--force] [--dry-run] [--dir <path>]
|
|
12
|
+
*
|
|
13
|
+
* Non-interactive design (LLM orchestrator cannot handle stdin):
|
|
14
|
+
* - Default: existing active sprint → error exit 1
|
|
15
|
+
* - --force: override existing sprint, backup old state to .sprint-history/
|
|
16
|
+
* - --dry-run: pre-check conflicts, no writes
|
|
17
|
+
* - Idempotent: same task_description + branch → output dashboard, exit 0
|
|
18
|
+
*
|
|
19
|
+
* @module sprint-init
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { execFileSync } = require('child_process');
|
|
27
|
+
const { SprintStateManager } = require('./sprint-state-manager');
|
|
28
|
+
const { renderDashboard } = require('./phase-transition');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect current git branch.
|
|
32
|
+
* @param {string} cwd - Working directory
|
|
33
|
+
* @returns {string} Branch name or 'detached'
|
|
34
|
+
*/
|
|
35
|
+
function detectBranch(cwd) {
|
|
36
|
+
try {
|
|
37
|
+
const branch = execFileSync('git', ['branch', '--show-current'], {
|
|
38
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
}).trim();
|
|
40
|
+
return branch || 'detached';
|
|
41
|
+
} catch {
|
|
42
|
+
return 'unknown';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate sprint ID from date and sequence.
|
|
48
|
+
* @returns {string} Sprint ID like "sprint-20260722-01"
|
|
49
|
+
*/
|
|
50
|
+
function generateSprintId() {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
53
|
+
const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');
|
|
54
|
+
return `sprint-${dateStr}-${seq}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Backup existing sprint state to .sprint-history/.
|
|
59
|
+
* @param {string} projectRoot - Project root directory
|
|
60
|
+
* @param {object} state - Current sprint state
|
|
61
|
+
*/
|
|
62
|
+
function backupSprintState(projectRoot, state) {
|
|
63
|
+
const historyDir = path.join(projectRoot, '.sprint-history');
|
|
64
|
+
if (!fs.existsSync(historyDir)) {
|
|
65
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
68
|
+
const backupFile = path.join(historyDir, `sprint-state-${state.id || 'unknown'}-${timestamp}.json`);
|
|
69
|
+
fs.writeFileSync(backupFile, JSON.stringify(state, null, 2), 'utf8');
|
|
70
|
+
return backupFile;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* CLI entry point for sprint-init command.
|
|
75
|
+
* @param {string[]} args - CLI arguments
|
|
76
|
+
* @returns {Promise<number>} Exit code
|
|
77
|
+
*/
|
|
78
|
+
async function handleSprintInit(args = []) {
|
|
79
|
+
if (args.includes('--help')) {
|
|
80
|
+
console.log('Usage: xp-gate sprint-init "<task_description>" [options]');
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log('Arguments:');
|
|
83
|
+
console.log(' task_description Sprint task description (required)');
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log('Options:');
|
|
86
|
+
console.log(' --issues <list> Comma-separated issue numbers (e.g. "#123,#456")');
|
|
87
|
+
console.log(' --force Override existing active sprint (backs up old state)');
|
|
88
|
+
console.log(' --dry-run Pre-check conflicts without writing');
|
|
89
|
+
console.log(' --dir <path> Project directory (default: cwd)');
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log('Examples:');
|
|
92
|
+
console.log(' xp-gate sprint-init "Implement user auth"');
|
|
93
|
+
console.log(' xp-gate sprint-init "Fix #366" --issues "#366" --force');
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Parse arguments
|
|
98
|
+
const forceFlag = args.includes('--force');
|
|
99
|
+
const dryRunFlag = args.includes('--dry-run');
|
|
100
|
+
const dirIdx = args.indexOf('--dir');
|
|
101
|
+
const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
|
|
102
|
+
const issuesIdx = args.indexOf('--issues');
|
|
103
|
+
const issues = issuesIdx >= 0 ? args[issuesIdx + 1] : '';
|
|
104
|
+
|
|
105
|
+
// First non-flag argument is task_description
|
|
106
|
+
const taskDescription = args.find(a => !a.startsWith('--') && a !== projectDir && a !== issues);
|
|
107
|
+
if (!taskDescription) {
|
|
108
|
+
console.error('Error: Missing task_description. Usage: xp-gate sprint-init "<task_description>"');
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const manager = new SprintStateManager(projectDir);
|
|
113
|
+
const branch = detectBranch(projectDir);
|
|
114
|
+
|
|
115
|
+
// Check existing sprint state
|
|
116
|
+
let existingState = null;
|
|
117
|
+
try {
|
|
118
|
+
existingState = manager.read();
|
|
119
|
+
} catch {
|
|
120
|
+
// Corrupted state — treat as no state (will be overwritten)
|
|
121
|
+
existingState = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (existingState) {
|
|
125
|
+
// Idempotent check: same task_description + branch → output dashboard, exit 0
|
|
126
|
+
const normalizedExisting = (existingState.task_description || '').trim();
|
|
127
|
+
const normalizedNew = taskDescription.trim();
|
|
128
|
+
const existingBranch = existingState.isolation?.branch || '';
|
|
129
|
+
|
|
130
|
+
if (normalizedExisting === normalizedNew && existingBranch === branch && branch !== 'detached') {
|
|
131
|
+
console.log('ℹ️ Sprint already initialized (idempotent call). Current state:');
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(renderDashboard(existingState));
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Conflict: existing active sprint
|
|
138
|
+
if (!forceFlag) {
|
|
139
|
+
if (dryRunFlag) {
|
|
140
|
+
console.log(JSON.stringify({
|
|
141
|
+
decision: 'deny',
|
|
142
|
+
reason: `Active sprint "${existingState.id}" exists (task: "${normalizedExisting}")`,
|
|
143
|
+
hint: 'Use --force to override (backs up old state)',
|
|
144
|
+
}));
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
console.error(`Error: Active sprint "${existingState.id}" already exists.`);
|
|
148
|
+
console.error(` Task: "${normalizedExisting}"`);
|
|
149
|
+
console.error(` Branch: ${existingBranch}`);
|
|
150
|
+
console.error(' Use --force to override (backs up old state to .sprint-history/)');
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --force: backup and override
|
|
155
|
+
const backupPath = backupSprintState(projectDir, existingState);
|
|
156
|
+
console.log(`📦 Backed up existing sprint to: ${backupPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (dryRunFlag) {
|
|
160
|
+
console.log(JSON.stringify({
|
|
161
|
+
decision: 'allow',
|
|
162
|
+
reason: existingState ? 'Would override existing sprint (--force)' : 'No existing sprint',
|
|
163
|
+
branch,
|
|
164
|
+
task_description: taskDescription.trim(),
|
|
165
|
+
}));
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Create new sprint state
|
|
170
|
+
const sprintId = generateSprintId();
|
|
171
|
+
const newState = {
|
|
172
|
+
_schema_version: 1,
|
|
173
|
+
id: sprintId,
|
|
174
|
+
task_description: taskDescription.trim(),
|
|
175
|
+
phase: 1,
|
|
176
|
+
status: 'in_progress',
|
|
177
|
+
started_at: new Date().toISOString(),
|
|
178
|
+
phase_history: [
|
|
179
|
+
{
|
|
180
|
+
phase: 1,
|
|
181
|
+
name: 'PREP',
|
|
182
|
+
status: 'in_progress',
|
|
183
|
+
started_at: new Date().toISOString(),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
isolation: {
|
|
187
|
+
worktree_path: projectDir,
|
|
188
|
+
branch,
|
|
189
|
+
},
|
|
190
|
+
outputs: issues ? { issues } : {},
|
|
191
|
+
metrics: {},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Write state atomically
|
|
195
|
+
manager.write(newState);
|
|
196
|
+
|
|
197
|
+
console.log(`✅ Sprint initialized: ${sprintId}`);
|
|
198
|
+
console.log(` Task: ${taskDescription.trim()}`);
|
|
199
|
+
console.log(` Branch: ${branch}`);
|
|
200
|
+
if (issues) console.log(` Issues: ${issues}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(renderDashboard(newState));
|
|
203
|
+
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { handleSprintInit, detectBranch, generateSprintId, backupSprintState };
|