@boyingliu01/xp-gate 0.15.3 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/go.sh +0 -0
  6. package/adapters/iac.sh +0 -0
  7. package/adapters/java.sh +0 -0
  8. package/adapters/kotlin.sh +0 -0
  9. package/adapters/objectivec.sh +0 -0
  10. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  12. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  14. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  16. package/adapters/powershell.sh +0 -0
  17. package/adapters/python.sh +0 -0
  18. package/adapters/shell.sh +0 -0
  19. package/adapters/swift.sh +0 -0
  20. package/adapters/typescript.sh +0 -0
  21. package/bin/xp-gate.js +1 -1
  22. package/gate-3.sh +0 -0
  23. package/gate-4.sh +0 -0
  24. package/gate-7.sh +0 -0
  25. package/gate-8.sh +0 -0
  26. package/gate-9.sh +0 -0
  27. package/hooks/adapter-common.sh +0 -0
  28. package/hooks/gate-3.sh +0 -0
  29. package/hooks/gate-4.sh +0 -0
  30. package/hooks/gate-7.sh +0 -0
  31. package/hooks/gate-8.sh +0 -0
  32. package/hooks/gate-9.sh +0 -0
  33. package/hooks/pre-commit +9 -0
  34. package/hooks/pre-push +0 -0
  35. package/lib/__tests__/install-cmd.test.js +0 -35
  36. package/lib/__tests__/phase-transition.test.js +272 -272
  37. package/lib/__tests__/sprint-audit.test.js +207 -207
  38. package/lib/doctor-tui.js +198 -0
  39. package/lib/doctor.js +95 -179
  40. package/lib/init.js +6 -4
  41. package/lib/install-cmd.js +31 -42
  42. package/lib/language-tools.js +833 -833
  43. package/lib/phase-transition.js +249 -249
  44. package/lib/sprint-audit.js +235 -235
  45. package/lib/uninstall.js +22 -0
  46. package/mock-policy/AGENTS.md +2 -2
  47. package/mutation/AGENTS.md +2 -2
  48. package/package.json +1 -1
  49. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  50. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  51. package/plugins/claude-code/bin/xp-gate-check +0 -0
  52. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  53. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  54. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +10 -0
  55. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  56. package/plugins/opencode/package.json +1 -1
  57. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  58. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  59. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +10 -0
  60. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  61. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  62. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  63. package/plugins/qoder/agents/delphi-technical.md +48 -48
  64. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  65. package/plugins/qoder/bin/xp-gate-check +0 -0
  66. package/plugins/qoder/plugin.json +1 -1
  67. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  68. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  69. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  70. package/principles/AGENTS.md +2 -2
  71. package/scripts/delphi-external-review.cjs +432 -432
  72. package/skills/delphi-review/AGENTS.md +2 -2
  73. package/skills/sprint-flow/AGENTS.md +2 -2
  74. package/skills/sprint-flow/references/phase-4-verify.md +10 -0
  75. package/skills/test-specification-alignment/AGENTS.md +2 -2
@@ -1,249 +1,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 { SprintStateManager } = require('./sprint-state-manager');
28
-
29
- /**
30
- * Shared phase name lookup — single source of truth.
31
- * Used by renderDashboard, sprint-audit, and Layer 1 gate check.
32
- */
33
- const PHASE_NAMES = {
34
- 1: 'PREP', 2: 'DESIGN', 3: 'BUILD',
35
- 4: 'VERIFY', 5: 'SHIP', 6: 'CLOSE',
36
- };
37
-
38
- /**
39
- * Render ASCII dashboard from sprint state.
40
- * Reuses the formatSprintTable logic from sprint-status.
41
- * @param {object} state - Sprint state object
42
- * @returns {string} Rendered dashboard
43
- */
44
- function renderDashboard(state) {
45
- if (!state) return 'No sprint state found';
46
-
47
- const PHASE_NAMES_LOCAL = PHASE_NAMES;
48
-
49
- function statusIcon(status) {
50
- switch (status) {
51
- case 'completed': return '✅';
52
- case 'in_progress': return '🔄';
53
- case 'skipped': return '⏭️';
54
- case 'failed': return '❌';
55
- case 'paused': return '⏸️';
56
- default: return '⏳';
57
- }
58
- }
59
-
60
- function formatDuration(seconds) {
61
- if (seconds == null || seconds === 0) return '-';
62
- if (seconds < 60) return `${seconds}s`;
63
- const mins = Math.floor(seconds / 60);
64
- if (mins < 60) return `${mins}m`;
65
- const hrs = Math.floor(mins / 60);
66
- if (hrs < 24) return `${hrs}h ${mins % 60}m`;
67
- return `${Math.floor(hrs / 24)}d ${hrs % 24}h`;
68
- }
69
-
70
- const branch = state.isolation?.branch || 'unknown';
71
- const sprintId = state.id || 'unknown';
72
- const taskDesc = state.task_description || state.issue_title || '-';
73
- const overallStatus = state.status || 'running';
74
- const startedAt = state.started_at ? state.started_at.slice(0, 16).replace('T', ' ') : '-';
75
-
76
- // Build phase history lookup
77
- const historyLookup = {};
78
- if (Array.isArray(state.phase_history)) {
79
- for (const ph of state.phase_history) {
80
- historyLookup[String(ph.phase)] = ph;
81
- }
82
- }
83
-
84
- const currentPhase = state.phase || 1;
85
- const currentPhaseName = PHASE_NAMES_LOCAL[String(currentPhase)] || `Phase ${currentPhase}`;
86
-
87
- // Progress bar
88
- const completedCount = Object.values(historyLookup).filter(h => h.status === 'completed').length;
89
- const pct = Math.round((completedCount / 6) * 100);
90
- const filled = '█'.repeat(completedCount);
91
- const current = currentPhase <= 6 ? '▓' : '';
92
- const empty = '░'.repeat(Math.max(0, 6 - completedCount - (current ? 1 : 0)));
93
- const progressBar = `[${filled}${current}${empty}] ${pct}%`;
94
-
95
- // Build lines
96
- const lines = [];
97
- lines.push('+============================================================+');
98
- lines.push(`| SPRINT PROGRESS ${sprintId.padEnd(20)}|`);
99
- lines.push('+============================================================+');
100
- lines.push(`| 需求: ${(taskDesc).slice(0, 54).padEnd(54)}|`);
101
- lines.push(`| 分支: ${(branch).slice(0, 54).padEnd(54)}|`);
102
- lines.push(`| 状态: ${(overallStatus).slice(0, 15).padEnd(15)} 启动: ${startedAt.slice(0, 19).padEnd(19)}|`);
103
- lines.push('+============================================================+');
104
- lines.push('| |');
105
-
106
- for (let i = 1; i <= 6; i++) {
107
- const entry = historyLookup[String(i)];
108
- const icon = statusIcon(entry?.status || (i < currentPhase ? 'completed' : i === currentPhase ? 'in_progress' : 'pending'));
109
- const name = (PHASE_NAMES_LOCAL[String(i)] || `Phase ${i}`).padEnd(10);
110
- const dur = formatDuration(entry?.duration_seconds).padEnd(8);
111
- lines.push(`| ${icon} Phase ${i}/6 ${name} ${dur} |`);
112
- }
113
-
114
- lines.push('| |');
115
- lines.push(`| ${progressBar.padEnd(58)}|`);
116
- lines.push('+============================================================+');
117
- lines.push(`| > 当前: Phase ${currentPhase}/6 ${currentPhaseName}`.padEnd(59) + '|');
118
- lines.push(`| 状态: ${(entry_status(currentPhase, historyLookup)).padEnd(50)}|`);
119
- lines.push('+============================================================+');
120
-
121
- // Outputs section
122
- if (state.outputs && Object.keys(state.outputs).length > 0) {
123
- lines.push('| 输出物: |');
124
- for (const [key, val] of Object.entries(state.outputs)) {
125
- lines.push(`| ${(`${key}: ${val}`).slice(0, 55).padEnd(55)}|`);
126
- }
127
- lines.push('+============================================================+');
128
- }
129
-
130
- return lines.join('\n');
131
-
132
- function entry_status(phaseNum, lookup) {
133
- const e = lookup[String(phaseNum)];
134
- return e?.status || (phaseNum < currentPhase ? 'completed' : phaseNum === currentPhase ? 'in_progress' : 'pending');
135
- }
136
- }
137
-
138
- /**
139
- * CLI entry point for phase-transition command.
140
- * @param {string[]} args - CLI arguments
141
- * @returns {Promise<number>} Exit code
142
- */
143
- async function handlePhaseTransition(args = []) {
144
- if (args.includes('--help')) {
145
- console.log('Usage: xp-gate phase-transition <phase> <status> [options]');
146
- console.log('');
147
- console.log('Arguments:');
148
- console.log(' phase Phase number (1-6)');
149
- console.log(' status Status: in_progress | completed | skipped | failed | paused');
150
- console.log('');
151
- console.log('Options:');
152
- console.log(' --outputs <json> JSON object of outputs to record');
153
- console.log(' --render Render ASCII dashboard after transition');
154
- console.log(' --dir <path> Project directory (default: cwd)');
155
- console.log('');
156
- console.log('Examples:');
157
- console.log(' xp-gate phase-transition 1 completed');
158
- console.log(' xp-gate phase-transition 3 completed --render');
159
- console.log(' xp-gate phase-transition 2 in_progress --outputs \'{"spec":"path.yaml"}\'');
160
- return 0;
161
- }
162
-
163
- if (args.length < 2) {
164
- console.error('Error: Missing required arguments. Usage: xp-gate phase-transition <phase> <status>');
165
- return 1;
166
- }
167
-
168
- const phase = parseInt(args[0], 10);
169
- const status = args[1];
170
- const renderFlag = args.includes('--render');
171
- const dirIdx = args.indexOf('--dir');
172
- const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
173
-
174
- const outputsIdx = args.indexOf('--outputs');
175
- let outputs = {};
176
- if (outputsIdx >= 0 && args[outputsIdx + 1]) {
177
- try {
178
- outputs = JSON.parse(args[outputsIdx + 1]);
179
- } catch (err) {
180
- console.error(`Error: Invalid JSON for --outputs: ${err.message}`);
181
- return 1;
182
- }
183
- }
184
-
185
- // Validate inputs
186
- if (phase < 1 || phase > 6) {
187
- console.error(`Error: Phase must be 1-6, got ${phase}`);
188
- return 1;
189
- }
190
-
191
- const validStatuses = ['in_progress', 'completed', 'skipped', 'failed', 'paused'];
192
- if (!validStatuses.includes(status)) {
193
- console.error(`Error: Status must be one of: ${validStatuses.join(', ')}`);
194
- return 1;
195
- }
196
-
197
- // Execute transition
198
- try {
199
- const manager = new SprintStateManager(projectDir);
200
-
201
- // ── Layer 1: Pre-transition gate check ──
202
- // Check BEFORE transitionPhase() to validate previous phase completion.
203
- // Runs when status === 'in_progress' and phase >= 2.
204
- // No side effects — only outputs WARNING, does not modify state.
205
- if (status === 'in_progress' && phase >= 2) {
206
- const preState = manager.read();
207
- if (preState && Array.isArray(preState.phase_history)) {
208
- const prevPhase = phase - 1;
209
- const prevEntry = preState.phase_history.find(e => e.phase === prevPhase);
210
- const prevName = PHASE_NAMES[prevPhase] || `Phase ${prevPhase}`;
211
- // 'completed' and 'skipped' are both valid predecessor statuses
212
- if (!prevEntry || (prevEntry.status !== 'completed' && prevEntry.status !== 'skipped')) {
213
- console.warn(
214
- `⚠️ [sprint-audit] Phase ${prevPhase} (${prevName}) not recorded as 'completed'`
215
- );
216
- console.warn(
217
- ` Previous phase may have been skipped. Run: xp-gate phase-transition ${prevPhase} completed`
218
- );
219
- }
220
- }
221
- // If no sprint-state.json exists yet, skip check (no previous phase to validate)
222
- }
223
-
224
- const newState = manager.transitionPhase(phase, status, { outputs });
225
-
226
- console.log(`✅ Phase ${phase} transitioned to '${status}'`);
227
-
228
- // ── Layer 1.5: Auto-trigger Layer 2 reminder on Phase 6 completed ──
229
- if (phase === 6 && status === 'completed') {
230
- console.log('');
231
- console.log('📋 [sprint-audit] Sprint complete — run final coverage audit:');
232
- console.log(' npx xp-gate sprint-audit');
233
- }
234
-
235
- // Auto-render dashboard if --render flag is set
236
- if (renderFlag) {
237
- console.log('');
238
- const dashboard = renderDashboard(newState);
239
- console.log(dashboard);
240
- }
241
-
242
- return 0;
243
- } catch (err) {
244
- console.error(`Error: Phase transition failed: ${err.message}`);
245
- return 1;
246
- }
247
- }
248
-
249
- module.exports = { handlePhaseTransition, renderDashboard, PHASE_NAMES };
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 { SprintStateManager } = require('./sprint-state-manager');
28
+
29
+ /**
30
+ * Shared phase name lookup — single source of truth.
31
+ * Used by renderDashboard, sprint-audit, and Layer 1 gate check.
32
+ */
33
+ const PHASE_NAMES = {
34
+ 1: 'PREP', 2: 'DESIGN', 3: 'BUILD',
35
+ 4: 'VERIFY', 5: 'SHIP', 6: 'CLOSE',
36
+ };
37
+
38
+ /**
39
+ * Render ASCII dashboard from sprint state.
40
+ * Reuses the formatSprintTable logic from sprint-status.
41
+ * @param {object} state - Sprint state object
42
+ * @returns {string} Rendered dashboard
43
+ */
44
+ function renderDashboard(state) {
45
+ if (!state) return 'No sprint state found';
46
+
47
+ const PHASE_NAMES_LOCAL = PHASE_NAMES;
48
+
49
+ function statusIcon(status) {
50
+ switch (status) {
51
+ case 'completed': return '✅';
52
+ case 'in_progress': return '🔄';
53
+ case 'skipped': return '⏭️';
54
+ case 'failed': return '❌';
55
+ case 'paused': return '⏸️';
56
+ default: return '⏳';
57
+ }
58
+ }
59
+
60
+ function formatDuration(seconds) {
61
+ if (seconds == null || seconds === 0) return '-';
62
+ if (seconds < 60) return `${seconds}s`;
63
+ const mins = Math.floor(seconds / 60);
64
+ if (mins < 60) return `${mins}m`;
65
+ const hrs = Math.floor(mins / 60);
66
+ if (hrs < 24) return `${hrs}h ${mins % 60}m`;
67
+ return `${Math.floor(hrs / 24)}d ${hrs % 24}h`;
68
+ }
69
+
70
+ const branch = state.isolation?.branch || 'unknown';
71
+ const sprintId = state.id || 'unknown';
72
+ const taskDesc = state.task_description || state.issue_title || '-';
73
+ const overallStatus = state.status || 'running';
74
+ const startedAt = state.started_at ? state.started_at.slice(0, 16).replace('T', ' ') : '-';
75
+
76
+ // Build phase history lookup
77
+ const historyLookup = {};
78
+ if (Array.isArray(state.phase_history)) {
79
+ for (const ph of state.phase_history) {
80
+ historyLookup[String(ph.phase)] = ph;
81
+ }
82
+ }
83
+
84
+ const currentPhase = state.phase || 1;
85
+ const currentPhaseName = PHASE_NAMES_LOCAL[String(currentPhase)] || `Phase ${currentPhase}`;
86
+
87
+ // Progress bar
88
+ const completedCount = Object.values(historyLookup).filter(h => h.status === 'completed').length;
89
+ const pct = Math.round((completedCount / 6) * 100);
90
+ const filled = '█'.repeat(completedCount);
91
+ const current = currentPhase <= 6 ? '▓' : '';
92
+ const empty = '░'.repeat(Math.max(0, 6 - completedCount - (current ? 1 : 0)));
93
+ const progressBar = `[${filled}${current}${empty}] ${pct}%`;
94
+
95
+ // Build lines
96
+ const lines = [];
97
+ lines.push('+============================================================+');
98
+ lines.push(`| SPRINT PROGRESS ${sprintId.padEnd(20)}|`);
99
+ lines.push('+============================================================+');
100
+ lines.push(`| 需求: ${(taskDesc).slice(0, 54).padEnd(54)}|`);
101
+ lines.push(`| 分支: ${(branch).slice(0, 54).padEnd(54)}|`);
102
+ lines.push(`| 状态: ${(overallStatus).slice(0, 15).padEnd(15)} 启动: ${startedAt.slice(0, 19).padEnd(19)}|`);
103
+ lines.push('+============================================================+');
104
+ lines.push('| |');
105
+
106
+ for (let i = 1; i <= 6; i++) {
107
+ const entry = historyLookup[String(i)];
108
+ const icon = statusIcon(entry?.status || (i < currentPhase ? 'completed' : i === currentPhase ? 'in_progress' : 'pending'));
109
+ const name = (PHASE_NAMES_LOCAL[String(i)] || `Phase ${i}`).padEnd(10);
110
+ const dur = formatDuration(entry?.duration_seconds).padEnd(8);
111
+ lines.push(`| ${icon} Phase ${i}/6 ${name} ${dur} |`);
112
+ }
113
+
114
+ lines.push('| |');
115
+ lines.push(`| ${progressBar.padEnd(58)}|`);
116
+ lines.push('+============================================================+');
117
+ lines.push(`| > 当前: Phase ${currentPhase}/6 ${currentPhaseName}`.padEnd(59) + '|');
118
+ lines.push(`| 状态: ${(entry_status(currentPhase, historyLookup)).padEnd(50)}|`);
119
+ lines.push('+============================================================+');
120
+
121
+ // Outputs section
122
+ if (state.outputs && Object.keys(state.outputs).length > 0) {
123
+ lines.push('| 输出物: |');
124
+ for (const [key, val] of Object.entries(state.outputs)) {
125
+ lines.push(`| ${(`${key}: ${val}`).slice(0, 55).padEnd(55)}|`);
126
+ }
127
+ lines.push('+============================================================+');
128
+ }
129
+
130
+ return lines.join('\n');
131
+
132
+ function entry_status(phaseNum, lookup) {
133
+ const e = lookup[String(phaseNum)];
134
+ return e?.status || (phaseNum < currentPhase ? 'completed' : phaseNum === currentPhase ? 'in_progress' : 'pending');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * CLI entry point for phase-transition command.
140
+ * @param {string[]} args - CLI arguments
141
+ * @returns {Promise<number>} Exit code
142
+ */
143
+ async function handlePhaseTransition(args = []) {
144
+ if (args.includes('--help')) {
145
+ console.log('Usage: xp-gate phase-transition <phase> <status> [options]');
146
+ console.log('');
147
+ console.log('Arguments:');
148
+ console.log(' phase Phase number (1-6)');
149
+ console.log(' status Status: in_progress | completed | skipped | failed | paused');
150
+ console.log('');
151
+ console.log('Options:');
152
+ console.log(' --outputs <json> JSON object of outputs to record');
153
+ console.log(' --render Render ASCII dashboard after transition');
154
+ console.log(' --dir <path> Project directory (default: cwd)');
155
+ console.log('');
156
+ console.log('Examples:');
157
+ console.log(' xp-gate phase-transition 1 completed');
158
+ console.log(' xp-gate phase-transition 3 completed --render');
159
+ console.log(' xp-gate phase-transition 2 in_progress --outputs \'{"spec":"path.yaml"}\'');
160
+ return 0;
161
+ }
162
+
163
+ if (args.length < 2) {
164
+ console.error('Error: Missing required arguments. Usage: xp-gate phase-transition <phase> <status>');
165
+ return 1;
166
+ }
167
+
168
+ const phase = parseInt(args[0], 10);
169
+ const status = args[1];
170
+ const renderFlag = args.includes('--render');
171
+ const dirIdx = args.indexOf('--dir');
172
+ const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
173
+
174
+ const outputsIdx = args.indexOf('--outputs');
175
+ let outputs = {};
176
+ if (outputsIdx >= 0 && args[outputsIdx + 1]) {
177
+ try {
178
+ outputs = JSON.parse(args[outputsIdx + 1]);
179
+ } catch (err) {
180
+ console.error(`Error: Invalid JSON for --outputs: ${err.message}`);
181
+ return 1;
182
+ }
183
+ }
184
+
185
+ // Validate inputs
186
+ if (phase < 1 || phase > 6) {
187
+ console.error(`Error: Phase must be 1-6, got ${phase}`);
188
+ return 1;
189
+ }
190
+
191
+ const validStatuses = ['in_progress', 'completed', 'skipped', 'failed', 'paused'];
192
+ if (!validStatuses.includes(status)) {
193
+ console.error(`Error: Status must be one of: ${validStatuses.join(', ')}`);
194
+ return 1;
195
+ }
196
+
197
+ // Execute transition
198
+ try {
199
+ const manager = new SprintStateManager(projectDir);
200
+
201
+ // ── Layer 1: Pre-transition gate check ──
202
+ // Check BEFORE transitionPhase() to validate previous phase completion.
203
+ // Runs when status === 'in_progress' and phase >= 2.
204
+ // No side effects — only outputs WARNING, does not modify state.
205
+ if (status === 'in_progress' && phase >= 2) {
206
+ const preState = manager.read();
207
+ if (preState && Array.isArray(preState.phase_history)) {
208
+ const prevPhase = phase - 1;
209
+ const prevEntry = preState.phase_history.find(e => e.phase === prevPhase);
210
+ const prevName = PHASE_NAMES[prevPhase] || `Phase ${prevPhase}`;
211
+ // 'completed' and 'skipped' are both valid predecessor statuses
212
+ if (!prevEntry || (prevEntry.status !== 'completed' && prevEntry.status !== 'skipped')) {
213
+ console.warn(
214
+ `⚠️ [sprint-audit] Phase ${prevPhase} (${prevName}) not recorded as 'completed'`
215
+ );
216
+ console.warn(
217
+ ` Previous phase may have been skipped. Run: xp-gate phase-transition ${prevPhase} completed`
218
+ );
219
+ }
220
+ }
221
+ // If no sprint-state.json exists yet, skip check (no previous phase to validate)
222
+ }
223
+
224
+ const newState = manager.transitionPhase(phase, status, { outputs });
225
+
226
+ console.log(`✅ Phase ${phase} transitioned to '${status}'`);
227
+
228
+ // ── Layer 1.5: Auto-trigger Layer 2 reminder on Phase 6 completed ──
229
+ if (phase === 6 && status === 'completed') {
230
+ console.log('');
231
+ console.log('📋 [sprint-audit] Sprint complete — run final coverage audit:');
232
+ console.log(' npx xp-gate sprint-audit');
233
+ }
234
+
235
+ // Auto-render dashboard if --render flag is set
236
+ if (renderFlag) {
237
+ console.log('');
238
+ const dashboard = renderDashboard(newState);
239
+ console.log(dashboard);
240
+ }
241
+
242
+ return 0;
243
+ } catch (err) {
244
+ console.error(`Error: Phase transition failed: ${err.message}`);
245
+ return 1;
246
+ }
247
+ }
248
+
249
+ module.exports = { handlePhaseTransition, renderDashboard, PHASE_NAMES };