@boyingliu01/xp-gate 0.15.5 → 0.18.1
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/phase-transition.js
CHANGED
|
@@ -1,249 +1,499 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* xp-gate phase-transition — Programmatic sprint phase transition
|
|
3
|
-
*
|
|
4
|
-
* Resolves #338 (dashboard never auto-renders) and #146 (sprint-state enforcement).
|
|
5
|
-
*
|
|
6
|
-
* This CLI command is the programmatic entry point for sprint phase transitions.
|
|
7
|
-
* The sprint-flow SKILL.md MUST instruct the orchestrator to call this command
|
|
8
|
-
* after each phase completion, replacing text-level MUST instructions with
|
|
9
|
-
* a concrete CLI invocation.
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* xp-gate phase-transition <phase> <status> [--outputs <json>] [--render]
|
|
13
|
-
*
|
|
14
|
-
* Examples:
|
|
15
|
-
* xp-gate phase-transition 1 completed
|
|
16
|
-
* xp-gate phase-transition 2 in_progress
|
|
17
|
-
* xp-gate phase-transition 3 completed --outputs '{"spec":"path/to/spec.yaml"}'
|
|
18
|
-
* xp-gate phase-transition 3 completed --render (auto-render dashboard)
|
|
19
|
-
*
|
|
20
|
-
* @module phase-transition
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
'use strict';
|
|
24
|
-
|
|
25
|
-
const path = require('path');
|
|
26
|
-
const fs = require('fs');
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1
|
+
/**
|
|
2
|
+
* xp-gate phase-transition — Programmatic sprint phase transition
|
|
3
|
+
*
|
|
4
|
+
* Resolves #338 (dashboard never auto-renders) and #146 (sprint-state enforcement).
|
|
5
|
+
*
|
|
6
|
+
* This CLI command is the programmatic entry point for sprint phase transitions.
|
|
7
|
+
* The sprint-flow SKILL.md MUST instruct the orchestrator to call this command
|
|
8
|
+
* after each phase completion, replacing text-level MUST instructions with
|
|
9
|
+
* a concrete CLI invocation.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* xp-gate phase-transition <phase> <status> [--outputs <json>] [--render]
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* xp-gate phase-transition 1 completed
|
|
16
|
+
* xp-gate phase-transition 2 in_progress
|
|
17
|
+
* xp-gate phase-transition 3 completed --outputs '{"spec":"path/to/spec.yaml"}'
|
|
18
|
+
* xp-gate phase-transition 3 completed --render (auto-render dashboard)
|
|
19
|
+
*
|
|
20
|
+
* @module phase-transition
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const { execSync } = require('child_process');
|
|
29
|
+
const { SprintStateManager } = require('./sprint-state-manager');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared phase name lookup — single source of truth.
|
|
33
|
+
* Used by renderDashboard, sprint-audit, and Layer 1 gate check.
|
|
34
|
+
*/
|
|
35
|
+
const PHASE_NAMES = {
|
|
36
|
+
1: 'PREP', 2: 'DESIGN', 3: 'BUILD',
|
|
37
|
+
4: 'VERIFY', 5: 'SHIP', 6: 'CLOSE',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Phase-specific evidence file requirements.
|
|
42
|
+
* Each entry defines the evidence file path, required fields, and a blocking check.
|
|
43
|
+
* Phases NOT in this map require no evidence validation in v0.17.1.
|
|
44
|
+
*/
|
|
45
|
+
const EVIDENCE_FILES = {
|
|
46
|
+
2: {
|
|
47
|
+
path: '.sprint-state/phase-outputs/requirements-reviewed.json',
|
|
48
|
+
requiredFields: ['verdict', 'requirements_hash'],
|
|
49
|
+
blockingCheck: (data) => data.verdict === 'APPROVED',
|
|
50
|
+
blockingMessage: 'Requirements review verdict is not APPROVED',
|
|
51
|
+
},
|
|
52
|
+
4: {
|
|
53
|
+
path: '.sprint-state/phase-outputs/test-alignment-report.json',
|
|
54
|
+
requiredFields: ['alignment_status', 'head_commit', 'spec_hash'],
|
|
55
|
+
blockingCheck: (data) => data.alignment_status === 'PASS',
|
|
56
|
+
blockingMessage: 'Test alignment status is not PASS',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get current HEAD commit hash from a project directory.
|
|
62
|
+
* Returns 'unknown' if not in a git repo.
|
|
63
|
+
* @param {string} projectDir
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function getCurrentHeadCommit(projectDir) {
|
|
67
|
+
try {
|
|
68
|
+
return execSync('git rev-parse HEAD', {
|
|
69
|
+
cwd: projectDir,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
|
+
}).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute SHA-256 hash of a file's contents.
|
|
80
|
+
* @param {string} filePath
|
|
81
|
+
* @returns {string} hex digest
|
|
82
|
+
*/
|
|
83
|
+
function computeFileHash(filePath) {
|
|
84
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate phase evidence files for a given phase transition.
|
|
90
|
+
*
|
|
91
|
+
* Returns { ok: boolean, errors: string[], warnings: string[] }.
|
|
92
|
+
* - For phases not in EVIDENCE_FILES: ok=true, no errors.
|
|
93
|
+
* - For phases with evidence requirements:
|
|
94
|
+
* - If evidence_schema_version >= 2 (new sprint): missing/invalid evidence → ok=false (BLOCK)
|
|
95
|
+
* - If evidence_schema_version missing or < 2 (legacy sprint): missing/invalid → ok=true with WARNING
|
|
96
|
+
*
|
|
97
|
+
* @param {number} phase - Phase number (1-6)
|
|
98
|
+
* @param {string} projectDir - Project root directory
|
|
99
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
100
|
+
*/
|
|
101
|
+
function validateEvidence(phase, projectDir) {
|
|
102
|
+
const evidenceConfig = EVIDENCE_FILES[phase];
|
|
103
|
+
if (!evidenceConfig) {
|
|
104
|
+
return { ok: true, errors: [], warnings: [] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Read sprint state to determine evidence_schema_version
|
|
108
|
+
let evidenceSchemaVersion = 0;
|
|
109
|
+
try {
|
|
110
|
+
const manager = new SprintStateManager(projectDir);
|
|
111
|
+
const state = manager.read();
|
|
112
|
+
if (state && typeof state.evidence_schema_version === 'number') {
|
|
113
|
+
evidenceSchemaVersion = state.evidence_schema_version;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// If state can't be read, treat as legacy (version 0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isLegacySprint = evidenceSchemaVersion < 2;
|
|
120
|
+
const errors = [];
|
|
121
|
+
const warnings = [];
|
|
122
|
+
|
|
123
|
+
const evidenceFilePath = path.join(projectDir, evidenceConfig.path);
|
|
124
|
+
const fileName = path.basename(evidenceConfig.path);
|
|
125
|
+
|
|
126
|
+
// Check file existence
|
|
127
|
+
if (!fs.existsSync(evidenceFilePath)) {
|
|
128
|
+
const msg = `Evidence file missing: ${evidenceConfig.path}`;
|
|
129
|
+
if (isLegacySprint) {
|
|
130
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
131
|
+
return { ok: true, errors: [], warnings };
|
|
132
|
+
}
|
|
133
|
+
errors.push(msg);
|
|
134
|
+
return { ok: false, errors, warnings };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse JSON
|
|
138
|
+
let data;
|
|
139
|
+
try {
|
|
140
|
+
const raw = fs.readFileSync(evidenceFilePath, 'utf8');
|
|
141
|
+
data = JSON.parse(raw);
|
|
142
|
+
} catch {
|
|
143
|
+
const msg = `${fileName} is malformed JSON`;
|
|
144
|
+
if (isLegacySprint) {
|
|
145
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
146
|
+
return { ok: true, errors: [], warnings };
|
|
147
|
+
}
|
|
148
|
+
errors.push(msg);
|
|
149
|
+
return { ok: false, errors, warnings };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check required fields
|
|
153
|
+
for (const field of evidenceConfig.requiredFields) {
|
|
154
|
+
if (!(field in data)) {
|
|
155
|
+
const msg = `${fileName} is missing required field: ${field}`;
|
|
156
|
+
if (isLegacySprint) {
|
|
157
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
158
|
+
} else {
|
|
159
|
+
errors.push(msg);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// If any required fields missing for new sprint, block early
|
|
165
|
+
if (errors.length > 0) {
|
|
166
|
+
return { ok: false, errors, warnings };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Run blocking check
|
|
170
|
+
if (!evidenceConfig.blockingCheck(data)) {
|
|
171
|
+
const msg = `${fileName}: ${evidenceConfig.blockingMessage} (got: ${JSON.stringify(data[evidenceConfig.requiredFields[0]])})`;
|
|
172
|
+
if (isLegacySprint) {
|
|
173
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
174
|
+
return { ok: true, errors: [], warnings };
|
|
175
|
+
}
|
|
176
|
+
errors.push(msg);
|
|
177
|
+
return { ok: false, errors, warnings };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Phase-specific anti-staleness checks
|
|
181
|
+
if (phase === 4) {
|
|
182
|
+
// head_commit check
|
|
183
|
+
const currentHead = getCurrentHeadCommit(projectDir);
|
|
184
|
+
if (data.head_commit !== currentHead) {
|
|
185
|
+
const msg = `${fileName}: head_commit mismatch — report has "${data.head_commit}", current HEAD is "${currentHead}"`;
|
|
186
|
+
if (isLegacySprint) {
|
|
187
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
188
|
+
} else {
|
|
189
|
+
errors.push(msg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// spec_hash check (only if specification.yaml exists)
|
|
194
|
+
const specPath = path.join(projectDir, 'specification.yaml');
|
|
195
|
+
if (fs.existsSync(specPath)) {
|
|
196
|
+
const expectedHash = computeFileHash(specPath);
|
|
197
|
+
if (data.spec_hash !== expectedHash) {
|
|
198
|
+
const msg = `${fileName}: spec_hash mismatch — specification.yaml has changed since report was generated`;
|
|
199
|
+
if (isLegacySprint) {
|
|
200
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
201
|
+
} else {
|
|
202
|
+
errors.push(msg);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Phase 2: requirements_hash existence check (full verification in v0.18.0)
|
|
209
|
+
if (phase === 2) {
|
|
210
|
+
if (typeof data.requirements_hash !== 'string' || data.requirements_hash.length === 0) {
|
|
211
|
+
const msg = `${fileName}: requirements_hash must be a non-empty string`;
|
|
212
|
+
if (isLegacySprint) {
|
|
213
|
+
warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
|
|
214
|
+
} else {
|
|
215
|
+
errors.push(msg);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (errors.length > 0) {
|
|
221
|
+
return { ok: false, errors, warnings };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { ok: true, errors: [], warnings };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Append an evidence-skipped audit entry to .xp-gate/audit.jsonl.
|
|
229
|
+
* @param {string} projectDir
|
|
230
|
+
* @param {number} phase
|
|
231
|
+
* @param {string} reason
|
|
232
|
+
*/
|
|
233
|
+
function logEvidenceSkip(projectDir, phase, reason) {
|
|
234
|
+
const auditDir = path.join(projectDir, '.xp-gate');
|
|
235
|
+
if (!fs.existsSync(auditDir)) {
|
|
236
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
const entry = {
|
|
239
|
+
timestamp: new Date().toISOString(),
|
|
240
|
+
event: 'evidence_skipped',
|
|
241
|
+
phase,
|
|
242
|
+
reason,
|
|
243
|
+
commit_hash: getCurrentHeadCommit(projectDir),
|
|
244
|
+
};
|
|
245
|
+
fs.appendFileSync(path.join(auditDir, 'audit.jsonl'), JSON.stringify(entry) + '\n', 'utf8');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Render ASCII dashboard from sprint state.
|
|
250
|
+
* Reuses the formatSprintTable logic from sprint-status.
|
|
251
|
+
* @param {object} state - Sprint state object
|
|
252
|
+
* @returns {string} Rendered dashboard
|
|
253
|
+
*/
|
|
254
|
+
function renderDashboard(state) {
|
|
255
|
+
if (!state) return 'No sprint state found';
|
|
256
|
+
|
|
257
|
+
const PHASE_NAMES_LOCAL = PHASE_NAMES;
|
|
258
|
+
|
|
259
|
+
function statusIcon(status) {
|
|
260
|
+
switch (status) {
|
|
261
|
+
case 'completed': return '✅';
|
|
262
|
+
case 'in_progress': return '🔄';
|
|
263
|
+
case 'skipped': return '⏭️';
|
|
264
|
+
case 'failed': return '❌';
|
|
265
|
+
case 'paused': return '⏸️';
|
|
266
|
+
default: return '⏳';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatDuration(seconds) {
|
|
271
|
+
if (seconds == null || seconds === 0) return '-';
|
|
272
|
+
if (seconds < 60) return `${seconds}s`;
|
|
273
|
+
const mins = Math.floor(seconds / 60);
|
|
274
|
+
if (mins < 60) return `${mins}m`;
|
|
275
|
+
const hrs = Math.floor(mins / 60);
|
|
276
|
+
if (hrs < 24) return `${hrs}h ${mins % 60}m`;
|
|
277
|
+
return `${Math.floor(hrs / 24)}d ${hrs % 24}h`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const branch = state.isolation?.branch || 'unknown';
|
|
281
|
+
const sprintId = state.id || 'unknown';
|
|
282
|
+
const taskDesc = state.task_description || state.issue_title || '-';
|
|
283
|
+
const overallStatus = state.status || 'running';
|
|
284
|
+
const startedAt = state.started_at ? state.started_at.slice(0, 16).replace('T', ' ') : '-';
|
|
285
|
+
|
|
286
|
+
// Build phase history lookup
|
|
287
|
+
const historyLookup = {};
|
|
288
|
+
if (Array.isArray(state.phase_history)) {
|
|
289
|
+
for (const ph of state.phase_history) {
|
|
290
|
+
historyLookup[String(ph.phase)] = ph;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const currentPhase = state.phase || 1;
|
|
295
|
+
const currentPhaseName = PHASE_NAMES_LOCAL[String(currentPhase)] || `Phase ${currentPhase}`;
|
|
296
|
+
|
|
297
|
+
// Progress bar
|
|
298
|
+
const completedCount = Object.values(historyLookup).filter(h => h.status === 'completed').length;
|
|
299
|
+
const pct = Math.round((completedCount / 6) * 100);
|
|
300
|
+
const filled = '█'.repeat(completedCount);
|
|
301
|
+
const current = currentPhase <= 6 ? '▓' : '';
|
|
302
|
+
const empty = '░'.repeat(Math.max(0, 6 - completedCount - (current ? 1 : 0)));
|
|
303
|
+
const progressBar = `[${filled}${current}${empty}] ${pct}%`;
|
|
304
|
+
|
|
305
|
+
// Build lines
|
|
306
|
+
const lines = [];
|
|
307
|
+
lines.push('+============================================================+');
|
|
308
|
+
lines.push(`| SPRINT PROGRESS ${sprintId.padEnd(20)}|`);
|
|
309
|
+
lines.push('+============================================================+');
|
|
310
|
+
lines.push(`| 需求: ${(taskDesc).slice(0, 54).padEnd(54)}|`);
|
|
311
|
+
lines.push(`| 分支: ${(branch).slice(0, 54).padEnd(54)}|`);
|
|
312
|
+
lines.push(`| 状态: ${(overallStatus).slice(0, 15).padEnd(15)} 启动: ${startedAt.slice(0, 19).padEnd(19)}|`);
|
|
313
|
+
lines.push('+============================================================+');
|
|
314
|
+
lines.push('| |');
|
|
315
|
+
|
|
316
|
+
for (let i = 1; i <= 6; i++) {
|
|
317
|
+
const entry = historyLookup[String(i)];
|
|
318
|
+
const icon = statusIcon(entry?.status || (i < currentPhase ? 'completed' : i === currentPhase ? 'in_progress' : 'pending'));
|
|
319
|
+
const name = (PHASE_NAMES_LOCAL[String(i)] || `Phase ${i}`).padEnd(10);
|
|
320
|
+
const dur = formatDuration(entry?.duration_seconds).padEnd(8);
|
|
321
|
+
lines.push(`| ${icon} Phase ${i}/6 ${name} ${dur} |`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
lines.push('| |');
|
|
325
|
+
lines.push(`| ${progressBar.padEnd(58)}|`);
|
|
326
|
+
lines.push('+============================================================+');
|
|
327
|
+
lines.push(`| > 当前: Phase ${currentPhase}/6 ${currentPhaseName}`.padEnd(59) + '|');
|
|
328
|
+
lines.push(`| 状态: ${(entry_status(currentPhase, historyLookup)).padEnd(50)}|`);
|
|
329
|
+
lines.push('+============================================================+');
|
|
330
|
+
|
|
331
|
+
// Outputs section
|
|
332
|
+
if (state.outputs && Object.keys(state.outputs).length > 0) {
|
|
333
|
+
lines.push('| 输出物: |');
|
|
334
|
+
for (const [key, val] of Object.entries(state.outputs)) {
|
|
335
|
+
lines.push(`| ${(`${key}: ${val}`).slice(0, 55).padEnd(55)}|`);
|
|
336
|
+
}
|
|
337
|
+
lines.push('+============================================================+');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return lines.join('\n');
|
|
341
|
+
|
|
342
|
+
function entry_status(phaseNum, lookup) {
|
|
343
|
+
const e = lookup[String(phaseNum)];
|
|
344
|
+
return e?.status || (phaseNum < currentPhase ? 'completed' : phaseNum === currentPhase ? 'in_progress' : 'pending');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* CLI entry point for phase-transition command.
|
|
350
|
+
* @param {string[]} args - CLI arguments
|
|
351
|
+
* @returns {Promise<number>} Exit code
|
|
352
|
+
*/
|
|
353
|
+
async function handlePhaseTransition(args = []) {
|
|
354
|
+
if (args.includes('--help')) {
|
|
355
|
+
console.log('Usage: xp-gate phase-transition <phase> <status> [options]');
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log('Arguments:');
|
|
358
|
+
console.log(' phase Phase number (1-6)');
|
|
359
|
+
console.log(' status Status: in_progress | completed | skipped | failed | paused');
|
|
360
|
+
console.log('');
|
|
361
|
+
console.log('Options:');
|
|
362
|
+
console.log(' --outputs <json> JSON object of outputs to record');
|
|
363
|
+
console.log(' --render Render ASCII dashboard after transition');
|
|
364
|
+
console.log(' --dir <path> Project directory (default: cwd)');
|
|
365
|
+
console.log(' --skip-evidence <reason> Skip evidence validation (requires reason)');
|
|
366
|
+
console.log('');
|
|
367
|
+
console.log('Examples:');
|
|
368
|
+
console.log(' xp-gate phase-transition 1 completed');
|
|
369
|
+
console.log(' xp-gate phase-transition 3 completed --render');
|
|
370
|
+
console.log(' xp-gate phase-transition 2 in_progress --outputs \'{"spec":"path.yaml"}\'');
|
|
371
|
+
console.log(' xp-gate phase-transition 4 completed --skip-evidence "Emergency hotfix"');
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (args.length < 2) {
|
|
376
|
+
console.error('Error: Missing required arguments. Usage: xp-gate phase-transition <phase> <status>');
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const phase = parseInt(args[0], 10);
|
|
381
|
+
const status = args[1];
|
|
382
|
+
const renderFlag = args.includes('--render');
|
|
383
|
+
const dirIdx = args.indexOf('--dir');
|
|
384
|
+
const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
|
|
385
|
+
|
|
386
|
+
const outputsIdx = args.indexOf('--outputs');
|
|
387
|
+
let outputs = {};
|
|
388
|
+
if (outputsIdx >= 0 && args[outputsIdx + 1]) {
|
|
389
|
+
try {
|
|
390
|
+
outputs = JSON.parse(args[outputsIdx + 1]);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(`Error: Invalid JSON for --outputs: ${err.message}`);
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Parse --skip-evidence flag
|
|
398
|
+
const skipEvidenceIdx = args.indexOf('--skip-evidence');
|
|
399
|
+
let skipEvidence = false;
|
|
400
|
+
let skipEvidenceReason = '';
|
|
401
|
+
if (skipEvidenceIdx >= 0) {
|
|
402
|
+
skipEvidence = true;
|
|
403
|
+
const nextArg = args[skipEvidenceIdx + 1];
|
|
404
|
+
// Reason is the next arg if it exists and doesn't start with '--'
|
|
405
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
406
|
+
skipEvidenceReason = nextArg;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Validate inputs
|
|
411
|
+
if (phase < 1 || phase > 6) {
|
|
412
|
+
console.error(`Error: Phase must be 1-6, got ${phase}`);
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const validStatuses = ['in_progress', 'completed', 'skipped', 'failed', 'paused'];
|
|
417
|
+
if (!validStatuses.includes(status)) {
|
|
418
|
+
console.error(`Error: Status must be one of: ${validStatuses.join(', ')}`);
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Execute transition
|
|
423
|
+
try {
|
|
424
|
+
const manager = new SprintStateManager(projectDir);
|
|
425
|
+
|
|
426
|
+
// ── Layer 1: Pre-transition gate check ──
|
|
427
|
+
// Check BEFORE transitionPhase() to validate previous phase completion.
|
|
428
|
+
// Runs when status === 'in_progress' and phase >= 2.
|
|
429
|
+
// No side effects — only outputs WARNING, does not modify state.
|
|
430
|
+
if (status === 'in_progress' && phase >= 2) {
|
|
431
|
+
const preState = manager.read();
|
|
432
|
+
if (preState && Array.isArray(preState.phase_history)) {
|
|
433
|
+
const prevPhase = phase - 1;
|
|
434
|
+
const prevEntry = preState.phase_history.find(e => e.phase === prevPhase);
|
|
435
|
+
const prevName = PHASE_NAMES[prevPhase] || `Phase ${prevPhase}`;
|
|
436
|
+
// 'completed' and 'skipped' are both valid predecessor statuses
|
|
437
|
+
if (!prevEntry || (prevEntry.status !== 'completed' && prevEntry.status !== 'skipped')) {
|
|
438
|
+
console.warn(
|
|
439
|
+
`⚠️ [sprint-audit] Phase ${prevPhase} (${prevName}) not recorded as 'completed'`
|
|
440
|
+
);
|
|
441
|
+
console.warn(
|
|
442
|
+
` Previous phase may have been skipped. Run: xp-gate phase-transition ${prevPhase} completed`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// If no sprint-state.json exists yet, skip check (no previous phase to validate)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Layer 2: Evidence validation (blocks on status === 'completed') ──
|
|
450
|
+
if (status === 'completed' && EVIDENCE_FILES[phase]) {
|
|
451
|
+
if (skipEvidence) {
|
|
452
|
+
if (!skipEvidenceReason) {
|
|
453
|
+
console.error('Error: --skip-evidence requires --reason "<text>" to be provided');
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
logEvidenceSkip(projectDir, phase, skipEvidenceReason);
|
|
457
|
+
console.warn('⚠️ Evidence skipped. This will appear in retro reports. Single-sprint limit: >2 skips per sprint will trigger an alert.');
|
|
458
|
+
} else {
|
|
459
|
+
const evidence = validateEvidence(phase, projectDir);
|
|
460
|
+
for (const w of evidence.warnings) {
|
|
461
|
+
console.warn(`⚠️ [sprint-audit] ${w}`);
|
|
462
|
+
}
|
|
463
|
+
if (!evidence.ok) {
|
|
464
|
+
for (const e of evidence.errors) {
|
|
465
|
+
console.error(`❌ [sprint-audit] ${e}`);
|
|
466
|
+
}
|
|
467
|
+
console.error(` Phase ${phase} (${PHASE_NAMES[phase]}) cannot transition to 'completed' without valid evidence.`);
|
|
468
|
+
console.error(` Use --skip-evidence "<reason>" to bypass (audited).`);
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const newState = manager.transitionPhase(phase, status, { outputs });
|
|
475
|
+
|
|
476
|
+
console.log(`✅ Phase ${phase} transitioned to '${status}'`);
|
|
477
|
+
|
|
478
|
+
// ── Layer 1.5: Auto-trigger Layer 2 reminder on Phase 6 completed ──
|
|
479
|
+
if (phase === 6 && status === 'completed') {
|
|
480
|
+
console.log('');
|
|
481
|
+
console.log('📋 [sprint-audit] Sprint complete — run final coverage audit:');
|
|
482
|
+
console.log(' npx xp-gate sprint-audit');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Auto-render dashboard if --render flag is set
|
|
486
|
+
if (renderFlag) {
|
|
487
|
+
console.log('');
|
|
488
|
+
const dashboard = renderDashboard(newState);
|
|
489
|
+
console.log(dashboard);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return 0;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
console.error(`Error: Phase transition failed: ${err.message}`);
|
|
495
|
+
return 1;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = { handlePhaseTransition, renderDashboard, PHASE_NAMES, validateEvidence };
|