@boyingliu01/xp-gate 0.27.0 → 0.27.2

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 (95) 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 +0 -0
  22. package/gate-10.sh +0 -0
  23. package/gate-3.sh +0 -0
  24. package/gate-4.sh +0 -0
  25. package/gate-7.sh +0 -0
  26. package/gate-8.sh +0 -0
  27. package/gate-9.sh +0 -0
  28. package/hooks/adapter-common.sh +0 -0
  29. package/hooks/gate-10.sh +0 -0
  30. package/hooks/gate-3.sh +0 -0
  31. package/hooks/gate-4.sh +0 -0
  32. package/hooks/gate-7.sh +0 -0
  33. package/hooks/gate-8.sh +0 -0
  34. package/hooks/gate-9.sh +0 -0
  35. package/hooks/post-merge +0 -0
  36. package/hooks/pre-commit +0 -0
  37. package/hooks/pre-push +0 -0
  38. package/hooks/sprint-gate.sh +0 -0
  39. package/lib/__tests__/doctor-delphi.test.js +244 -244
  40. package/lib/__tests__/doctor-exec-retry.test.js +152 -0
  41. package/lib/__tests__/sprint-audit.test.js +207 -207
  42. package/lib/__tests__/windows-bash-probe.test.js +57 -7
  43. package/lib/detect-deps.js +69 -13
  44. package/lib/doctor.js +45 -11
  45. package/lib/mattpocock-license-verdict.json +1 -1
  46. package/lib/sprint-audit.js +235 -235
  47. package/mock-policy/AGENTS.md +2 -2
  48. package/mutation/AGENTS.md +2 -2
  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/delphi-review/AGENTS.md +2 -2
  54. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  55. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  56. package/plugins/dsh/lib/command.d.ts +32 -0
  57. package/plugins/dsh/lib/command.d.ts.map +1 -0
  58. package/plugins/dsh/lib/command.js +81 -0
  59. package/plugins/dsh/lib/delphi-config.d.ts +75 -0
  60. package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
  61. package/plugins/dsh/lib/delphi-config.js +155 -0
  62. package/plugins/dsh/lib/delphi-run.d.ts +24 -0
  63. package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
  64. package/plugins/dsh/lib/delphi-run.js +92 -0
  65. package/plugins/dsh/lib/delphi.d.ts +88 -0
  66. package/plugins/dsh/lib/delphi.d.ts.map +1 -0
  67. package/plugins/dsh/lib/delphi.js +161 -0
  68. package/plugins/dsh/lib/dialect.d.ts +24 -0
  69. package/plugins/dsh/lib/dialect.d.ts.map +1 -0
  70. package/plugins/dsh/lib/dialect.js +36 -0
  71. package/plugins/dsh/lib/gate-runner.d.ts +18 -0
  72. package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
  73. package/plugins/dsh/lib/gate-runner.js +46 -0
  74. package/plugins/dsh/lib/index.d.ts +5 -0
  75. package/plugins/dsh/lib/index.d.ts.map +1 -0
  76. package/plugins/dsh/lib/index.js +162 -0
  77. package/plugins/dsh/package.json +1 -1
  78. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  79. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  80. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  81. package/plugins/opencode/package.json +1 -1
  82. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  83. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  84. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  85. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  86. package/plugins/qoder/bin/xp-gate-check +0 -0
  87. package/plugins/qoder/plugin.json +1 -1
  88. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  89. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  90. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  91. package/principles/AGENTS.md +2 -2
  92. package/skills/delphi-review/AGENTS.md +2 -2
  93. package/skills/sprint-flow/AGENTS.md +2 -2
  94. package/skills/test-specification-alignment/AGENTS.md +2 -2
  95. package/sprint-gate.sh +0 -0
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
File without changes
package/gate-10.sh CHANGED
File without changes
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-10.sh CHANGED
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/post-merge CHANGED
File without changes
package/hooks/pre-commit CHANGED
File without changes
package/hooks/pre-push CHANGED
File without changes
File without changes
@@ -1,244 +1,244 @@
1
- /**
2
- * @test doctor-delphi
3
- * @intent Guard #414 REQ-3/REQ-6: `xp-gate doctor delphi` turns the failure
4
- * modes measured on real machines into diagnosable checks — drifted agent
5
- * bindings (B2), a skill install that silently lagged the published package
6
- * (B4), a runner script nobody can find (B3), and a config that cannot serve
7
- * three distinct models. Every check must be assertable in isolation, and the
8
- * npm-latest probe must be skippable so the doctor never needs the network.
9
- */
10
- const fs = require('fs');
11
- const path = require('path');
12
- const { resetLibModules, muteConsole, makeTempDirs, removeTempDirs } = require('./helpers/lib-sandbox.cjs');
13
-
14
- const LIB_MODULES = ['../init', '../detect-deps.js', '../shared-paths', '../doctor', '../delphi-setup'].map(name => require.resolve(name));
15
-
16
- function repoRoot() {
17
- return path.join(__dirname, '..', '..', '..', '..');
18
- }
19
-
20
- function bundledSkillDir() {
21
- return path.join(repoRoot(), 'src', 'npm-package', 'skills', 'delphi-review');
22
- }
23
-
24
- describe('doctor delphi (#414 REQ-3/REQ-6)', () => {
25
- let tmpDirs;
26
- let tmpHome;
27
- let originalHome;
28
-
29
- beforeEach(() => {
30
- tmpDirs = makeTempDirs(['doctor-delphi-home']);
31
- tmpHome = tmpDirs['doctor-delphi-home'];
32
- originalHome = process.env.HOME;
33
- process.env.HOME = tmpHome;
34
- fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
35
- resetLibModules(vi, LIB_MODULES);
36
- muteConsole(vi);
37
- });
38
-
39
- afterEach(() => {
40
- if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
41
- removeTempDirs(tmpDirs);
42
- vi.restoreAllMocks();
43
- });
44
-
45
- function healthyHome() {
46
- // A home that would pass the agent + skill-parity checks.
47
- fs.mkdirSync(path.join(tmpHome, '.qoder', 'agents'), { recursive: true });
48
- fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills', 'delphi-review'), { recursive: true });
49
- for (const role of ['architecture', 'technical', 'feasibility']) {
50
- fs.copyFileSync(
51
- path.join(repoRoot(), 'plugins', 'qoder', 'agents', `delphi-${role}.md`),
52
- path.join(tmpHome, '.qoder', 'agents', `delphi-${role}.md`),
53
- );
54
- }
55
- for (const f of fs.readdirSync(bundledSkillDir(), { withFileTypes: true })) {
56
- const dest = path.join(tmpHome, '.qoder', 'skills', 'delphi-review', f.name);
57
- if (f.isDirectory()) {
58
- fs.mkdirSync(dest, { recursive: true });
59
- for (const inner of fs.readdirSync(path.join(bundledSkillDir(), f.name))) {
60
- if (fs.statSync(path.join(bundledSkillDir(), f.name, inner)).isFile()) {
61
- fs.copyFileSync(path.join(bundledSkillDir(), f.name, inner), path.join(dest, inner));
62
- }
63
- }
64
- } else if (f.isFile()) {
65
- fs.copyFileSync(path.join(bundledSkillDir(), f.name), dest);
66
- }
67
- }
68
- }
69
-
70
- function writeConfig(configPath) {
71
- fs.writeFileSync(configPath, JSON.stringify({
72
- active_profile: 'default',
73
- profiles: {
74
- default: {
75
- providers: {
76
- gw: { base_url: 'https://gw.example/v1', api_key: 'k' },
77
- },
78
- experts: {
79
- architecture: { provider: 'gw', model: 'model-a' },
80
- technical: { provider: 'gw', model: 'model-b' },
81
- feasibility: { provider: 'gw', model: 'model-c' },
82
- },
83
- },
84
- },
85
- }));
86
- return configPath;
87
- }
88
-
89
- it('a fully healthy environment PASSes every check and exits 0', async () => {
90
- healthyHome();
91
- const { doctorDelphi } = require('../doctor');
92
- const configPath = writeConfig(path.join(tmpDirs['doctor-delphi-home'], '.delphi-config.json'));
93
- const localVersion = require('../../package.json').version;
94
- const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: localVersion });
95
- expect(code).toBe(0);
96
- });
97
-
98
- it('a stale agent binding FAILs and names delphi-setup as the repair path', async () => {
99
- healthyHome();
100
- fs.writeFileSync(
101
- path.join(tmpHome, '.qoder', 'agents', 'delphi-technical.md'),
102
- '---\nname: delphi-technical\nmodel: "[Retired](retired-id)"\n---\n# body\n',
103
- );
104
- const { doctorDelphi } = require('../doctor');
105
- const code = await doctorDelphi({ projectDir: tmpHome, configPath: path.join(tmpHome, 'absent.json'), npmLatest: null });
106
- expect(code).toBe(1);
107
- });
108
-
109
- it('a skill install that drifted from the bundled copy FAILs with a reinstall hint', async () => {
110
- healthyHome();
111
- const installed = path.join(tmpHome, '.qoder', 'skills', 'delphi-review', 'SKILL.md');
112
- fs.appendFileSync(installed, '\ndrifted line\n');
113
- const logs = [];
114
- const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
115
- const { doctorDelphi } = require('../doctor');
116
- await doctorDelphi({ projectDir: tmpHome, configPath: path.join(tmpHome, 'absent.json'), npmLatest: null });
117
- logSpy.mockRestore();
118
- expect(logs.join('\n')).toMatch(/install-skill|reinstall/i);
119
- });
120
-
121
- it('a duplicate-model config FAILs instead of promising a review that will BLOCK', async () => {
122
- healthyHome();
123
- const configPath = path.join(tmpHome, '.delphi-config.json');
124
- fs.writeFileSync(configPath, JSON.stringify({
125
- active_profile: 'default',
126
- profiles: {
127
- default: {
128
- providers: { gw: { base_url: 'https://gw.example/v1', api_key: 'k' } },
129
- experts: {
130
- architecture: { provider: 'gw', model: 'same' },
131
- technical: { provider: 'gw', model: 'same' },
132
- feasibility: { provider: 'gw', model: 'model-c' },
133
- },
134
- },
135
- },
136
- }));
137
- const { doctorDelphi } = require('../doctor');
138
- const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
139
- expect(code).toBe(1);
140
- });
141
-
142
- it('a missing config FAILs with the copy-the-example hint', async () => {
143
- healthyHome();
144
- const logs = [];
145
- const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
146
- const { doctorDelphi } = require('../doctor');
147
- await doctorDelphi({ projectDir: tmpHome, configPath: path.join(tmpHome, 'absent.json'), npmLatest: null });
148
- logSpy.mockRestore();
149
- expect(logs.join('\n')).toMatch(/\.delphi-config\.json/);
150
- });
151
-
152
- it('npmLatest=null skips the version check without failing the run', async () => {
153
- healthyHome();
154
- const { doctorDelphi } = require('../doctor');
155
- const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
156
- const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
157
- expect(code).toBe(0);
158
- });
159
-
160
- // R1 FC-07: the switch shipping off is the platform default, not a defect —
161
- // only enabled-but-underprovisioned is a real FAIL.
162
- it('DSH with subagentModelSelection absent is SKIP, not FAIL', async () => {
163
- healthyHome();
164
- fs.mkdirSync(path.join(tmpHome, '.dsh'), { recursive: true });
165
- fs.writeFileSync(path.join(tmpHome, '.dsh', 'settings.json'), JSON.stringify({ other: true }));
166
- const { doctorDelphi } = require('../doctor');
167
- const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
168
- const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
169
- expect(code).toBe(0);
170
- });
171
-
172
- // R1 FC-09: a project-level agent set (delphi-setup --local --project) is
173
- // what the doctor reads, so the repair loop closes.
174
- it('a complete project-level agent set takes precedence over the HOME set', async () => {
175
- healthyHome();
176
- const projDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'doctor-delphi-proj-'));
177
- try {
178
- fs.mkdirSync(path.join(projDir, '.qoder', 'agents'), { recursive: true });
179
- for (const role of ['architecture', 'technical', 'feasibility']) {
180
- fs.copyFileSync(
181
- path.join(repoRoot(), 'plugins', 'qoder', 'agents', `delphi-${role}.md`),
182
- path.join(projDir, '.qoder', 'agents', `delphi-${role}.md`),
183
- );
184
- }
185
- // Corrupt the HOME set; the project set must win and the run stays green.
186
- fs.writeFileSync(
187
- path.join(tmpHome, '.qoder', 'agents', 'delphi-technical.md'),
188
- '---\nname: delphi-technical\nmodel: "[Retired](retired-id)"\n---\n# body\n',
189
- );
190
- const { doctorDelphi } = require('../doctor');
191
- const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
192
- const code = await doctorDelphi({
193
- projectDir: projDir, configPath, npmLatest: null,
194
- });
195
- expect(code).toBe(0);
196
- } finally {
197
- fs.rmSync(projDir, { recursive: true, force: true });
198
- }
199
- });
200
-
201
- // R1 MI-03: parity covers the whole bundled file set, so a stale references/
202
- // copy cannot hide behind a matching SKILL.md.
203
- it('a drifted references file FAILs parity even when SKILL.md matches', async () => {
204
- healthyHome();
205
- // R2 MJ-R2-01: the assertion must be the ONLY way this test can fail. An
206
- // absent config would exit 1 regardless of parity, so the fixture supplies
207
- // a valid one and lets the drifted file carry the verdict.
208
- const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
209
- const refsDir = path.join(bundledSkillDir(), 'references');
210
- const refFile = fs.existsSync(refsDir)
211
- ? fs.readdirSync(refsDir).find((f) => f.endsWith('.md'))
212
- : null;
213
- expect(refFile).toBeTruthy(); // explicit: without a bundled reference this test has nothing to say
214
- fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills', 'delphi-review', 'references'), { recursive: true });
215
- fs.appendFileSync(
216
- path.join(tmpHome, '.qoder', 'skills', 'delphi-review', 'references', refFile),
217
- '\ndrift\n',
218
- );
219
- const { doctorDelphi } = require('../doctor');
220
- const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
221
- expect(code).toBe(1);
222
- });
223
-
224
- // R3 FC-01/FC-02: the empty-bundle branch is driven directly through the
225
- // injectable bundledSkillDir — the healthy fixture cannot exercise it, and
226
- // an indirect output-shape assertion would pass even without the fix.
227
- it('an empty bundled skill directory FAILs parity instead of passing vacuously', async () => {
228
- healthyHome();
229
- const emptyBundle = fs.mkdtempSync(path.join(require('os').tmpdir(), 'doctor-empty-bundle-'));
230
- try {
231
- const logs = [];
232
- const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
233
- const { doctorDelphi } = require('../doctor');
234
- const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
235
- const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null, bundledSkillDir: emptyBundle });
236
- logSpy.mockRestore();
237
- expect(code).toBe(1);
238
- const parityLine = logs.find((l) => l.includes('Delphi skill install parity'));
239
- expect(parityLine).toContain('missing or empty');
240
- } finally {
241
- fs.rmSync(emptyBundle, { recursive: true, force: true });
242
- }
243
- });
244
- });
1
+ /**
2
+ * @test doctor-delphi
3
+ * @intent Guard #414 REQ-3/REQ-6: `xp-gate doctor delphi` turns the failure
4
+ * modes measured on real machines into diagnosable checks — drifted agent
5
+ * bindings (B2), a skill install that silently lagged the published package
6
+ * (B4), a runner script nobody can find (B3), and a config that cannot serve
7
+ * three distinct models. Every check must be assertable in isolation, and the
8
+ * npm-latest probe must be skippable so the doctor never needs the network.
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { resetLibModules, muteConsole, makeTempDirs, removeTempDirs } = require('./helpers/lib-sandbox.cjs');
13
+
14
+ const LIB_MODULES = ['../init', '../detect-deps.js', '../shared-paths', '../doctor', '../delphi-setup'].map(name => require.resolve(name));
15
+
16
+ function repoRoot() {
17
+ return path.join(__dirname, '..', '..', '..', '..');
18
+ }
19
+
20
+ function bundledSkillDir() {
21
+ return path.join(repoRoot(), 'src', 'npm-package', 'skills', 'delphi-review');
22
+ }
23
+
24
+ describe('doctor delphi (#414 REQ-3/REQ-6)', () => {
25
+ let tmpDirs;
26
+ let tmpHome;
27
+ let originalHome;
28
+
29
+ beforeEach(() => {
30
+ tmpDirs = makeTempDirs(['doctor-delphi-home']);
31
+ tmpHome = tmpDirs['doctor-delphi-home'];
32
+ originalHome = process.env.HOME;
33
+ process.env.HOME = tmpHome;
34
+ fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
35
+ resetLibModules(vi, LIB_MODULES);
36
+ muteConsole(vi);
37
+ });
38
+
39
+ afterEach(() => {
40
+ if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
41
+ removeTempDirs(tmpDirs);
42
+ vi.restoreAllMocks();
43
+ });
44
+
45
+ function healthyHome() {
46
+ // A home that would pass the agent + skill-parity checks.
47
+ fs.mkdirSync(path.join(tmpHome, '.qoder', 'agents'), { recursive: true });
48
+ fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills', 'delphi-review'), { recursive: true });
49
+ for (const role of ['architecture', 'technical', 'feasibility']) {
50
+ fs.copyFileSync(
51
+ path.join(repoRoot(), 'plugins', 'qoder', 'agents', `delphi-${role}.md`),
52
+ path.join(tmpHome, '.qoder', 'agents', `delphi-${role}.md`),
53
+ );
54
+ }
55
+ for (const f of fs.readdirSync(bundledSkillDir(), { withFileTypes: true })) {
56
+ const dest = path.join(tmpHome, '.qoder', 'skills', 'delphi-review', f.name);
57
+ if (f.isDirectory()) {
58
+ fs.mkdirSync(dest, { recursive: true });
59
+ for (const inner of fs.readdirSync(path.join(bundledSkillDir(), f.name))) {
60
+ if (fs.statSync(path.join(bundledSkillDir(), f.name, inner)).isFile()) {
61
+ fs.copyFileSync(path.join(bundledSkillDir(), f.name, inner), path.join(dest, inner));
62
+ }
63
+ }
64
+ } else if (f.isFile()) {
65
+ fs.copyFileSync(path.join(bundledSkillDir(), f.name), dest);
66
+ }
67
+ }
68
+ }
69
+
70
+ function writeConfig(configPath) {
71
+ fs.writeFileSync(configPath, JSON.stringify({
72
+ active_profile: 'default',
73
+ profiles: {
74
+ default: {
75
+ providers: {
76
+ gw: { base_url: 'https://gw.example/v1', api_key: 'k' },
77
+ },
78
+ experts: {
79
+ architecture: { provider: 'gw', model: 'model-a' },
80
+ technical: { provider: 'gw', model: 'model-b' },
81
+ feasibility: { provider: 'gw', model: 'model-c' },
82
+ },
83
+ },
84
+ },
85
+ }));
86
+ return configPath;
87
+ }
88
+
89
+ it('a fully healthy environment PASSes every check and exits 0', async () => {
90
+ healthyHome();
91
+ const { doctorDelphi } = require('../doctor');
92
+ const configPath = writeConfig(path.join(tmpDirs['doctor-delphi-home'], '.delphi-config.json'));
93
+ const localVersion = require('../../package.json').version;
94
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: localVersion });
95
+ expect(code).toBe(0);
96
+ });
97
+
98
+ it('a stale agent binding FAILs and names delphi-setup as the repair path', async () => {
99
+ healthyHome();
100
+ fs.writeFileSync(
101
+ path.join(tmpHome, '.qoder', 'agents', 'delphi-technical.md'),
102
+ '---\nname: delphi-technical\nmodel: "[Retired](retired-id)"\n---\n# body\n',
103
+ );
104
+ const { doctorDelphi } = require('../doctor');
105
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath: path.join(tmpHome, 'absent.json'), npmLatest: null });
106
+ expect(code).toBe(1);
107
+ });
108
+
109
+ it('a skill install that drifted from the bundled copy FAILs with a reinstall hint', async () => {
110
+ healthyHome();
111
+ const installed = path.join(tmpHome, '.qoder', 'skills', 'delphi-review', 'SKILL.md');
112
+ fs.appendFileSync(installed, '\ndrifted line\n');
113
+ const logs = [];
114
+ const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
115
+ const { doctorDelphi } = require('../doctor');
116
+ await doctorDelphi({ projectDir: tmpHome, configPath: path.join(tmpHome, 'absent.json'), npmLatest: null });
117
+ logSpy.mockRestore();
118
+ expect(logs.join('\n')).toMatch(/install-skill|reinstall/i);
119
+ });
120
+
121
+ it('a duplicate-model config FAILs instead of promising a review that will BLOCK', async () => {
122
+ healthyHome();
123
+ const configPath = path.join(tmpHome, '.delphi-config.json');
124
+ fs.writeFileSync(configPath, JSON.stringify({
125
+ active_profile: 'default',
126
+ profiles: {
127
+ default: {
128
+ providers: { gw: { base_url: 'https://gw.example/v1', api_key: 'k' } },
129
+ experts: {
130
+ architecture: { provider: 'gw', model: 'same' },
131
+ technical: { provider: 'gw', model: 'same' },
132
+ feasibility: { provider: 'gw', model: 'model-c' },
133
+ },
134
+ },
135
+ },
136
+ }));
137
+ const { doctorDelphi } = require('../doctor');
138
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
139
+ expect(code).toBe(1);
140
+ });
141
+
142
+ it('a missing config FAILs with the copy-the-example hint', async () => {
143
+ healthyHome();
144
+ const logs = [];
145
+ const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
146
+ const { doctorDelphi } = require('../doctor');
147
+ await doctorDelphi({ projectDir: tmpHome, configPath: path.join(tmpHome, 'absent.json'), npmLatest: null });
148
+ logSpy.mockRestore();
149
+ expect(logs.join('\n')).toMatch(/\.delphi-config\.json/);
150
+ });
151
+
152
+ it('npmLatest=null skips the version check without failing the run', async () => {
153
+ healthyHome();
154
+ const { doctorDelphi } = require('../doctor');
155
+ const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
156
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
157
+ expect(code).toBe(0);
158
+ });
159
+
160
+ // R1 FC-07: the switch shipping off is the platform default, not a defect —
161
+ // only enabled-but-underprovisioned is a real FAIL.
162
+ it('DSH with subagentModelSelection absent is SKIP, not FAIL', async () => {
163
+ healthyHome();
164
+ fs.mkdirSync(path.join(tmpHome, '.dsh'), { recursive: true });
165
+ fs.writeFileSync(path.join(tmpHome, '.dsh', 'settings.json'), JSON.stringify({ other: true }));
166
+ const { doctorDelphi } = require('../doctor');
167
+ const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
168
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
169
+ expect(code).toBe(0);
170
+ });
171
+
172
+ // R1 FC-09: a project-level agent set (delphi-setup --local --project) is
173
+ // what the doctor reads, so the repair loop closes.
174
+ it('a complete project-level agent set takes precedence over the HOME set', async () => {
175
+ healthyHome();
176
+ const projDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'doctor-delphi-proj-'));
177
+ try {
178
+ fs.mkdirSync(path.join(projDir, '.qoder', 'agents'), { recursive: true });
179
+ for (const role of ['architecture', 'technical', 'feasibility']) {
180
+ fs.copyFileSync(
181
+ path.join(repoRoot(), 'plugins', 'qoder', 'agents', `delphi-${role}.md`),
182
+ path.join(projDir, '.qoder', 'agents', `delphi-${role}.md`),
183
+ );
184
+ }
185
+ // Corrupt the HOME set; the project set must win and the run stays green.
186
+ fs.writeFileSync(
187
+ path.join(tmpHome, '.qoder', 'agents', 'delphi-technical.md'),
188
+ '---\nname: delphi-technical\nmodel: "[Retired](retired-id)"\n---\n# body\n',
189
+ );
190
+ const { doctorDelphi } = require('../doctor');
191
+ const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
192
+ const code = await doctorDelphi({
193
+ projectDir: projDir, configPath, npmLatest: null,
194
+ });
195
+ expect(code).toBe(0);
196
+ } finally {
197
+ fs.rmSync(projDir, { recursive: true, force: true });
198
+ }
199
+ });
200
+
201
+ // R1 MI-03: parity covers the whole bundled file set, so a stale references/
202
+ // copy cannot hide behind a matching SKILL.md.
203
+ it('a drifted references file FAILs parity even when SKILL.md matches', async () => {
204
+ healthyHome();
205
+ // R2 MJ-R2-01: the assertion must be the ONLY way this test can fail. An
206
+ // absent config would exit 1 regardless of parity, so the fixture supplies
207
+ // a valid one and lets the drifted file carry the verdict.
208
+ const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
209
+ const refsDir = path.join(bundledSkillDir(), 'references');
210
+ const refFile = fs.existsSync(refsDir)
211
+ ? fs.readdirSync(refsDir).find((f) => f.endsWith('.md'))
212
+ : null;
213
+ expect(refFile).toBeTruthy(); // explicit: without a bundled reference this test has nothing to say
214
+ fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills', 'delphi-review', 'references'), { recursive: true });
215
+ fs.appendFileSync(
216
+ path.join(tmpHome, '.qoder', 'skills', 'delphi-review', 'references', refFile),
217
+ '\ndrift\n',
218
+ );
219
+ const { doctorDelphi } = require('../doctor');
220
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null });
221
+ expect(code).toBe(1);
222
+ });
223
+
224
+ // R3 FC-01/FC-02: the empty-bundle branch is driven directly through the
225
+ // injectable bundledSkillDir — the healthy fixture cannot exercise it, and
226
+ // an indirect output-shape assertion would pass even without the fix.
227
+ it('an empty bundled skill directory FAILs parity instead of passing vacuously', async () => {
228
+ healthyHome();
229
+ const emptyBundle = fs.mkdtempSync(path.join(require('os').tmpdir(), 'doctor-empty-bundle-'));
230
+ try {
231
+ const logs = [];
232
+ const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
233
+ const { doctorDelphi } = require('../doctor');
234
+ const configPath = writeConfig(path.join(tmpHome, '.delphi-config.json'));
235
+ const code = await doctorDelphi({ projectDir: tmpHome, configPath, npmLatest: null, bundledSkillDir: emptyBundle });
236
+ logSpy.mockRestore();
237
+ expect(code).toBe(1);
238
+ const parityLine = logs.find((l) => l.includes('Delphi skill install parity'));
239
+ expect(parityLine).toContain('missing or empty');
240
+ } finally {
241
+ fs.rmSync(emptyBundle, { recursive: true, force: true });
242
+ }
243
+ });
244
+ });