@boyingliu01/xp-gate 0.14.29 → 0.14.30

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 (68) 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 +8 -0
  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 +0 -0
  34. package/hooks/pre-push +0 -0
  35. package/lib/__tests__/phase-transition.test.js +272 -174
  36. package/lib/__tests__/sprint-audit.test.js +207 -0
  37. package/lib/phase-transition.js +249 -211
  38. package/lib/sprint-audit.js +235 -0
  39. package/mock-policy/AGENTS.md +2 -2
  40. package/mutation/AGENTS.md +2 -2
  41. package/package.json +2 -2
  42. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  43. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  44. package/plugins/claude-code/bin/xp-gate-check +0 -0
  45. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  46. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  47. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +10 -0
  48. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  49. package/plugins/opencode/package.json +1 -1
  50. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  51. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  52. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +10 -0
  53. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  54. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  55. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  56. package/plugins/qoder/agents/delphi-technical.md +48 -48
  57. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  58. package/plugins/qoder/bin/xp-gate-check +0 -0
  59. package/plugins/qoder/plugin.json +1 -1
  60. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  61. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  62. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  63. package/principles/AGENTS.md +2 -2
  64. package/scripts/delphi-external-review.cjs +432 -432
  65. package/skills/delphi-review/AGENTS.md +2 -2
  66. package/skills/sprint-flow/AGENTS.md +2 -2
  67. package/skills/sprint-flow/references/orchestration-rules.md +10 -0
  68. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/adapter-common.sh CHANGED
File without changes
package/adapters/cpp.sh CHANGED
File without changes
package/adapters/dart.sh CHANGED
File without changes
File without changes
package/adapters/go.sh CHANGED
File without changes
package/adapters/iac.sh CHANGED
File without changes
package/adapters/java.sh CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/adapters/shell.sh CHANGED
File without changes
package/adapters/swift.sh CHANGED
File without changes
File without changes
package/bin/xp-gate.js CHANGED
@@ -203,6 +203,14 @@ const COMMANDS = {
203
203
  handlePhaseTransition(subargs).then(code => process.exit(code));
204
204
  },
205
205
  usage: 'xp-gate phase-transition <phase> <status> [--render] [--outputs <json>]'
206
+ },
207
+ 'sprint-audit': {
208
+ description: 'Final sprint completeness audit (Phase 6 CLOSE)',
209
+ run: subargs => {
210
+ const { handleSprintAudit } = require('../lib/sprint-audit.js');
211
+ handleSprintAudit(subargs).then(code => process.exit(code));
212
+ },
213
+ usage: 'xp-gate sprint-audit [--dir <path>] [--json]'
206
214
  }
207
215
  };
208
216
 
package/gate-3.sh CHANGED
File without changes
package/gate-4.sh CHANGED
File without changes
package/gate-7.sh CHANGED
File without changes
package/gate-8.sh CHANGED
File without changes
package/gate-9.sh CHANGED
File without changes
File without changes
package/hooks/gate-3.sh CHANGED
File without changes
package/hooks/gate-4.sh CHANGED
File without changes
package/hooks/gate-7.sh CHANGED
File without changes
package/hooks/gate-8.sh CHANGED
File without changes
package/hooks/gate-9.sh CHANGED
File without changes
package/hooks/pre-commit CHANGED
File without changes
package/hooks/pre-push CHANGED
File without changes
@@ -1,174 +1,272 @@
1
- /**
2
- * Tests for phase-transition CLI command
3
- * @test REQ-338 Phase transition CLI + dashboard rendering
4
- * @intent Verify phase-transition command updates state and renders dashboard
5
- * @covers AC-338-01 (CLI transitions phase and writes state)
6
- * @covers AC-338-02 (--render flag outputs ASCII dashboard)
7
- * @covers AC-338-03 (invalid inputs return error exit code)
8
- * @covers AC-338-04 (--outputs flag records outputs in state)
9
- */
10
-
11
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
12
- import fs from 'node:fs';
13
- import path from 'node:path';
14
- import os from 'node:os';
15
- import { handlePhaseTransition, renderDashboard } from '../phase-transition.js';
16
-
17
- describe('phase-transition', () => {
18
- let tmpDir;
19
-
20
- beforeEach(() => {
21
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-transition-test-'));
22
- });
23
-
24
- afterEach(() => {
25
- fs.rmSync(tmpDir, { recursive: true, force: true });
26
- });
27
-
28
- describe('handlePhaseTransition()', () => {
29
- it('returns 0 and shows help with --help', async () => {
30
- const code = await handlePhaseTransition(['--help']);
31
- expect(code).toBe(0);
32
- });
33
-
34
- it('returns 1 for missing arguments', async () => {
35
- const code = await handlePhaseTransition([]);
36
- expect(code).toBe(1);
37
- });
38
-
39
- it('returns 1 for invalid phase number', async () => {
40
- const code = await handlePhaseTransition(['7', 'completed']);
41
- expect(code).toBe(1);
42
- });
43
-
44
- it('returns 1 for invalid status', async () => {
45
- const code = await handlePhaseTransition(['1', 'invalid_status']);
46
- expect(code).toBe(1);
47
- });
48
-
49
- it('transitions phase and writes state file', async () => {
50
- const code = await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
51
- expect(code).toBe(0);
52
-
53
- const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
54
- expect(fs.existsSync(stateFile)).toBe(true);
55
-
56
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
57
- expect(state.phase).toBe(1);
58
- expect(state.phase_history).toHaveLength(1);
59
- expect(state.phase_history[0].status).toBe('completed');
60
- expect(state.phase_history[0].phase_name).toBe('PREP');
61
- });
62
-
63
- it('records outputs when --outputs flag is provided', async () => {
64
- const outputs = JSON.stringify({ spec: 'path/to/spec.yaml' });
65
- const code = await handlePhaseTransition(['2', 'completed', '--outputs', outputs, '--dir', tmpDir]);
66
- expect(code).toBe(0);
67
-
68
- const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
69
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
70
- expect(state.outputs.spec).toBe('path/to/spec.yaml');
71
- });
72
-
73
- it('returns 1 for invalid --outputs JSON', async () => {
74
- const code = await handlePhaseTransition(['1', 'completed', '--outputs', 'not-json', '--dir', tmpDir]);
75
- expect(code).toBe(1);
76
- });
77
-
78
- it('transitions through multiple phases sequentially', async () => {
79
- await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
80
- await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
81
- await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
82
- await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
83
-
84
- const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
85
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
86
-
87
- expect(state.phase).toBe(3);
88
- expect(state.phase_history).toHaveLength(3);
89
- expect(state.phase_history[0].status).toBe('completed');
90
- expect(state.phase_history[1].status).toBe('completed');
91
- expect(state.phase_history[2].status).toBe('in_progress');
92
- });
93
-
94
- it('updates existing entry when same phase is transitioned again', async () => {
95
- await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
96
- await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
97
-
98
- const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
99
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
100
-
101
- // Should NOT have duplicate entries
102
- expect(state.phase_history).toHaveLength(1);
103
- expect(state.phase_history[0].status).toBe('completed');
104
- expect(state.phase_history[0].completed_at).toBeDefined();
105
- });
106
- });
107
-
108
- describe('renderDashboard()', () => {
109
- it('returns "No sprint state found" for null state', () => {
110
- const result = renderDashboard(null);
111
- expect(result).toBe('No sprint state found');
112
- });
113
-
114
- it('renders dashboard with all 6 phases', () => {
115
- const state = {
116
- id: 'sprint-test-123',
117
- task_description: 'Test feature',
118
- phase: 3,
119
- status: 'in_progress',
120
- started_at: '2026-07-21T10:00:00Z',
121
- phase_history: [
122
- { phase: 1, phase_name: 'PREP', status: 'completed', duration_seconds: 120 },
123
- { phase: 2, phase_name: 'DESIGN', status: 'completed', duration_seconds: 300 },
124
- { phase: 3, phase_name: 'BUILD', status: 'in_progress' },
125
- ],
126
- isolation: { branch: 'sprint/test-branch' },
127
- outputs: {},
128
- };
129
-
130
- const result = renderDashboard(state);
131
-
132
- expect(result).toContain('SPRINT PROGRESS');
133
- expect(result).toContain('sprint-test-123');
134
- expect(result).toContain('Test feature');
135
- expect(result).toContain('sprint/test-branch');
136
- expect(result).toContain('Phase 1/6');
137
- expect(result).toContain('Phase 6/6');
138
- expect(result).toContain('✅'); // completed phases
139
- expect(result).toContain('🔄'); // in_progress phase
140
- expect(result).toContain('⏳'); // pending phases
141
- });
142
-
143
- it('shows outputs section when outputs exist', () => {
144
- const state = {
145
- id: 'sprint-out',
146
- task_description: 'Feature',
147
- phase: 2,
148
- status: 'in_progress',
149
- started_at: '2026-07-21T10:00:00Z',
150
- phase_history: [],
151
- isolation: { branch: 'test' },
152
- outputs: { spec: 'design.yaml', pr: '#42' },
153
- };
154
-
155
- const result = renderDashboard(state);
156
- expect(result).toContain('输出物');
157
- expect(result).toContain('spec: design.yaml');
158
- expect(result).toContain('pr: #42');
159
- });
160
-
161
- it('handles minimal state gracefully', () => {
162
- const state = {
163
- id: 'sprint-min',
164
- phase: 1,
165
- status: 'in_progress',
166
- phase_history: [],
167
- };
168
-
169
- const result = renderDashboard(state);
170
- expect(result).toContain('SPRINT PROGRESS');
171
- expect(result).toContain('sprint-min');
172
- });
173
- });
174
- });
1
+ /**
2
+ * Tests for phase-transition CLI command
3
+ * @test REQ-338 Phase transition CLI + dashboard rendering
4
+ * @intent Verify phase-transition command updates state and renders dashboard
5
+ * @covers AC-338-01 (CLI transitions phase and writes state)
6
+ * @covers AC-338-02 (--render flag outputs ASCII dashboard)
7
+ * @covers AC-338-03 (invalid inputs return error exit code)
8
+ * @covers AC-338-04 (--outputs flag records outputs in state)
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import os from 'node:os';
15
+ import { handlePhaseTransition, renderDashboard, PHASE_NAMES } from '../phase-transition.js';
16
+
17
+ describe('phase-transition', () => {
18
+ let tmpDir;
19
+
20
+ beforeEach(() => {
21
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-transition-test-'));
22
+ });
23
+
24
+ afterEach(() => {
25
+ fs.rmSync(tmpDir, { recursive: true, force: true });
26
+ });
27
+
28
+ describe('handlePhaseTransition()', () => {
29
+ it('returns 0 and shows help with --help', async () => {
30
+ const code = await handlePhaseTransition(['--help']);
31
+ expect(code).toBe(0);
32
+ });
33
+
34
+ it('returns 1 for missing arguments', async () => {
35
+ const code = await handlePhaseTransition([]);
36
+ expect(code).toBe(1);
37
+ });
38
+
39
+ it('returns 1 for invalid phase number', async () => {
40
+ const code = await handlePhaseTransition(['7', 'completed']);
41
+ expect(code).toBe(1);
42
+ });
43
+
44
+ it('returns 1 for invalid status', async () => {
45
+ const code = await handlePhaseTransition(['1', 'invalid_status']);
46
+ expect(code).toBe(1);
47
+ });
48
+
49
+ it('transitions phase and writes state file', async () => {
50
+ const code = await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
51
+ expect(code).toBe(0);
52
+
53
+ const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
54
+ expect(fs.existsSync(stateFile)).toBe(true);
55
+
56
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
57
+ expect(state.phase).toBe(1);
58
+ expect(state.phase_history).toHaveLength(1);
59
+ expect(state.phase_history[0].status).toBe('completed');
60
+ expect(state.phase_history[0].phase_name).toBe('PREP');
61
+ });
62
+
63
+ it('records outputs when --outputs flag is provided', async () => {
64
+ const outputs = JSON.stringify({ spec: 'path/to/spec.yaml' });
65
+ const code = await handlePhaseTransition(['2', 'completed', '--outputs', outputs, '--dir', tmpDir]);
66
+ expect(code).toBe(0);
67
+
68
+ const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
69
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
70
+ expect(state.outputs.spec).toBe('path/to/spec.yaml');
71
+ });
72
+
73
+ it('returns 1 for invalid --outputs JSON', async () => {
74
+ const code = await handlePhaseTransition(['1', 'completed', '--outputs', 'not-json', '--dir', tmpDir]);
75
+ expect(code).toBe(1);
76
+ });
77
+
78
+ it('transitions through multiple phases sequentially', async () => {
79
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
80
+ await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
81
+ await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
82
+ await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
83
+
84
+ const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
85
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
86
+
87
+ expect(state.phase).toBe(3);
88
+ expect(state.phase_history).toHaveLength(3);
89
+ expect(state.phase_history[0].status).toBe('completed');
90
+ expect(state.phase_history[1].status).toBe('completed');
91
+ expect(state.phase_history[2].status).toBe('in_progress');
92
+ });
93
+
94
+ it('updates existing entry when same phase is transitioned again', async () => {
95
+ await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
96
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
97
+
98
+ const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
99
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
100
+
101
+ // Should NOT have duplicate entries
102
+ expect(state.phase_history).toHaveLength(1);
103
+ expect(state.phase_history[0].status).toBe('completed');
104
+ expect(state.phase_history[0].completed_at).toBeDefined();
105
+ });
106
+ });
107
+
108
+ describe('renderDashboard()', () => {
109
+ it('returns "No sprint state found" for null state', () => {
110
+ const result = renderDashboard(null);
111
+ expect(result).toBe('No sprint state found');
112
+ });
113
+
114
+ it('renders dashboard with all 6 phases', () => {
115
+ const state = {
116
+ id: 'sprint-test-123',
117
+ task_description: 'Test feature',
118
+ phase: 3,
119
+ status: 'in_progress',
120
+ started_at: '2026-07-21T10:00:00Z',
121
+ phase_history: [
122
+ { phase: 1, phase_name: 'PREP', status: 'completed', duration_seconds: 120 },
123
+ { phase: 2, phase_name: 'DESIGN', status: 'completed', duration_seconds: 300 },
124
+ { phase: 3, phase_name: 'BUILD', status: 'in_progress' },
125
+ ],
126
+ isolation: { branch: 'sprint/test-branch' },
127
+ outputs: {},
128
+ };
129
+
130
+ const result = renderDashboard(state);
131
+
132
+ expect(result).toContain('SPRINT PROGRESS');
133
+ expect(result).toContain('sprint-test-123');
134
+ expect(result).toContain('Test feature');
135
+ expect(result).toContain('sprint/test-branch');
136
+ expect(result).toContain('Phase 1/6');
137
+ expect(result).toContain('Phase 6/6');
138
+ expect(result).toContain('✅'); // completed phases
139
+ expect(result).toContain('🔄'); // in_progress phase
140
+ expect(result).toContain('⏳'); // pending phases
141
+ });
142
+
143
+ it('shows outputs section when outputs exist', () => {
144
+ const state = {
145
+ id: 'sprint-out',
146
+ task_description: 'Feature',
147
+ phase: 2,
148
+ status: 'in_progress',
149
+ started_at: '2026-07-21T10:00:00Z',
150
+ phase_history: [],
151
+ isolation: { branch: 'test' },
152
+ outputs: { spec: 'design.yaml', pr: '#42' },
153
+ };
154
+
155
+ const result = renderDashboard(state);
156
+ expect(result).toContain('输出物');
157
+ expect(result).toContain('spec: design.yaml');
158
+ expect(result).toContain('pr: #42');
159
+ });
160
+
161
+ it('handles minimal state gracefully', () => {
162
+ const state = {
163
+ id: 'sprint-min',
164
+ phase: 1,
165
+ status: 'in_progress',
166
+ phase_history: [],
167
+ };
168
+
169
+ const result = renderDashboard(state);
170
+ expect(result).toContain('SPRINT PROGRESS');
171
+ expect(result).toContain('sprint-min');
172
+ });
173
+ });
174
+ describe('Layer 1: Pre-transition gate check', () => {
175
+ it('warns when previous phase not completed', async () => {
176
+ // Set up: Phase 1 completed, then try Phase 3 in_progress (skipping Phase 2)
177
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
178
+
179
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
180
+ const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
181
+ expect(code).toBe(0);
182
+ expect(warnSpy).toHaveBeenCalledWith(
183
+ expect.stringContaining('Phase 2 (DESIGN) not recorded')
184
+ );
185
+ warnSpy.mockRestore();
186
+ });
187
+
188
+ it('does NOT warn when previous phase is completed', async () => {
189
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
190
+ await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
191
+
192
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
193
+ const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
194
+ expect(code).toBe(0);
195
+ expect(warnSpy).not.toHaveBeenCalled();
196
+ warnSpy.mockRestore();
197
+ });
198
+
199
+ it('does NOT warn when previous phase is skipped', async () => {
200
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
201
+ await handlePhaseTransition(['2', 'skipped', '--dir', tmpDir]);
202
+
203
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
204
+ const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
205
+ expect(code).toBe(0);
206
+ expect(warnSpy).not.toHaveBeenCalled();
207
+ warnSpy.mockRestore();
208
+ });
209
+
210
+ it('skips check when no sprint-state.json exists', async () => {
211
+ // No state file — fresh project
212
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
213
+ const code = await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
214
+ expect(code).toBe(0);
215
+ expect(warnSpy).not.toHaveBeenCalled();
216
+ warnSpy.mockRestore();
217
+ });
218
+
219
+ it('does NOT check for Phase 1 (no predecessor)', async () => {
220
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
221
+ const code = await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
222
+ expect(code).toBe(0);
223
+ expect(warnSpy).not.toHaveBeenCalled();
224
+ warnSpy.mockRestore();
225
+ });
226
+
227
+ it('handles phase_history with gaps correctly', async () => {
228
+ // Set up: Phase 1 completed, Phase 3 completed (gap at Phase 2)
229
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
230
+ // Manually add Phase 3 without Phase 2
231
+ const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
232
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
233
+ state.phase_history.push({ phase: 3, phase_name: 'BUILD', status: 'completed' });
234
+ state.phase = 3;
235
+ fs.writeFileSync(stateFile, JSON.stringify(state));
236
+
237
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
238
+ const code = await handlePhaseTransition(['4', 'in_progress', '--dir', tmpDir]);
239
+ expect(code).toBe(0);
240
+ // Phase 3 is completed, so no warning for Phase 3→4 transition
241
+ expect(warnSpy).not.toHaveBeenCalled();
242
+ warnSpy.mockRestore();
243
+ });
244
+ });
245
+
246
+ describe('Layer 1.5: Phase 6 auto-trigger', () => {
247
+ it('outputs sprint-audit reminder on Phase 6 completed', async () => {
248
+ // Set up state through Phase 5
249
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
250
+ await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
251
+ await handlePhaseTransition(['3', 'completed', '--dir', tmpDir]);
252
+ await handlePhaseTransition(['4', 'completed', '--dir', tmpDir]);
253
+ await handlePhaseTransition(['5', 'completed', '--dir', tmpDir]);
254
+
255
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
256
+ const code = await handlePhaseTransition(['6', 'completed', '--dir', tmpDir]);
257
+ expect(code).toBe(0);
258
+ expect(logSpy).toHaveBeenCalledWith(
259
+ expect.stringContaining('sprint-audit')
260
+ );
261
+ logSpy.mockRestore();
262
+ });
263
+ });
264
+
265
+ describe('PHASE_NAMES export', () => {
266
+ it('exports PHASE_NAMES as shared constant', () => {
267
+ expect(PHASE_NAMES).toBeDefined();
268
+ expect(PHASE_NAMES[1]).toBe('PREP');
269
+ expect(PHASE_NAMES[6]).toBe('CLOSE');
270
+ });
271
+ });
272
+ });