@boyingliu01/xp-gate 0.27.4 → 0.27.5

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 (100) 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/gate-3.sh +59 -3
  6. package/adapters/gate-4.sh +73 -10
  7. package/adapters/gate-7.sh +4 -0
  8. package/adapters/go.sh +0 -0
  9. package/adapters/iac.sh +0 -0
  10. package/adapters/java.sh +0 -0
  11. package/adapters/kotlin.sh +0 -0
  12. package/adapters/objectivec.sh +0 -0
  13. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  14. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  15. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  16. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  17. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  18. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  19. package/adapters/powershell.sh +8 -0
  20. package/adapters/python.sh +0 -0
  21. package/adapters/shell.sh +0 -0
  22. package/adapters/swift.sh +0 -0
  23. package/adapters/typescript.sh +0 -0
  24. package/bin/xp-gate.js +0 -0
  25. package/gate-10.sh +11 -0
  26. package/gate-3.sh +8 -0
  27. package/gate-4.sh +8 -0
  28. package/gate-7.sh +4 -0
  29. package/gate-8.sh +0 -0
  30. package/gate-9.sh +0 -0
  31. package/hooks/adapter-common.sh +0 -0
  32. package/hooks/gate-10.sh +11 -0
  33. package/hooks/gate-3.sh +8 -0
  34. package/hooks/gate-4.sh +8 -0
  35. package/hooks/gate-7.sh +4 -0
  36. package/hooks/gate-8.sh +0 -0
  37. package/hooks/gate-9.sh +0 -0
  38. package/hooks/post-merge +0 -0
  39. package/hooks/pre-commit +53 -11
  40. package/hooks/pre-push +0 -0
  41. package/hooks/sprint-gate.sh +0 -0
  42. package/lib/__tests__/doctor-delphi.test.js +293 -244
  43. package/lib/__tests__/sprint-audit.test.js +207 -207
  44. package/lib/doctor.js +46 -3
  45. package/lib/mattpocock-license-verdict.json +1 -1
  46. package/lib/sprint-audit.js +235 -235
  47. package/mock-policy/AGENTS.md +3 -3
  48. package/mutation/AGENTS.md +3 -3
  49. package/package.json +1 -1
  50. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  51. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  52. package/plugins/claude-code/bin/xp-gate-check +0 -0
  53. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +9 -2
  54. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  55. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  56. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  57. package/plugins/dsh/package.json +1 -1
  58. package/plugins/dsh/skills/batch-grill-me/SKILL.md +9 -2
  59. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  60. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  61. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  62. package/plugins/opencode/package.json +1 -1
  63. package/plugins/opencode/skills/batch-grill-me/SKILL.md +9 -2
  64. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  65. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  66. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  67. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  68. package/plugins/qoder/bin/xp-gate-check +0 -0
  69. package/plugins/qoder/plugin.json +1 -1
  70. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  71. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  72. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  73. package/principles/AGENTS.md +3 -3
  74. package/skills/batch-grill-me/SKILL.md +9 -2
  75. package/skills/delphi-review/AGENTS.md +3 -3
  76. package/skills/sprint-flow/AGENTS.md +3 -3
  77. package/skills/test-specification-alignment/AGENTS.md +3 -3
  78. package/sprint-gate.sh +0 -0
  79. package/plugins/dsh/lib/command.d.ts +0 -32
  80. package/plugins/dsh/lib/command.d.ts.map +0 -1
  81. package/plugins/dsh/lib/command.js +0 -81
  82. package/plugins/dsh/lib/delphi-config.d.ts +0 -75
  83. package/plugins/dsh/lib/delphi-config.d.ts.map +0 -1
  84. package/plugins/dsh/lib/delphi-config.js +0 -183
  85. package/plugins/dsh/lib/delphi-run.d.ts +0 -24
  86. package/plugins/dsh/lib/delphi-run.d.ts.map +0 -1
  87. package/plugins/dsh/lib/delphi-run.js +0 -92
  88. package/plugins/dsh/lib/delphi.d.ts +0 -88
  89. package/plugins/dsh/lib/delphi.d.ts.map +0 -1
  90. package/plugins/dsh/lib/delphi.js +0 -161
  91. package/plugins/dsh/lib/dialect.d.ts +0 -24
  92. package/plugins/dsh/lib/dialect.d.ts.map +0 -1
  93. package/plugins/dsh/lib/dialect.js +0 -36
  94. package/plugins/dsh/lib/gate-runner.d.ts +0 -18
  95. package/plugins/dsh/lib/gate-runner.d.ts.map +0 -1
  96. package/plugins/dsh/lib/gate-runner.js +0 -46
  97. package/plugins/dsh/lib/index.d.ts +0 -5
  98. package/plugins/dsh/lib/index.d.ts.map +0 -1
  99. package/plugins/dsh/lib/index.js +0 -162
  100. package/plugins/dsh/node_modules/.vite/vitest/results.json +0 -1
@@ -1,207 +1,207 @@
1
- /**
2
- * Tests for sprint-audit CLI command (Layer 2)
3
- * @test REQ-338 Sprint audit — final completeness check
4
- * @intent Verify sprint-audit correctly checks phase coverage, artifacts, consistency
5
- * @covers AC-AUDIT-01 (SKIP when no sprint-state.json)
6
- * @covers AC-AUDIT-02 (PASS when 6/6 completed)
7
- * @covers AC-AUDIT-03 (PASS_WITH_WARNINGS when 4-5/6 completed)
8
- * @covers AC-AUDIT-04 (FAIL when <4/6 completed)
9
- * @covers AC-AUDIT-05 (FAIL on state inconsistency)
10
- * @covers AC-AUDIT-06 (--json flag outputs JSON)
11
- * @covers AC-AUDIT-07 (report persisted to audit-report.json)
12
- */
13
-
14
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
15
- import fs from 'node:fs';
16
- import path from 'node:path';
17
- import os from 'node:os';
18
- import { handleSprintAudit } from '../sprint-audit.js';
19
-
20
- describe('sprint-audit', () => {
21
- let tmpDir;
22
-
23
- beforeEach(() => {
24
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-audit-test-'));
25
- });
26
-
27
- afterEach(() => {
28
- fs.rmSync(tmpDir, { recursive: true, force: true });
29
- });
30
-
31
- function writeState(state) {
32
- const stateDir = path.join(tmpDir, '.sprint-state');
33
- fs.mkdirSync(stateDir, { recursive: true });
34
- fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state));
35
- }
36
-
37
- function makeState(overrides = {}) {
38
- return {
39
- _schema_version: 1,
40
- id: 'sprint-test-audit',
41
- task_description: 'Test audit feature',
42
- phase: 6,
43
- status: 'completed',
44
- started_at: '2026-07-22T10:00:00Z',
45
- phase_history: [],
46
- isolation: { branch: 'sprint/test-audit', worktree_path: null },
47
- outputs: {},
48
- ...overrides,
49
- };
50
- }
51
-
52
- function makePhaseHistory(completedPhases) {
53
- const phaseNames = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
54
- return completedPhases.map((p, i) => ({
55
- phase: p,
56
- phase_name: phaseNames[p - 1],
57
- status: 'completed',
58
- started_at: `2026-07-22T10:${String(i).padStart(2, '0')}:00Z`,
59
- completed_at: `2026-07-22T10:${String(i).padStart(2, '0')}:30Z`,
60
- duration_seconds: 30,
61
- }));
62
- }
63
-
64
- describe('handleSprintAudit()', () => {
65
- it('returns 0 and shows help with --help', async () => {
66
- const code = await handleSprintAudit(['--help']);
67
- expect(code).toBe(0);
68
- });
69
-
70
- it('returns SKIP (exit 0) when no sprint-state.json exists', async () => {
71
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
72
- const code = await handleSprintAudit(['--dir', tmpDir]);
73
- expect(code).toBe(0);
74
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No sprint-state.json'));
75
- logSpy.mockRestore();
76
- });
77
-
78
- it('returns PASS when 6/6 phases completed', async () => {
79
- writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
80
-
81
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
82
- const code = await handleSprintAudit(['--dir', tmpDir]);
83
- expect(code).toBe(0);
84
- // Check output contains PASS
85
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
86
- expect(output).toContain('PASS');
87
- expect(output).toContain('6/6');
88
- logSpy.mockRestore();
89
- });
90
-
91
- it('returns PASS_WITH_WARNINGS when 5/6 completed', async () => {
92
- writeState(makeState({ phase: 5, phase_history: makePhaseHistory([1, 2, 3, 4, 5]) }));
93
-
94
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
95
- const code = await handleSprintAudit(['--dir', tmpDir]);
96
- expect(code).toBe(0);
97
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
98
- expect(output).toContain('PASS_WITH_WARNINGS');
99
- logSpy.mockRestore();
100
- });
101
-
102
- it('returns PASS_WITH_WARNINGS when 4/6 completed', async () => {
103
- writeState(makeState({ phase: 4, phase_history: makePhaseHistory([1, 2, 3, 4]) }));
104
-
105
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
106
- const code = await handleSprintAudit(['--dir', tmpDir]);
107
- expect(code).toBe(0);
108
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
109
- expect(output).toContain('PASS_WITH_WARNINGS');
110
- logSpy.mockRestore();
111
- });
112
-
113
- it('returns FAIL when <4/6 completed', async () => {
114
- writeState(makeState({ phase: 3, phase_history: makePhaseHistory([1, 2, 3]) }));
115
-
116
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
117
- const code = await handleSprintAudit(['--dir', tmpDir]);
118
- expect(code).toBe(1);
119
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
120
- expect(output).toContain('FAIL');
121
- logSpy.mockRestore();
122
- });
123
-
124
- it('returns FAIL on state inconsistency (phase vs phase_history)', async () => {
125
- const history = makePhaseHistory([1, 2, 3, 4, 5, 6]);
126
- writeState(makeState({ phase: 3, phase_history: history })); // phase=3 but last entry is 6
127
-
128
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
129
- const code = await handleSprintAudit(['--dir', tmpDir]);
130
- expect(code).toBe(1);
131
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
132
- expect(output).toContain('State inconsistency');
133
- logSpy.mockRestore();
134
- });
135
-
136
- it('outputs JSON when --json flag is set', async () => {
137
- writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
138
-
139
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
140
- const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
141
- expect(code).toBe(0);
142
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
143
- const result = JSON.parse(output);
144
- expect(result.verdict).toBe('PASS');
145
- expect(result.coverage.completed).toBe(6);
146
- logSpy.mockRestore();
147
- });
148
-
149
- it('counts skipped phases as completed in coverage', async () => {
150
- const history = makePhaseHistory([1, 2, 3, 4, 5]);
151
- history.push({
152
- phase: 6, phase_name: 'CLOSE', status: 'skipped',
153
- started_at: '2026-07-22T10:05:00Z', completed_at: '2026-07-22T10:05:00Z', duration_seconds: 0,
154
- });
155
- writeState(makeState({ phase: 6, phase_history: history }));
156
-
157
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
158
- const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
159
- expect(code).toBe(0);
160
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
161
- const result = JSON.parse(output);
162
- expect(result.coverage.completed).toBe(6);
163
- expect(result.verdict).toBe('PASS');
164
- logSpy.mockRestore();
165
- });
166
-
167
- it('warns about missing summary files for completed phases', async () => {
168
- writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
169
- // No phase-outputs directory → all summaries missing
170
-
171
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
172
- const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
173
- expect(code).toBe(0);
174
- const output = logSpy.mock.calls.map(c => c[0]).join('\n');
175
- const result = JSON.parse(output);
176
- expect(result.warnings.length).toBeGreaterThan(0);
177
- expect(result.warnings.some(w => w.includes('summary missing'))).toBe(true);
178
- logSpy.mockRestore();
179
- });
180
-
181
- it('persists audit report to audit-report.json', async () => {
182
- writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
183
-
184
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
185
- await handleSprintAudit(['--dir', tmpDir]);
186
- logSpy.mockRestore();
187
-
188
- const reportPath = path.join(tmpDir, '.sprint-state', 'audit-report.json');
189
- expect(fs.existsSync(reportPath)).toBe(true);
190
- const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
191
- expect(report.verdict).toBe('PASS');
192
- expect(report.sprint_id).toBe('sprint-test-audit');
193
- });
194
-
195
- it('handles corrupted sprint-state.json gracefully', async () => {
196
- const stateDir = path.join(tmpDir, '.sprint-state');
197
- fs.mkdirSync(stateDir, { recursive: true });
198
- fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), 'not-valid-json');
199
-
200
- const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
201
- const code = await handleSprintAudit(['--dir', tmpDir]);
202
- expect(code).toBe(1);
203
- expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse'));
204
- errSpy.mockRestore();
205
- });
206
- });
207
- });
1
+ /**
2
+ * Tests for sprint-audit CLI command (Layer 2)
3
+ * @test REQ-338 Sprint audit — final completeness check
4
+ * @intent Verify sprint-audit correctly checks phase coverage, artifacts, consistency
5
+ * @covers AC-AUDIT-01 (SKIP when no sprint-state.json)
6
+ * @covers AC-AUDIT-02 (PASS when 6/6 completed)
7
+ * @covers AC-AUDIT-03 (PASS_WITH_WARNINGS when 4-5/6 completed)
8
+ * @covers AC-AUDIT-04 (FAIL when <4/6 completed)
9
+ * @covers AC-AUDIT-05 (FAIL on state inconsistency)
10
+ * @covers AC-AUDIT-06 (--json flag outputs JSON)
11
+ * @covers AC-AUDIT-07 (report persisted to audit-report.json)
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import os from 'node:os';
18
+ import { handleSprintAudit } from '../sprint-audit.js';
19
+
20
+ describe('sprint-audit', () => {
21
+ let tmpDir;
22
+
23
+ beforeEach(() => {
24
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-audit-test-'));
25
+ });
26
+
27
+ afterEach(() => {
28
+ fs.rmSync(tmpDir, { recursive: true, force: true });
29
+ });
30
+
31
+ function writeState(state) {
32
+ const stateDir = path.join(tmpDir, '.sprint-state');
33
+ fs.mkdirSync(stateDir, { recursive: true });
34
+ fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state));
35
+ }
36
+
37
+ function makeState(overrides = {}) {
38
+ return {
39
+ _schema_version: 1,
40
+ id: 'sprint-test-audit',
41
+ task_description: 'Test audit feature',
42
+ phase: 6,
43
+ status: 'completed',
44
+ started_at: '2026-07-22T10:00:00Z',
45
+ phase_history: [],
46
+ isolation: { branch: 'sprint/test-audit', worktree_path: null },
47
+ outputs: {},
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function makePhaseHistory(completedPhases) {
53
+ const phaseNames = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
54
+ return completedPhases.map((p, i) => ({
55
+ phase: p,
56
+ phase_name: phaseNames[p - 1],
57
+ status: 'completed',
58
+ started_at: `2026-07-22T10:${String(i).padStart(2, '0')}:00Z`,
59
+ completed_at: `2026-07-22T10:${String(i).padStart(2, '0')}:30Z`,
60
+ duration_seconds: 30,
61
+ }));
62
+ }
63
+
64
+ describe('handleSprintAudit()', () => {
65
+ it('returns 0 and shows help with --help', async () => {
66
+ const code = await handleSprintAudit(['--help']);
67
+ expect(code).toBe(0);
68
+ });
69
+
70
+ it('returns SKIP (exit 0) when no sprint-state.json exists', async () => {
71
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
72
+ const code = await handleSprintAudit(['--dir', tmpDir]);
73
+ expect(code).toBe(0);
74
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No sprint-state.json'));
75
+ logSpy.mockRestore();
76
+ });
77
+
78
+ it('returns PASS when 6/6 phases completed', async () => {
79
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
80
+
81
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
82
+ const code = await handleSprintAudit(['--dir', tmpDir]);
83
+ expect(code).toBe(0);
84
+ // Check output contains PASS
85
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
86
+ expect(output).toContain('PASS');
87
+ expect(output).toContain('6/6');
88
+ logSpy.mockRestore();
89
+ });
90
+
91
+ it('returns PASS_WITH_WARNINGS when 5/6 completed', async () => {
92
+ writeState(makeState({ phase: 5, phase_history: makePhaseHistory([1, 2, 3, 4, 5]) }));
93
+
94
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
95
+ const code = await handleSprintAudit(['--dir', tmpDir]);
96
+ expect(code).toBe(0);
97
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
98
+ expect(output).toContain('PASS_WITH_WARNINGS');
99
+ logSpy.mockRestore();
100
+ });
101
+
102
+ it('returns PASS_WITH_WARNINGS when 4/6 completed', async () => {
103
+ writeState(makeState({ phase: 4, phase_history: makePhaseHistory([1, 2, 3, 4]) }));
104
+
105
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
106
+ const code = await handleSprintAudit(['--dir', tmpDir]);
107
+ expect(code).toBe(0);
108
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
109
+ expect(output).toContain('PASS_WITH_WARNINGS');
110
+ logSpy.mockRestore();
111
+ });
112
+
113
+ it('returns FAIL when <4/6 completed', async () => {
114
+ writeState(makeState({ phase: 3, phase_history: makePhaseHistory([1, 2, 3]) }));
115
+
116
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
117
+ const code = await handleSprintAudit(['--dir', tmpDir]);
118
+ expect(code).toBe(1);
119
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
120
+ expect(output).toContain('FAIL');
121
+ logSpy.mockRestore();
122
+ });
123
+
124
+ it('returns FAIL on state inconsistency (phase vs phase_history)', async () => {
125
+ const history = makePhaseHistory([1, 2, 3, 4, 5, 6]);
126
+ writeState(makeState({ phase: 3, phase_history: history })); // phase=3 but last entry is 6
127
+
128
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
129
+ const code = await handleSprintAudit(['--dir', tmpDir]);
130
+ expect(code).toBe(1);
131
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
132
+ expect(output).toContain('State inconsistency');
133
+ logSpy.mockRestore();
134
+ });
135
+
136
+ it('outputs JSON when --json flag is set', async () => {
137
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
138
+
139
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
140
+ const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
141
+ expect(code).toBe(0);
142
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
143
+ const result = JSON.parse(output);
144
+ expect(result.verdict).toBe('PASS');
145
+ expect(result.coverage.completed).toBe(6);
146
+ logSpy.mockRestore();
147
+ });
148
+
149
+ it('counts skipped phases as completed in coverage', async () => {
150
+ const history = makePhaseHistory([1, 2, 3, 4, 5]);
151
+ history.push({
152
+ phase: 6, phase_name: 'CLOSE', status: 'skipped',
153
+ started_at: '2026-07-22T10:05:00Z', completed_at: '2026-07-22T10:05:00Z', duration_seconds: 0,
154
+ });
155
+ writeState(makeState({ phase: 6, phase_history: history }));
156
+
157
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
158
+ const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
159
+ expect(code).toBe(0);
160
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
161
+ const result = JSON.parse(output);
162
+ expect(result.coverage.completed).toBe(6);
163
+ expect(result.verdict).toBe('PASS');
164
+ logSpy.mockRestore();
165
+ });
166
+
167
+ it('warns about missing summary files for completed phases', async () => {
168
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
169
+ // No phase-outputs directory → all summaries missing
170
+
171
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
172
+ const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
173
+ expect(code).toBe(0);
174
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
175
+ const result = JSON.parse(output);
176
+ expect(result.warnings.length).toBeGreaterThan(0);
177
+ expect(result.warnings.some(w => w.includes('summary missing'))).toBe(true);
178
+ logSpy.mockRestore();
179
+ });
180
+
181
+ it('persists audit report to audit-report.json', async () => {
182
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
183
+
184
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
185
+ await handleSprintAudit(['--dir', tmpDir]);
186
+ logSpy.mockRestore();
187
+
188
+ const reportPath = path.join(tmpDir, '.sprint-state', 'audit-report.json');
189
+ expect(fs.existsSync(reportPath)).toBe(true);
190
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
191
+ expect(report.verdict).toBe('PASS');
192
+ expect(report.sprint_id).toBe('sprint-test-audit');
193
+ });
194
+
195
+ it('handles corrupted sprint-state.json gracefully', async () => {
196
+ const stateDir = path.join(tmpDir, '.sprint-state');
197
+ fs.mkdirSync(stateDir, { recursive: true });
198
+ fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), 'not-valid-json');
199
+
200
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
201
+ const code = await handleSprintAudit(['--dir', tmpDir]);
202
+ expect(code).toBe(1);
203
+ expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse'));
204
+ errSpy.mockRestore();
205
+ });
206
+ });
207
+ });
package/lib/doctor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execSync, spawnSync } = require('child_process');
3
+ const { execSync, execFileSync, spawnSync } = require('child_process');
4
4
  const { exec } = require('child_process');
5
5
  const util = require('util');
6
6
  const execAsync = util.promisify(exec);
@@ -1959,7 +1959,11 @@ function fixMissingHooks(mode, srcDir, hooksDir) {
1959
1959
  */
1960
1960
  function fixCoreHooksPath(globalHooksDir) {
1961
1961
  try {
1962
- execSync(`git config --global core.hooksPath "${globalHooksDir}"`, {
1962
+ // Args array instead of shell string interpolation: the value is a path
1963
+ // that must never be re-parsed by a shell. Also keeps semgrep's
1964
+ // detect-child-process rule from flagging this audited-safe call
1965
+ // (same baseline as .semgrepignore).
1966
+ execFileSync('git', ['config', '--global', 'core.hooksPath', globalHooksDir], {
1963
1967
  stdio: ['pipe', 'pipe', 'pipe'],
1964
1968
  timeout: EXEC_TIMEOUT_MS,
1965
1969
  });
@@ -2684,6 +2688,11 @@ function formatDoctorJson(checks, issues) {
2684
2688
  // runner script surfaced only as a failed review (B3).
2685
2689
  async function doctorDelphi(options = {}) {
2686
2690
  const { configPath, npmLatest } = options;
2691
+ // #541: the shadow check needs the loader's global fallback path. It is
2692
+ // injectable for tests; the default mirrors delphi-config.ts /
2693
+ // delphi-external-review.cjs resolution (XDG_CONFIG_HOME honored).
2694
+ const globalConfigPath = options.globalConfigPath
2695
+ || path.join(process.env.XDG_CONFIG_HOME || path.join(require('os').homedir(), '.config'), 'xp-gate', '.delphi-config.json');
2687
2696
  const checks = [];
2688
2697
  const add = (name, status, detail) => checks.push({ name, status, detail });
2689
2698
 
@@ -2856,6 +2865,40 @@ async function doctorDelphi(options = {}) {
2856
2865
  add('Delphi config (.delphi-config.json)', 'PASS',
2857
2866
  `profile "${profileName}": ${models.join(' / ')}`);
2858
2867
  }
2868
+ // #541 follow-up: shadow detection. Loaders fall back to the global
2869
+ // config ONLY on ENOENT, so this project file always wins here. If the
2870
+ // machine-wide config exists AND seats differ, the effective model mix
2871
+ // depends on cwd — the cross-project drift class behind the 2026-10-03
2872
+ // quota burn. Warn (intentional overrides are legal), never fail.
2873
+ try {
2874
+ if (fs.existsSync(globalConfigPath) && path.resolve(globalConfigPath) !== path.resolve(cfgPath)) {
2875
+ const gRaw = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
2876
+ const gProfileName = gRaw.active_profile || 'default';
2877
+ const gProfile = gRaw.profiles && gRaw.profiles[gProfileName];
2878
+ const gExperts = (gProfile && gProfile.experts) || {};
2879
+ // Seat shape is object-keyed OR array in the wild (delphi-config.ts
2880
+ // accepts both); normalize to a role->model map before comparing.
2881
+ const pickExpert = (experts, role, idx) => {
2882
+ if (!experts) return null;
2883
+ if (Array.isArray(experts)) {
2884
+ const e = experts.find((x) => x && x.role === role) || experts[idx];
2885
+ return (e && e.model) || null;
2886
+ }
2887
+ return (experts[role] && experts[role].model) || null;
2888
+ };
2889
+ const gModels = roles.map((r, i) => pickExpert(gExperts, r, i));
2890
+ // Compare only roles BOTH sides define: a role missing on one side
2891
+ // is an incomplete config (check 5 already FAILs it), not drift.
2892
+ const differing = roles.filter((r, i) => gModels[i] && models[i] && gModels[i] !== models[i]);
2893
+ if (differing.length > 0) {
2894
+ add('Delphi config shadow (#541)', 'WARN',
2895
+ `project config shadows ${globalConfigPath}; seats differ (${differing.join(', ')}) — reviews run from other directories will use different models`);
2896
+ }
2897
+ }
2898
+ } catch {
2899
+ // A broken/unreadable global config is not this check's failure mode;
2900
+ // silence here must not mask it elsewhere, so nothing is claimed.
2901
+ }
2859
2902
  }
2860
2903
  } catch (err) {
2861
2904
  add('Delphi config (.delphi-config.json)', 'FAIL', `unparseable: ${err.message}`);
@@ -2926,7 +2969,7 @@ async function doctorDelphi(options = {}) {
2926
2969
  }
2927
2970
 
2928
2971
  for (const check of checks) {
2929
- const mark = check.status === 'PASS' ? '✓' : check.status === 'SKIP' ? '○' : '✗';
2972
+ const mark = check.status === 'PASS' ? '✓' : check.status === 'SKIP' ? '○' : check.status === 'WARN' ? '⚠' : '✗';
2930
2973
  console.log(` ${mark} ${check.name}: ${check.detail || check.status}`);
2931
2974
  }
2932
2975
  const failed = checks.filter((c) => c.status === 'FAIL').length;
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-17T15:37:03.841Z",
5
+ "checked_at": "2026-10-18T12:53:19.772Z",
6
6
  "source": "network"
7
7
  }