gsd-pi 2.37.1 → 2.38.0-dev.63ad7e5

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 (201) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resources/extensions/browser-tools/package.json +3 -1
  10. package/dist/resources/extensions/cmux/index.js +55 -1
  11. package/dist/resources/extensions/context7/package.json +1 -1
  12. package/dist/resources/extensions/env-utils.js +29 -0
  13. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  14. package/dist/resources/extensions/google-search/package.json +3 -1
  15. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +68 -97
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +143 -96
  24. package/dist/resources/extensions/gsd/captures.js +9 -1
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  27. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  28. package/dist/resources/extensions/gsd/commands.js +22 -2
  29. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  30. package/dist/resources/extensions/gsd/detection.js +1 -2
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  32. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  33. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  34. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  35. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  36. package/dist/resources/extensions/gsd/doctor.js +184 -11
  37. package/dist/resources/extensions/gsd/export.js +1 -1
  38. package/dist/resources/extensions/gsd/files.js +43 -2
  39. package/dist/resources/extensions/gsd/forensics.js +1 -1
  40. package/dist/resources/extensions/gsd/index.js +2 -1
  41. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  42. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  43. package/dist/resources/extensions/gsd/package.json +1 -1
  44. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  45. package/dist/resources/extensions/gsd/preferences-types.js +2 -2
  46. package/dist/resources/extensions/gsd/preferences-validation.js +43 -11
  47. package/dist/resources/extensions/gsd/preferences.js +5 -5
  48. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  49. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  50. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  51. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  52. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  54. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  55. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  56. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  57. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  58. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  59. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  60. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  61. package/dist/resources/extensions/gsd/state.js +1 -1
  62. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  63. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  64. package/dist/resources/extensions/gsd/worktree.js +35 -16
  65. package/dist/resources/extensions/remote-questions/status.js +2 -1
  66. package/dist/resources/extensions/remote-questions/store.js +2 -1
  67. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  68. package/dist/resources/extensions/subagent/index.js +12 -3
  69. package/dist/resources/extensions/subagent/isolation.js +2 -1
  70. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  71. package/dist/resources/extensions/universal-config/package.json +1 -1
  72. package/dist/welcome-screen.d.ts +12 -0
  73. package/dist/welcome-screen.js +53 -0
  74. package/package.json +2 -1
  75. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  76. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  78. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/models.generated.js +172 -0
  80. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  82. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  83. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  84. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  85. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  86. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  88. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  89. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  90. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  91. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  92. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  93. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  95. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  96. package/packages/pi-ai/dist/types.d.ts +2 -2
  97. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/types.js.map +1 -1
  99. package/packages/pi-ai/package.json +1 -0
  100. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  101. package/packages/pi-ai/src/models.generated.ts +172 -0
  102. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  103. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  104. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  105. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  106. package/packages/pi-ai/src/types.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  109. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  112. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  115. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  116. package/pkg/package.json +1 -1
  117. package/src/resources/extensions/cmux/index.ts +57 -1
  118. package/src/resources/extensions/env-utils.ts +31 -0
  119. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  120. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  121. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  122. package/src/resources/extensions/gsd/auto-loop.ts +88 -133
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  124. package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
  125. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  126. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  127. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  128. package/src/resources/extensions/gsd/auto.ts +139 -101
  129. package/src/resources/extensions/gsd/captures.ts +10 -1
  130. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  131. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  132. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  133. package/src/resources/extensions/gsd/commands.ts +24 -2
  134. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  135. package/src/resources/extensions/gsd/detection.ts +2 -2
  136. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  137. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  138. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  139. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  140. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  141. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  142. package/src/resources/extensions/gsd/doctor.ts +177 -13
  143. package/src/resources/extensions/gsd/export.ts +1 -1
  144. package/src/resources/extensions/gsd/files.ts +47 -2
  145. package/src/resources/extensions/gsd/forensics.ts +1 -1
  146. package/src/resources/extensions/gsd/index.ts +3 -1
  147. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  148. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  149. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  150. package/src/resources/extensions/gsd/preferences-types.ts +5 -5
  151. package/src/resources/extensions/gsd/preferences-validation.ts +42 -11
  152. package/src/resources/extensions/gsd/preferences.ts +5 -5
  153. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  154. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  156. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  157. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  158. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  159. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  160. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  161. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  162. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  163. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  164. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  165. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  166. package/src/resources/extensions/gsd/state.ts +1 -1
  167. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  168. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  169. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
  170. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  171. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  172. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  173. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  174. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  175. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  176. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  177. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  178. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  179. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  180. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  181. package/src/resources/extensions/gsd/types.ts +43 -1
  182. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  183. package/src/resources/extensions/gsd/worktree.ts +35 -15
  184. package/src/resources/extensions/remote-questions/status.ts +3 -1
  185. package/src/resources/extensions/remote-questions/store.ts +3 -1
  186. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  187. package/src/resources/extensions/subagent/index.ts +12 -3
  188. package/src/resources/extensions/subagent/isolation.ts +3 -1
  189. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  190. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  191. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  192. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  193. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  194. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  195. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  196. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  197. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  198. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  199. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  200. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  201. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -100,6 +100,99 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
100
100
  assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
101
101
  });
102
102
 
103
+ describe("createGridLayout", () => {
104
+ // Create a mock CmuxClient that tracks createSplitFrom calls
105
+ function makeMockClient() {
106
+ let nextId = 1;
107
+ const calls: Array<{ source: string | undefined; direction: string }> = [];
108
+
109
+ const client = {
110
+ calls,
111
+ async createGridLayout(count: number) {
112
+ // Simulate the grid layout logic with a fake client
113
+ if (count <= 0) return [];
114
+ const surfaces: string[] = [];
115
+
116
+ const createSplitFrom = async (source: string | undefined, direction: string) => {
117
+ calls.push({ source, direction });
118
+ return `surface-${nextId++}`;
119
+ };
120
+
121
+ const rightCol = await createSplitFrom("gsd-surface", "right");
122
+ surfaces.push(rightCol);
123
+ if (count === 1) return surfaces;
124
+
125
+ const bottomRight = await createSplitFrom(rightCol, "down");
126
+ surfaces.push(bottomRight);
127
+ if (count === 2) return surfaces;
128
+
129
+ const bottomLeft = await createSplitFrom("gsd-surface", "down");
130
+ surfaces.push(bottomLeft);
131
+ if (count === 3) return surfaces;
132
+
133
+ let lastSurface = bottomRight;
134
+ for (let i = 3; i < count; i++) {
135
+ const next = await createSplitFrom(lastSurface, "down");
136
+ surfaces.push(next);
137
+ lastSurface = next;
138
+ }
139
+
140
+ return surfaces;
141
+ },
142
+ };
143
+ return client;
144
+ }
145
+
146
+ test("1 agent creates single right split", async () => {
147
+ const mock = makeMockClient();
148
+ const surfaces = await mock.createGridLayout(1);
149
+ assert.equal(surfaces.length, 1);
150
+ assert.deepEqual(mock.calls, [
151
+ { source: "gsd-surface", direction: "right" },
152
+ ]);
153
+ });
154
+
155
+ test("2 agents creates right column then splits it down", async () => {
156
+ const mock = makeMockClient();
157
+ const surfaces = await mock.createGridLayout(2);
158
+ assert.equal(surfaces.length, 2);
159
+ assert.deepEqual(mock.calls, [
160
+ { source: "gsd-surface", direction: "right" },
161
+ { source: "surface-1", direction: "down" },
162
+ ]);
163
+ });
164
+
165
+ test("3 agents creates 2x2 grid (gsd + 3 agent surfaces)", async () => {
166
+ const mock = makeMockClient();
167
+ const surfaces = await mock.createGridLayout(3);
168
+ assert.equal(surfaces.length, 3);
169
+ assert.deepEqual(mock.calls, [
170
+ { source: "gsd-surface", direction: "right" },
171
+ { source: "surface-1", direction: "down" },
172
+ { source: "gsd-surface", direction: "down" },
173
+ ]);
174
+ });
175
+
176
+ test("4 agents creates 2x2 grid with extra split", async () => {
177
+ const mock = makeMockClient();
178
+ const surfaces = await mock.createGridLayout(4);
179
+ assert.equal(surfaces.length, 4);
180
+ assert.deepEqual(mock.calls, [
181
+ { source: "gsd-surface", direction: "right" },
182
+ { source: "surface-1", direction: "down" },
183
+ { source: "gsd-surface", direction: "down" },
184
+ { source: "surface-2", direction: "down" },
185
+ ]);
186
+ });
187
+
188
+ test("0 agents returns empty", async () => {
189
+ const mock = makeMockClient();
190
+ const surfaces = await mock.createGridLayout(0);
191
+ assert.equal(surfaces.length, 0);
192
+ assert.equal(mock.calls.length, 0);
193
+ });
194
+ });
195
+
103
196
  describe("cmux extension discovery opt-out", () => {
104
197
  test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => {
105
198
  const cmuxDir = path.resolve(
@@ -0,0 +1,266 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+
5
+ import { runGSDDoctor } from "../doctor.js";
6
+ import { formatDoctorReportJson } from "../doctor-format.js";
7
+ import { createTestContext } from "./test-helpers.ts";
8
+
9
+ const { assertEq, assertTrue, assertMatch, report } = createTestContext();
10
+
11
+ // ── Helpers ─────────────────────────────────────────────────────────────────
12
+
13
+ function makeBase(): { base: string; gsd: string; mDir: string } {
14
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-enh-"));
15
+ const gsd = join(base, ".gsd");
16
+ const mDir = join(gsd, "milestones", "M001");
17
+ mkdirSync(join(mDir, "slices"), { recursive: true });
18
+ return { base, gsd, mDir };
19
+ }
20
+
21
+ function writeRoadmap(mDir: string, content: string): void {
22
+ writeFileSync(join(mDir, "M001-ROADMAP.md"), content);
23
+ }
24
+
25
+ function writeSlice(mDir: string, sliceId: string, planContent: string): string {
26
+ const sDir = join(mDir, "slices", sliceId);
27
+ const tDir = join(sDir, "tasks");
28
+ mkdirSync(tDir, { recursive: true });
29
+ writeFileSync(join(sDir, `${sliceId}-PLAN.md`), planContent);
30
+ return sDir;
31
+ }
32
+
33
+ async function main(): Promise<void> {
34
+ // ── 1. Circular dependency detection ──────────────────────────────────────
35
+ console.log("\n=== circular dependency detection ===");
36
+ {
37
+ const { base, mDir } = makeBase();
38
+ writeRoadmap(mDir, `# M001: Circular Test\n\n## Slices\n- [ ] **S01: Slice A** \`risk:low\` \`depends:[S02]\`\n > After this: done\n- [ ] **S02: Slice B** \`risk:low\` \`depends:[S01]\`\n > After this: done\n`);
39
+ writeSlice(mDir, "S01", "# S01: Slice A\n\n**Goal:** A\n**Demo:** A\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
40
+ writeSlice(mDir, "S02", "# S02: Slice B\n\n**Goal:** B\n**Demo:** B\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
41
+
42
+ const result = await runGSDDoctor(base, { fix: false });
43
+ assertTrue(
44
+ result.issues.some(i => i.code === "circular_slice_dependency"),
45
+ "detects circular dependency S01 → S02 → S01",
46
+ );
47
+ rmSync(base, { recursive: true, force: true });
48
+ }
49
+
50
+ // ── 2. Duplicate task IDs ──────────────────────────────────────────────────
51
+ console.log("\n=== duplicate task IDs ===");
52
+ {
53
+ const { base, mDir } = makeBase();
54
+ writeRoadmap(mDir, `# M001: Dup Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
55
+ writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: First** `est:10m`\n Task one.\n- [ ] **T01: Duplicate** `est:10m`\n Task dup.\n");
56
+
57
+ const result = await runGSDDoctor(base, { fix: false });
58
+ assertTrue(
59
+ result.issues.some(i => i.code === "duplicate_task_id"),
60
+ "detects duplicate task ID T01",
61
+ );
62
+ rmSync(base, { recursive: true, force: true });
63
+ }
64
+
65
+ // ── 3. Orphaned slice directory ──────────────────────────────────────────
66
+ console.log("\n=== orphaned slice directory ===");
67
+ {
68
+ const { base, mDir } = makeBase();
69
+ writeRoadmap(mDir, `# M001: Orphan Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
70
+ writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
71
+ // Create an extra slice directory not in roadmap
72
+ mkdirSync(join(mDir, "slices", "S99"), { recursive: true });
73
+
74
+ const result = await runGSDDoctor(base, { fix: false });
75
+ assertTrue(
76
+ result.issues.some(i => i.code === "orphaned_slice_directory" && i.message.includes("S99")),
77
+ "detects orphaned slice directory S99",
78
+ );
79
+ rmSync(base, { recursive: true, force: true });
80
+ }
81
+
82
+ // ── 4. Task file not in plan ───────────────────────────────────────────────
83
+ console.log("\n=== task file not in plan ===");
84
+ {
85
+ const { base, mDir } = makeBase();
86
+ writeRoadmap(mDir, `# M001: Extra Task Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
87
+ const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
88
+ // T01 summary (matches plan)
89
+ writeFileSync(join(sDir, "tasks", "T01-SUMMARY.md"), "---\nstatus: done\n---\n# T01\nDone.\n");
90
+ // T99 summary (NOT in plan)
91
+ writeFileSync(join(sDir, "tasks", "T99-SUMMARY.md"), "---\nstatus: done\n---\n# T99\nExtra.\n");
92
+
93
+ const result = await runGSDDoctor(base, { fix: false });
94
+ assertTrue(
95
+ result.issues.some(i => i.code === "task_file_not_in_plan" && i.message.includes("T99")),
96
+ "detects task summary T99 not in plan",
97
+ );
98
+ rmSync(base, { recursive: true, force: true });
99
+ }
100
+
101
+ // ── 5. Stale REPLAN file ────────────────────────────────────────────────────
102
+ console.log("\n=== stale REPLAN detection ===");
103
+ {
104
+ const { base, mDir } = makeBase();
105
+ writeRoadmap(mDir, `# M001: Replan Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
106
+ const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
107
+ writeFileSync(join(sDir, "tasks", "T01-SUMMARY.md"), "---\nstatus: done\ncompleted_at: 2026-01-01T00:00:00Z\n---\n# T01\nDone.\n");
108
+ // Add a REPLAN file even though all tasks are done
109
+ writeFileSync(join(sDir, "S01-REPLAN.md"), "# S01 REPLAN\nSomething changed.\n");
110
+
111
+ const result = await runGSDDoctor(base, { fix: false });
112
+ assertTrue(
113
+ result.issues.some(i => i.code === "stale_replan_file"),
114
+ "detects stale REPLAN when all tasks are done",
115
+ );
116
+ rmSync(base, { recursive: true, force: true });
117
+ }
118
+
119
+ // ── 6. Metrics ledger corrupt ───────────────────────────────────────────────
120
+ console.log("\n=== metrics ledger corrupt ===");
121
+ {
122
+ const { base, gsd, mDir } = makeBase();
123
+ writeRoadmap(mDir, `# M001: Metrics Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
124
+ writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
125
+ // Write invalid metrics.json
126
+ writeFileSync(join(gsd, "metrics.json"), '{"version":2,"data":[]}');
127
+
128
+ const result = await runGSDDoctor(base, { fix: false });
129
+ assertTrue(
130
+ result.issues.some(i => i.code === "metrics_ledger_corrupt"),
131
+ "detects corrupt metrics ledger (version != 1)",
132
+ );
133
+ rmSync(base, { recursive: true, force: true });
134
+ }
135
+
136
+ // ── 7. Large planning file ──────────────────────────────────────────────────
137
+ console.log("\n=== large planning file ===");
138
+ {
139
+ const { base, mDir } = makeBase();
140
+ writeRoadmap(mDir, `# M001: Large File Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
141
+ const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
142
+ // Write a 101KB .md file
143
+ const bigContent = "# Big File\n" + "x".repeat(101 * 1024);
144
+ writeFileSync(join(sDir, "BIGFILE.md"), bigContent);
145
+
146
+ const result = await runGSDDoctor(base, { fix: false });
147
+ assertTrue(
148
+ result.issues.some(i => i.code === "large_planning_file"),
149
+ "detects large planning file over 100KB",
150
+ );
151
+ rmSync(base, { recursive: true, force: true });
152
+ }
153
+
154
+ // ── 8. Future timestamp ─────────────────────────────────────────────────────
155
+ console.log("\n=== future timestamp ===");
156
+ {
157
+ const { base, mDir } = makeBase();
158
+ writeRoadmap(mDir, `# M001: Timestamp Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
159
+ const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
160
+ // completed_at is 2 days in the future
161
+ const futureDate = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString();
162
+ writeFileSync(
163
+ join(sDir, "tasks", "T01-SUMMARY.md"),
164
+ `---\nstatus: done\ncompleted_at: ${futureDate}\n---\n# T01\nDone.\n`,
165
+ );
166
+
167
+ const result = await runGSDDoctor(base, { fix: false });
168
+ assertTrue(
169
+ result.issues.some(i => i.code === "future_timestamp"),
170
+ "detects future completed_at timestamp",
171
+ );
172
+ rmSync(base, { recursive: true, force: true });
173
+ }
174
+
175
+ // ── 9. JSON output format ───────────────────────────────────────────────────
176
+ console.log("\n=== JSON output format ===");
177
+ {
178
+ const { base, mDir } = makeBase();
179
+ writeRoadmap(mDir, `# M001: JSON Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
180
+ writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
181
+
182
+ const result = await runGSDDoctor(base, { fix: false });
183
+ const json = formatDoctorReportJson(result);
184
+
185
+ let parsed: unknown;
186
+ try {
187
+ parsed = JSON.parse(json);
188
+ } catch {
189
+ parsed = null;
190
+ }
191
+
192
+ assertTrue(parsed !== null, "formatDoctorReportJson produces valid JSON");
193
+ assertTrue(typeof (parsed as Record<string, unknown>)?.ok === "boolean", "JSON has ok field");
194
+ assertTrue(Array.isArray((parsed as Record<string, unknown>)?.issues), "JSON has issues array");
195
+ assertTrue(Array.isArray((parsed as Record<string, unknown>)?.fixesApplied), "JSON has fixesApplied array");
196
+ assertTrue(typeof (parsed as Record<string, unknown>)?.generatedAt === "string", "JSON has generatedAt field");
197
+ assertTrue(typeof (parsed as Record<string, unknown>)?.summary === "object", "JSON has summary object");
198
+
199
+ rmSync(base, { recursive: true, force: true });
200
+ }
201
+
202
+ // ── 10. Dry-run mode ────────────────────────────────────────────────────────
203
+ console.log("\n=== dry-run mode ===");
204
+ {
205
+ const { base, mDir } = makeBase();
206
+ writeRoadmap(mDir, `# M001: Dry Run Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
207
+ const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
208
+
209
+ const result = await runGSDDoctor(base, { fix: true, dryRun: true });
210
+ // In dry-run mode, no actual files should be created
211
+ assertTrue(!existsSync(join(sDir, "S01-SUMMARY.md")), "dry-run does not create slice summary");
212
+ assertTrue(
213
+ result.fixesApplied.some(f => f.startsWith("[dry-run]")),
214
+ "dry-run mode reports would-fix entries",
215
+ );
216
+
217
+ rmSync(base, { recursive: true, force: true });
218
+ }
219
+
220
+ // ── 11. Per-check timing ─────────────────────────────────────────────────────
221
+ console.log("\n=== per-check timing ===");
222
+ {
223
+ const { base, mDir } = makeBase();
224
+ writeRoadmap(mDir, `# M001: Timing Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
225
+ writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
226
+
227
+ const result = await runGSDDoctor(base, { fix: false });
228
+ assertTrue(result.timing !== undefined, "report includes timing");
229
+ assertTrue(typeof result.timing?.git === "number", "timing.git is a number");
230
+ assertTrue(typeof result.timing?.runtime === "number", "timing.runtime is a number");
231
+ assertTrue(typeof result.timing?.environment === "number", "timing.environment is a number");
232
+ assertTrue(typeof result.timing?.gsdState === "number", "timing.gsdState is a number");
233
+
234
+ rmSync(base, { recursive: true, force: true });
235
+ }
236
+
237
+ // ── 12. Doctor history ───────────────────────────────────────────────────────
238
+ console.log("\n=== doctor history ===");
239
+ {
240
+ const { base, gsd, mDir } = makeBase();
241
+ writeRoadmap(mDir, `# M001: History Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
242
+ writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
243
+
244
+ await runGSDDoctor(base, { fix: false });
245
+
246
+ const historyPath = join(gsd, "doctor-history.jsonl");
247
+ assertTrue(existsSync(historyPath), "doctor-history.jsonl is created after run");
248
+
249
+ const { readDoctorHistory } = await import("../doctor.js");
250
+ const history = await readDoctorHistory(base);
251
+ assertTrue(history.length >= 1, "history has at least one entry");
252
+ assertTrue(typeof history[0]?.ts === "string", "history entry has ts field");
253
+ assertTrue(typeof history[0]?.ok === "boolean", "history entry has ok field");
254
+ assertTrue(typeof history[0]?.errors === "number", "history entry has errors count");
255
+ assertTrue(Array.isArray(history[0]?.codes), "history entry has codes array");
256
+
257
+ rmSync(base, { recursive: true, force: true });
258
+ }
259
+
260
+ report();
261
+ }
262
+
263
+ main().catch(err => {
264
+ console.error(err);
265
+ process.exit(1);
266
+ });
@@ -47,6 +47,18 @@ function withEnv(vars: Record<string, string | undefined>, fn: () => void): void
47
47
  }
48
48
  }
49
49
 
50
+ function withCwd(nextCwd: string, fn: () => void): void {
51
+ const saved = process.cwd();
52
+ process.chdir(nextCwd);
53
+ try {
54
+ fn();
55
+ } finally {
56
+ process.chdir(saved);
57
+ }
58
+ }
59
+
60
+ const PRESENT_TEST_VALUE = "configured";
61
+
50
62
  // ─── formatProviderReport ─────────────────────────────────────────────────────
51
63
 
52
64
  test("formatProviderReport returns fallback for empty results", () => {
@@ -184,7 +196,7 @@ test("runProviderChecks detects Anthropic key from ANTHROPIC_API_KEY env var", (
184
196
  // Isolate from real HOME so loadEffectiveGSDPreferences returns null (default → anthropic)
185
197
  // and auth.json lookups hit an empty directory.
186
198
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-env-test-")));
187
- withEnv({ ANTHROPIC_API_KEY: "sk-ant-test-key", HOME: tmpHome }, () => {
199
+ withEnv({ ANTHROPIC_API_KEY: "sk-ant-test-key", ANTHROPIC_OAUTH_TOKEN: undefined, HOME: tmpHome }, () => {
188
200
  try {
189
201
  const results = runProviderChecks();
190
202
  const anthropic = results.find(r => r.name === "anthropic");
@@ -199,7 +211,15 @@ test("runProviderChecks detects Anthropic key from ANTHROPIC_API_KEY env var", (
199
211
 
200
212
  test("runProviderChecks returns error for Anthropic when no key present", () => {
201
213
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
202
- withEnv({ ANTHROPIC_API_KEY: undefined, HOME: tmpHome }, () => {
214
+ withEnv({
215
+ ANTHROPIC_API_KEY: undefined,
216
+ ANTHROPIC_OAUTH_TOKEN: undefined,
217
+ // Clear cross-provider routing env vars (GitHub Copilot can serve Claude models)
218
+ COPILOT_GITHUB_TOKEN: undefined,
219
+ GH_TOKEN: undefined,
220
+ GITHUB_TOKEN: undefined,
221
+ HOME: tmpHome,
222
+ }, () => {
203
223
  try {
204
224
  const results = runProviderChecks();
205
225
  const anthropic = results.find(r => r.name === "anthropic");
@@ -275,7 +295,7 @@ test("runProviderChecks detects key from auth.json", () => {
275
295
  });
276
296
 
277
297
  test("runProviderChecks ignores empty placeholder keys in auth.json", () => {
278
- withEnv({ ANTHROPIC_API_KEY: undefined }, () => {
298
+ withEnv({ ANTHROPIC_API_KEY: undefined, ANTHROPIC_OAUTH_TOKEN: undefined, COPILOT_GITHUB_TOKEN: undefined, GH_TOKEN: undefined, GITHUB_TOKEN: undefined }, () => {
279
299
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
280
300
  const agentDir = join(tmpHome, ".gsd", "agent");
281
301
  mkdirSync(agentDir, { recursive: true });
@@ -296,3 +316,171 @@ test("runProviderChecks ignores empty placeholder keys in auth.json", () => {
296
316
  rmSync(tmpHome, { recursive: true, force: true });
297
317
  });
298
318
  });
319
+
320
+ // ─── runProviderChecks — cross-provider routing ──────────────────────────────
321
+
322
+ test("runProviderChecks reports ok for Anthropic when GitHub Copilot env var is set", () => {
323
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-copilot-test-")));
324
+ withEnv({
325
+ ANTHROPIC_API_KEY: undefined,
326
+ ANTHROPIC_OAUTH_TOKEN: undefined,
327
+ COPILOT_GITHUB_TOKEN: PRESENT_TEST_VALUE,
328
+ GH_TOKEN: undefined,
329
+ GITHUB_TOKEN: undefined,
330
+ HOME: tmpHome,
331
+ }, () => {
332
+ try {
333
+ const results = runProviderChecks();
334
+ const anthropic = results.find(r => r.name === "anthropic");
335
+ assert.ok(anthropic, "anthropic result should exist");
336
+ assert.equal(anthropic!.status, "ok", "should be ok when Copilot auth is available");
337
+ assert.ok(anthropic!.message.includes("GitHub Copilot"), "should mention cross-provider source");
338
+ } finally {
339
+ rmSync(tmpHome, { recursive: true, force: true });
340
+ }
341
+ });
342
+ });
343
+
344
+ test("runProviderChecks reports ok for Anthropic via GITHUB_TOKEN cross-provider routing", () => {
345
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-ghtoken-test-")));
346
+ withEnv({
347
+ ANTHROPIC_API_KEY: undefined,
348
+ ANTHROPIC_OAUTH_TOKEN: undefined,
349
+ COPILOT_GITHUB_TOKEN: undefined,
350
+ GH_TOKEN: undefined,
351
+ GITHUB_TOKEN: PRESENT_TEST_VALUE,
352
+ HOME: tmpHome,
353
+ }, () => {
354
+ try {
355
+ const results = runProviderChecks();
356
+ const anthropic = results.find(r => r.name === "anthropic");
357
+ assert.ok(anthropic, "anthropic result should exist");
358
+ assert.equal(anthropic!.status, "ok", "should be ok when GITHUB_TOKEN provides Copilot access");
359
+ } finally {
360
+ rmSync(tmpHome, { recursive: true, force: true });
361
+ }
362
+ });
363
+ });
364
+
365
+ test("runProviderChecks detects ANTHROPIC_OAUTH_TOKEN as valid Anthropic auth", () => {
366
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-oauth-test-")));
367
+ withEnv({
368
+ ANTHROPIC_API_KEY: undefined,
369
+ ANTHROPIC_OAUTH_TOKEN: PRESENT_TEST_VALUE,
370
+ COPILOT_GITHUB_TOKEN: undefined,
371
+ GH_TOKEN: undefined,
372
+ GITHUB_TOKEN: undefined,
373
+ HOME: tmpHome,
374
+ }, () => {
375
+ try {
376
+ const results = runProviderChecks();
377
+ const anthropic = results.find(r => r.name === "anthropic");
378
+ assert.ok(anthropic, "anthropic result should exist");
379
+ assert.equal(anthropic!.status, "ok", "should be ok when ANTHROPIC_OAUTH_TOKEN is set");
380
+ assert.ok(anthropic!.message.includes("env"), "should report env source");
381
+ } finally {
382
+ rmSync(tmpHome, { recursive: true, force: true });
383
+ }
384
+ });
385
+ });
386
+
387
+ test("runProviderChecks reports ok via Copilot auth.json for Anthropic", () => {
388
+ withEnv({
389
+ ANTHROPIC_API_KEY: undefined,
390
+ ANTHROPIC_OAUTH_TOKEN: undefined,
391
+ COPILOT_GITHUB_TOKEN: undefined,
392
+ GH_TOKEN: undefined,
393
+ GITHUB_TOKEN: undefined,
394
+ }, () => {
395
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-copilot-auth-test-")));
396
+ const agentDir = join(tmpHome, ".gsd", "agent");
397
+ mkdirSync(agentDir, { recursive: true });
398
+
399
+ // GitHub Copilot OAuth in auth.json
400
+ const authData = {
401
+ "github-copilot": { type: "oauth", apiKey: "ghu_copilot-key", expires: Date.now() + 3_600_000 },
402
+ };
403
+ writeFileSync(join(agentDir, "auth.json"), JSON.stringify(authData));
404
+
405
+ withEnv({ HOME: tmpHome }, () => {
406
+ const results = runProviderChecks();
407
+ const anthropic = results.find(r => r.name === "anthropic");
408
+ assert.ok(anthropic, "anthropic result should exist");
409
+ assert.equal(anthropic!.status, "ok", "should be ok when Copilot is authenticated in auth.json");
410
+ assert.ok(anthropic!.message.includes("GitHub Copilot"), "should mention Copilot as source");
411
+ });
412
+
413
+ rmSync(tmpHome, { recursive: true, force: true });
414
+ });
415
+ });
416
+
417
+ test("runProviderChecks uses provider-qualified anthropic-vertex model IDs", () => {
418
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-home-")));
419
+ const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-")));
420
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
421
+ writeFileSync(
422
+ join(repo, ".gsd", "preferences.md"),
423
+ [
424
+ "---",
425
+ "models:",
426
+ " execution: anthropic-vertex/claude-sonnet-4-6",
427
+ "---",
428
+ "",
429
+ ].join("\n"),
430
+ );
431
+
432
+ withEnv({
433
+ HOME: tmpHome,
434
+ ANTHROPIC_API_KEY: undefined,
435
+ ANTHROPIC_OAUTH_TOKEN: undefined,
436
+ ANTHROPIC_VERTEX_PROJECT_ID: "vertex-project",
437
+ }, () => {
438
+ withCwd(repo, () => {
439
+ const results = runProviderChecks();
440
+ const vertex = results.find(r => r.name === "anthropic-vertex");
441
+ const anthropic = results.find(r => r.name === "anthropic");
442
+ assert.ok(vertex, "anthropic-vertex result should exist");
443
+ assert.equal(vertex!.status, "ok", "should accept ANTHROPIC_VERTEX_PROJECT_ID as configured");
444
+ assert.ok(!anthropic || !anthropic.required, "plain anthropic should not be required for anthropic-vertex config");
445
+ });
446
+ });
447
+
448
+ rmSync(repo, { recursive: true, force: true });
449
+ rmSync(tmpHome, { recursive: true, force: true });
450
+ });
451
+
452
+ test("runProviderChecks uses object provider field for anthropic-vertex models", () => {
453
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-home-")));
454
+ const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-")));
455
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
456
+ writeFileSync(
457
+ join(repo, ".gsd", "preferences.md"),
458
+ [
459
+ "---",
460
+ "models:",
461
+ " execution:",
462
+ " model: claude-sonnet-4-6",
463
+ " provider: anthropic-vertex",
464
+ "---",
465
+ "",
466
+ ].join("\n"),
467
+ );
468
+
469
+ withEnv({
470
+ HOME: tmpHome,
471
+ ANTHROPIC_API_KEY: undefined,
472
+ ANTHROPIC_OAUTH_TOKEN: undefined,
473
+ ANTHROPIC_VERTEX_PROJECT_ID: undefined,
474
+ }, () => {
475
+ withCwd(repo, () => {
476
+ const results = runProviderChecks();
477
+ const vertex = results.find(r => r.name === "anthropic-vertex");
478
+ assert.ok(vertex, "anthropic-vertex result should exist");
479
+ assert.equal(vertex!.status, "error", "missing vertex config should be reported against anthropic-vertex");
480
+ assert.ok(vertex!.detail?.includes("ANTHROPIC_VERTEX_PROJECT_ID"), "should point to vertex setup");
481
+ });
482
+ });
483
+
484
+ rmSync(repo, { recursive: true, force: true });
485
+ rmSync(tmpHome, { recursive: true, force: true });
486
+ });