gsd-pi 2.37.1 → 2.38.0-dev.29edcdc

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 (239) 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/resource-loader.js +34 -1
  10. package/dist/resources/extensions/browser-tools/package.json +3 -1
  11. package/dist/resources/extensions/cmux/index.js +55 -1
  12. package/dist/resources/extensions/context7/package.json +1 -1
  13. package/dist/resources/extensions/env-utils.js +29 -0
  14. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  15. package/dist/resources/extensions/github-sync/cli.js +284 -0
  16. package/dist/resources/extensions/github-sync/index.js +73 -0
  17. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  18. package/dist/resources/extensions/github-sync/sync.js +424 -0
  19. package/dist/resources/extensions/github-sync/templates.js +118 -0
  20. package/dist/resources/extensions/github-sync/types.js +7 -0
  21. package/dist/resources/extensions/google-search/package.json +3 -1
  22. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  23. package/dist/resources/extensions/gsd/auto-dispatch.js +75 -10
  24. package/dist/resources/extensions/gsd/auto-loop.js +597 -588
  25. package/dist/resources/extensions/gsd/auto-post-unit.js +111 -68
  26. package/dist/resources/extensions/gsd/auto-prompts.js +114 -45
  27. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  28. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  29. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  30. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  31. package/dist/resources/extensions/gsd/auto.js +143 -96
  32. package/dist/resources/extensions/gsd/captures.js +9 -1
  33. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  34. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  35. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  36. package/dist/resources/extensions/gsd/commands.js +24 -3
  37. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  38. package/dist/resources/extensions/gsd/detection.js +1 -2
  39. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  40. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  41. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  42. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  43. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  44. package/dist/resources/extensions/gsd/doctor.js +204 -12
  45. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  46. package/dist/resources/extensions/gsd/export.js +1 -1
  47. package/dist/resources/extensions/gsd/files.js +47 -2
  48. package/dist/resources/extensions/gsd/forensics.js +1 -1
  49. package/dist/resources/extensions/gsd/git-service.js +15 -12
  50. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  51. package/dist/resources/extensions/gsd/index.js +24 -20
  52. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  53. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  54. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  55. package/dist/resources/extensions/gsd/package.json +1 -1
  56. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  57. package/dist/resources/extensions/gsd/preferences-types.js +3 -2
  58. package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
  59. package/dist/resources/extensions/gsd/preferences.js +8 -5
  60. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  61. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  62. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  63. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  64. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  68. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  69. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  70. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  71. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  72. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  73. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  74. package/dist/resources/extensions/gsd/state.js +1 -1
  75. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  76. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  77. package/dist/resources/extensions/gsd/worktree.js +35 -16
  78. package/dist/resources/extensions/mcp-client/index.js +14 -1
  79. package/dist/resources/extensions/remote-questions/status.js +2 -1
  80. package/dist/resources/extensions/remote-questions/store.js +2 -1
  81. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  82. package/dist/resources/extensions/subagent/index.js +12 -3
  83. package/dist/resources/extensions/subagent/isolation.js +2 -1
  84. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  85. package/dist/resources/extensions/universal-config/package.json +1 -1
  86. package/dist/welcome-screen.d.ts +12 -0
  87. package/dist/welcome-screen.js +53 -0
  88. package/package.json +2 -1
  89. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  90. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  91. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  92. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  93. package/packages/pi-ai/dist/models.generated.js +172 -0
  94. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  95. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  96. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  97. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  98. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  99. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  100. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  101. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  102. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  103. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  104. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  106. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  109. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  110. package/packages/pi-ai/dist/types.d.ts +2 -2
  111. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  112. package/packages/pi-ai/dist/types.js.map +1 -1
  113. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  114. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  115. package/packages/pi-ai/package.json +1 -0
  116. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  117. package/packages/pi-ai/src/models.generated.ts +172 -0
  118. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  119. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  120. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  121. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  122. package/packages/pi-ai/src/types.ts +2 -0
  123. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  124. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  126. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  129. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  132. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  133. package/packages/pi-coding-agent/package.json +1 -1
  134. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  135. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  136. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  137. package/pkg/package.json +1 -1
  138. package/src/resources/extensions/cmux/index.ts +57 -1
  139. package/src/resources/extensions/env-utils.ts +31 -0
  140. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  141. package/src/resources/extensions/github-sync/cli.ts +364 -0
  142. package/src/resources/extensions/github-sync/index.ts +93 -0
  143. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  144. package/src/resources/extensions/github-sync/sync.ts +556 -0
  145. package/src/resources/extensions/github-sync/templates.ts +183 -0
  146. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  147. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  148. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  149. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  150. package/src/resources/extensions/github-sync/types.ts +47 -0
  151. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  152. package/src/resources/extensions/gsd/auto-dispatch.ts +100 -9
  153. package/src/resources/extensions/gsd/auto-loop.ts +484 -546
  154. package/src/resources/extensions/gsd/auto-post-unit.ts +92 -42
  155. package/src/resources/extensions/gsd/auto-prompts.ts +150 -48
  156. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  157. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  158. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  159. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  160. package/src/resources/extensions/gsd/auto.ts +139 -101
  161. package/src/resources/extensions/gsd/captures.ts +10 -1
  162. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  163. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  164. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  165. package/src/resources/extensions/gsd/commands.ts +26 -4
  166. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  167. package/src/resources/extensions/gsd/detection.ts +2 -2
  168. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  169. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  170. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  171. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  172. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  173. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  174. package/src/resources/extensions/gsd/doctor.ts +199 -14
  175. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  176. package/src/resources/extensions/gsd/export.ts +1 -1
  177. package/src/resources/extensions/gsd/files.ts +50 -3
  178. package/src/resources/extensions/gsd/forensics.ts +1 -1
  179. package/src/resources/extensions/gsd/git-service.ts +20 -10
  180. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  181. package/src/resources/extensions/gsd/index.ts +24 -17
  182. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  183. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  184. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  185. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  186. package/src/resources/extensions/gsd/preferences-types.ts +9 -5
  187. package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
  188. package/src/resources/extensions/gsd/preferences.ts +8 -5
  189. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  190. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  191. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  192. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  193. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  194. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  195. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  196. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  197. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  198. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  199. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  200. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  201. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  202. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  203. package/src/resources/extensions/gsd/state.ts +1 -1
  204. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  205. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  206. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  207. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  208. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  209. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  210. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  211. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  212. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  213. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  214. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  215. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  216. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  217. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  218. package/src/resources/extensions/gsd/types.ts +43 -1
  219. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  220. package/src/resources/extensions/gsd/worktree.ts +35 -15
  221. package/src/resources/extensions/mcp-client/index.ts +17 -1
  222. package/src/resources/extensions/remote-questions/status.ts +3 -1
  223. package/src/resources/extensions/remote-questions/store.ts +3 -1
  224. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  225. package/src/resources/extensions/subagent/index.ts +12 -3
  226. package/src/resources/extensions/subagent/isolation.ts +3 -1
  227. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  228. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  229. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  230. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  231. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  232. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  233. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  234. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  235. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  236. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  237. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  238. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  239. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -355,6 +355,63 @@ function checkGitRemote(basePath) {
355
355
  }
356
356
  return { name: "git_remote", status: "ok", message: "Git remote reachable" };
357
357
  }
358
+ /**
359
+ * Check if the project build passes (opt-in slow check, use --build flag).
360
+ * Runs npm run build and reports failure as env_build.
361
+ */
362
+ function checkBuildHealth(basePath) {
363
+ const pkgPath = join(basePath, "package.json");
364
+ if (!existsSync(pkgPath))
365
+ return null;
366
+ try {
367
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
368
+ const buildScript = pkg.scripts?.build;
369
+ if (!buildScript)
370
+ return null;
371
+ const result = tryExec("npm run build 2>&1", basePath);
372
+ if (result === null) {
373
+ return {
374
+ name: "build",
375
+ status: "error",
376
+ message: "Build failed — npm run build exited non-zero",
377
+ detail: "Fix build errors before dispatching work",
378
+ };
379
+ }
380
+ return { name: "build", status: "ok", message: "Build passes" };
381
+ }
382
+ catch {
383
+ return null;
384
+ }
385
+ }
386
+ /**
387
+ * Check if tests pass (opt-in slow check, use --test flag).
388
+ * Runs npm test and reports failures as env_test.
389
+ */
390
+ function checkTestHealth(basePath) {
391
+ const pkgPath = join(basePath, "package.json");
392
+ if (!existsSync(pkgPath))
393
+ return null;
394
+ try {
395
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
396
+ const testScript = pkg.scripts?.test;
397
+ // Skip if no test script or the default placeholder
398
+ if (!testScript || testScript.includes("no test specified"))
399
+ return null;
400
+ const result = tryExec("npm test 2>&1", basePath);
401
+ if (result === null) {
402
+ return {
403
+ name: "test",
404
+ status: "warning",
405
+ message: "Tests failing — npm test exited non-zero",
406
+ detail: "Fix failing tests before shipping",
407
+ };
408
+ }
409
+ return { name: "test", status: "ok", message: "Tests pass" };
410
+ }
411
+ catch {
412
+ return null;
413
+ }
414
+ }
358
415
  // ── Public API ─────────────────────────────────────────────────────────────
359
416
  /**
360
417
  * Run all environment health checks. Returns structured results for
@@ -394,6 +451,24 @@ export function runFullEnvironmentChecks(basePath) {
394
451
  results.push(remoteCheck);
395
452
  return results;
396
453
  }
454
+ /**
455
+ * Run slow opt-in checks (build and/or test).
456
+ * These are never run on the pre-dispatch gate — only on explicit /gsd doctor --build/--test.
457
+ */
458
+ export function runSlowEnvironmentChecks(basePath, options) {
459
+ const results = [];
460
+ if (options?.includeBuild) {
461
+ const buildCheck = checkBuildHealth(basePath);
462
+ if (buildCheck)
463
+ results.push(buildCheck);
464
+ }
465
+ if (options?.includeTests) {
466
+ const testCheck = checkTestHealth(basePath);
467
+ if (testCheck)
468
+ results.push(testCheck);
469
+ }
470
+ return results;
471
+ }
397
472
  /**
398
473
  * Convert environment check results to DoctorIssue format for the doctor pipeline.
399
474
  */
@@ -417,6 +492,9 @@ export async function checkEnvironmentHealth(basePath, issues, options) {
417
492
  const results = options?.includeRemote
418
493
  ? runFullEnvironmentChecks(basePath)
419
494
  : runEnvironmentChecks(basePath);
495
+ if (options?.includeBuild || options?.includeTests) {
496
+ results.push(...runSlowEnvironmentChecks(basePath, options));
497
+ }
420
498
  issues.push(...environmentResultsToDoctorIssues(results));
421
499
  }
422
500
  /**
@@ -69,3 +69,18 @@ export function formatDoctorIssuesForPrompt(issues) {
69
69
  return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
70
70
  }).join("\n");
71
71
  }
72
+ /**
73
+ * Serialize a doctor report to JSON — suitable for CI/tooling integration.
74
+ * Usage: /gsd doctor --json
75
+ */
76
+ export function formatDoctorReportJson(report) {
77
+ return JSON.stringify({
78
+ ok: report.ok,
79
+ basePath: report.basePath,
80
+ generatedAt: new Date().toISOString(),
81
+ summary: summarizeDoctorIssues(report.issues),
82
+ issues: report.issues,
83
+ fixesApplied: report.fixesApplied,
84
+ ...(report.timing ? { timing: report.timing } : {}),
85
+ }, null, 2);
86
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { existsSync } from "node:fs";
14
14
  import { AuthStorage } from "@gsd/pi-coding-agent";
15
+ import { getEnvApiKey } from "@gsd/pi-ai";
15
16
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
17
  import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
17
18
  // ── Model → Provider ID mapping ───────────────────────────────────────────────
@@ -27,12 +28,15 @@ function modelToProviderId(model) {
27
28
  const prefix = model.split("/")[0].toLowerCase();
28
29
  // Map known prefixes to registry IDs
29
30
  const prefixMap = {
31
+ "anthropic-vertex": "anthropic-vertex",
30
32
  openrouter: "openrouter",
31
33
  groq: "groq",
32
34
  mistral: "mistral",
33
35
  google: "google",
36
+ "google-vertex": "google-vertex",
34
37
  anthropic: "anthropic",
35
38
  openai: "openai",
39
+ "github-copilot": "github-copilot",
36
40
  };
37
41
  if (prefixMap[prefix])
38
42
  return prefixMap[prefix];
@@ -65,11 +69,19 @@ function collectConfiguredModelProviders() {
65
69
  }
66
70
  const modelEntries = typeof models === "object" ? Object.values(models) : [];
67
71
  for (const entry of modelEntries) {
68
- const modelId = typeof entry === "string" ? entry
69
- : typeof entry === "object" && entry !== null && "model" in entry
70
- ? String(entry.model)
71
- : null;
72
- if (modelId) {
72
+ if (typeof entry === "string") {
73
+ const pid = modelToProviderId(entry);
74
+ if (pid)
75
+ providers.add(pid);
76
+ continue;
77
+ }
78
+ if (typeof entry === "object" && entry !== null && "model" in entry) {
79
+ const configuredProvider = "provider" in entry ? entry.provider : undefined;
80
+ if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) {
81
+ providers.add(configuredProvider);
82
+ continue;
83
+ }
84
+ const modelId = String(entry.model);
73
85
  const pid = modelToProviderId(modelId);
74
86
  if (pid)
75
87
  providers.add(pid);
@@ -108,31 +120,69 @@ function resolveKey(providerId) {
108
120
  // auth.json malformed — fall through to env check
109
121
  }
110
122
  }
111
- // Check environment variable
123
+ // Check environment variable using the authoritative env var resolution
124
+ // (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
125
+ // COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
126
+ if (getEnvApiKey(providerId)) {
127
+ return { found: true, source: "env", backedOff: false };
128
+ }
129
+ // Fall back to PROVIDER_REGISTRY env var for providers not covered by getEnvApiKey
130
+ // (e.g., search providers like Brave, Tavily; tool providers like Jina, Context7)
112
131
  if (info?.envVar && process.env[info.envVar]) {
113
132
  return { found: true, source: "env", backedOff: false };
114
133
  }
115
134
  return { found: false, source: "none", backedOff: false };
116
135
  }
117
136
  // ── Individual check groups ────────────────────────────────────────────────────
137
+ /**
138
+ * Providers that can serve models normally associated with another provider.
139
+ * Key = the provider whose models can be served, Value = alternative providers to check.
140
+ * e.g. GitHub Copilot subscriptions can access Claude and GPT models.
141
+ */
142
+ const PROVIDER_ROUTES = {
143
+ anthropic: ["github-copilot"],
144
+ openai: ["github-copilot"],
145
+ };
118
146
  function checkLlmProviders() {
119
147
  const required = collectConfiguredModelProviders();
120
148
  const results = [];
121
149
  for (const providerId of required) {
122
150
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
123
- const label = info?.label ?? providerId;
151
+ const label = providerId === "anthropic-vertex"
152
+ ? "Anthropic Vertex"
153
+ : info?.label ?? providerId;
124
154
  const lookup = resolveKey(providerId);
125
155
  if (!lookup.found) {
126
- const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
156
+ // Check if a cross-provider can serve this provider's models
157
+ const routes = PROVIDER_ROUTES[providerId];
158
+ const routeProvider = routes?.find(routeId => resolveKey(routeId).found);
159
+ if (routeProvider) {
160
+ const routeInfo = PROVIDER_REGISTRY.find(p => p.id === routeProvider);
161
+ const routeLabel = routeInfo?.label ?? routeProvider;
162
+ results.push({
163
+ name: providerId,
164
+ label,
165
+ category: "llm",
166
+ status: "ok",
167
+ message: `${label} — available via ${routeLabel}`,
168
+ required: true,
169
+ });
170
+ continue;
171
+ }
172
+ const envVar = providerId === "anthropic-vertex"
173
+ ? "ANTHROPIC_VERTEX_PROJECT_ID"
174
+ : info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
127
175
  results.push({
128
176
  name: providerId,
129
177
  label,
130
178
  category: "llm",
131
179
  status: "error",
132
- message: `${label} — no API key found`,
133
- detail: info?.hasOAuth
134
- ? `Run /gsd keys to authenticate`
135
- : `Set ${envVar} or run /gsd keys`,
180
+ message: `${label} — not configured`,
181
+ detail: providerId === "anthropic-vertex"
182
+ ? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
183
+ : info?.hasOAuth
184
+ ? `Run /gsd keys to authenticate`
185
+ : `Set ${envVar} or run /gsd keys`,
136
186
  required: true,
137
187
  });
138
188
  }
@@ -1,7 +1,7 @@
1
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
4
- import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
4
+ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
5
5
  import { deriveState, isMilestoneComplete } from "./state.js";
6
6
  import { invalidateAllCaches } from "./cache.js";
7
7
  import { loadEffectiveGSDPreferences } from "./preferences.js";
@@ -9,7 +9,7 @@ import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
9
9
  import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
10
10
  import { checkEnvironmentHealth } from "./doctor-environment.js";
11
11
  import { runProviderChecks } from "./doctor-providers.js";
12
- export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt } from "./doctor-format.js";
12
+ export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
13
13
  export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport } from "./doctor-environment.js";
14
14
  export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport } from "./progress-score.js";
15
15
  /**
@@ -257,10 +257,23 @@ async function markSliceDoneInRoadmap(basePath, milestoneId, sliceId, fixesAppli
257
257
  fixesApplied.push(`marked ${sliceId} done in ${roadmapPath}`);
258
258
  }
259
259
  }
260
+ async function markSliceUndoneInRoadmap(basePath, milestoneId, sliceId, fixesApplied) {
261
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
262
+ if (!roadmapPath)
263
+ return;
264
+ const content = await loadFile(roadmapPath);
265
+ if (!content)
266
+ return;
267
+ const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"), `$1[ ] **${sliceId}:`);
268
+ if (updated !== content) {
269
+ await saveFile(roadmapPath, updated);
270
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
271
+ }
272
+ }
260
273
  function matchesScope(unitId, scope) {
261
274
  if (!scope)
262
275
  return true;
263
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
276
+ return unitId === scope || unitId.startsWith(`${scope}/`);
264
277
  }
265
278
  function auditRequirements(content) {
266
279
  if (!content)
@@ -324,10 +337,68 @@ export async function selectDoctorScope(basePath, requestedScope) {
324
337
  }
325
338
  return state.registry[0]?.id;
326
339
  }
340
+ // ── Helper: circular dependency detection ──────────────────────────────────
341
+ function detectCircularDependencies(slices) {
342
+ const known = new Set(slices.map(s => s.id));
343
+ const adj = new Map();
344
+ for (const s of slices)
345
+ adj.set(s.id, s.depends.filter(d => known.has(d)));
346
+ const state = new Map();
347
+ for (const s of slices)
348
+ state.set(s.id, "unvisited");
349
+ const cycles = [];
350
+ function dfs(id, path) {
351
+ const st = state.get(id);
352
+ if (st === "done")
353
+ return;
354
+ if (st === "visiting") {
355
+ cycles.push([...path.slice(path.indexOf(id)), id]);
356
+ return;
357
+ }
358
+ state.set(id, "visiting");
359
+ for (const dep of adj.get(id) ?? [])
360
+ dfs(dep, [...path, id]);
361
+ state.set(id, "done");
362
+ }
363
+ for (const s of slices)
364
+ if (state.get(s.id) === "unvisited")
365
+ dfs(s.id, []);
366
+ return cycles;
367
+ }
368
+ async function appendDoctorHistory(basePath, report) {
369
+ try {
370
+ const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
371
+ const entry = JSON.stringify({
372
+ ts: new Date().toISOString(),
373
+ ok: report.ok,
374
+ errors: report.issues.filter(i => i.severity === "error").length,
375
+ warnings: report.issues.filter(i => i.severity === "warning").length,
376
+ fixes: report.fixesApplied.length,
377
+ codes: [...new Set(report.issues.map(i => i.code))],
378
+ });
379
+ const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
380
+ await saveFile(historyPath, existing + entry + "\n");
381
+ }
382
+ catch { /* non-fatal */ }
383
+ }
384
+ /** Read the last N doctor history entries. Returns most-recent-first. */
385
+ export async function readDoctorHistory(basePath, lastN = 50) {
386
+ try {
387
+ const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
388
+ if (!existsSync(historyPath))
389
+ return [];
390
+ const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
391
+ return lines.slice(-lastN).reverse().map(l => JSON.parse(l));
392
+ }
393
+ catch {
394
+ return [];
395
+ }
396
+ }
327
397
  export async function runGSDDoctor(basePath, options) {
328
398
  const issues = [];
329
399
  const fixesApplied = [];
330
400
  const fix = options?.fix === true;
401
+ const dryRun = options?.dryRun === true;
331
402
  const fixLevel = options?.fixLevel ?? "all";
332
403
  // Issue codes that represent completion state transitions — creating summary
333
404
  // stubs, marking slices/milestones done in the roadmap. These belong to the
@@ -336,12 +407,18 @@ export async function runGSDDoctor(basePath, options) {
336
407
  // detected and reported but never auto-fixed.
337
408
  /** Whether a given issue code should be auto-fixed at the current fixLevel. */
338
409
  const shouldFix = (code) => {
339
- if (!fix)
410
+ if (!fix || dryRun)
340
411
  return false;
341
412
  if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))
342
413
  return false;
343
414
  return true;
344
415
  };
416
+ /** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
417
+ const dryRunCanFix = (code, message) => {
418
+ if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
419
+ fixesApplied.push(`[dry-run] would fix: ${message}`);
420
+ }
421
+ };
345
422
  const prefs = loadEffectiveGSDPreferences();
346
423
  if (prefs) {
347
424
  const prefIssues = validatePreferenceShape(prefs.preferences);
@@ -357,18 +434,30 @@ export async function runGSDDoctor(basePath, options) {
357
434
  });
358
435
  }
359
436
  }
360
- // Git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files)
437
+ // Git health checks timed
438
+ const t0git = Date.now();
361
439
  const isolationMode = options?.isolationMode ??
362
440
  (prefs?.preferences?.git?.isolation === "none" ? "none" :
363
441
  prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
364
442
  await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
365
- // Runtime health checks (crash locks, completed-units, hook state, activity logs, STATE.md, gitignore)
443
+ const gitMs = Date.now() - t0git;
444
+ // Runtime health checks — timed
445
+ const t0runtime = Date.now();
366
446
  await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
367
- // Environment health checks (#1221: missing tools, port conflicts, stale deps, disk space)
368
- await checkEnvironmentHealth(basePath, issues, { includeRemote: !options?.scope });
447
+ const runtimeMs = Date.now() - t0runtime;
448
+ // Environment health checks timed
449
+ const t0env = Date.now();
450
+ await checkEnvironmentHealth(basePath, issues, {
451
+ includeRemote: !options?.scope,
452
+ includeBuild: options?.includeBuild,
453
+ includeTests: options?.includeTests,
454
+ });
455
+ const envMs = Date.now() - t0env;
369
456
  const milestonesPath = milestonesDir(basePath);
370
457
  if (!existsSync(milestonesPath)) {
371
- return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
458
+ const report = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
459
+ await appendDoctorHistory(basePath, report);
460
+ return report;
372
461
  }
373
462
  const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
374
463
  const requirementsContent = await loadFile(requirementsPath);
@@ -432,6 +521,46 @@ export async function runGSDDoctor(basePath, options) {
432
521
  if (!roadmapContent)
433
522
  continue;
434
523
  const roadmap = parseRoadmap(roadmapContent);
524
+ // ── Circular dependency detection ──────────────────────────────────────
525
+ for (const cycle of detectCircularDependencies(roadmap.slices)) {
526
+ issues.push({
527
+ severity: "error",
528
+ code: "circular_slice_dependency",
529
+ scope: "milestone",
530
+ unitId: milestoneId,
531
+ message: `Circular dependency detected: ${cycle.join(" → ")}`,
532
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
533
+ fixable: false,
534
+ });
535
+ }
536
+ // ── Orphaned slice directories ─────────────────────────────────────────
537
+ try {
538
+ const slicesDir = join(milestonePath, "slices");
539
+ if (existsSync(slicesDir)) {
540
+ const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
541
+ for (const entry of readdirSync(slicesDir)) {
542
+ try {
543
+ if (!lstatSync(join(slicesDir, entry)).isDirectory())
544
+ continue;
545
+ }
546
+ catch {
547
+ continue;
548
+ }
549
+ if (!knownSliceIds.has(entry)) {
550
+ issues.push({
551
+ severity: "warning",
552
+ code: "orphaned_slice_directory",
553
+ scope: "milestone",
554
+ unitId: milestoneId,
555
+ message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
556
+ file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
557
+ fixable: false,
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ catch { /* non-fatal */ }
435
564
  for (const slice of roadmap.slices) {
436
565
  const unitId = `${milestoneId}/${slice.id}`;
437
566
  if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId)
@@ -502,6 +631,34 @@ export async function runGSDDoctor(basePath, options) {
502
631
  }
503
632
  continue;
504
633
  }
634
+ // ── Duplicate task IDs ───────────────────────────────────────────────
635
+ const taskIdCounts = new Map();
636
+ for (const task of plan.tasks)
637
+ taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
638
+ for (const [taskId, count] of taskIdCounts) {
639
+ if (count > 1) {
640
+ issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
641
+ message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
642
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
643
+ }
644
+ }
645
+ // ── Task files on disk not in plan ────────────────────────────────────
646
+ try {
647
+ if (tasksDir) {
648
+ const planTaskIds = new Set(plan.tasks.map(t => t.id));
649
+ for (const f of readdirSync(tasksDir)) {
650
+ if (!f.endsWith("-SUMMARY.md"))
651
+ continue;
652
+ const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
653
+ if (!planTaskIds.has(diskTaskId)) {
654
+ issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
655
+ message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
656
+ file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
657
+ }
658
+ }
659
+ }
660
+ }
661
+ catch { /* non-fatal */ }
505
662
  let allTasksDone = plan.tasks.length > 0;
506
663
  for (const task of plan.tasks) {
507
664
  const taskUnitId = `${unitId}/${task.id}`;
@@ -517,6 +674,7 @@ export async function runGSDDoctor(basePath, options) {
517
674
  file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
518
675
  fixable: true,
519
676
  });
677
+ dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
520
678
  if (shouldFix("task_done_missing_summary")) {
521
679
  const stubPath = join(basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks", `${task.id}-SUMMARY.md`);
522
680
  const stubContent = [
@@ -575,6 +733,22 @@ export async function runGSDDoctor(basePath, options) {
575
733
  }
576
734
  }
577
735
  }
736
+ // ── Future timestamp check ─────────────────────────────────────
737
+ if (task.done && hasSummary && summaryPath) {
738
+ try {
739
+ const rawSummary = await loadFile(summaryPath);
740
+ const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
741
+ if (m) {
742
+ const ts = new Date(m[1].trim());
743
+ if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
744
+ issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
745
+ message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
746
+ file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
747
+ }
748
+ }
749
+ }
750
+ catch { /* non-fatal */ }
751
+ }
578
752
  allTasksDone = allTasksDone && task.done;
579
753
  }
580
754
  // Blocker-without-replan detection
@@ -604,6 +778,12 @@ export async function runGSDDoctor(basePath, options) {
604
778
  }
605
779
  }
606
780
  }
781
+ // ── Stale REPLAN: exists but all tasks done ────────────────────────
782
+ if (replanPath && allTasksDone) {
783
+ issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
784
+ message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
785
+ file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
786
+ }
607
787
  const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
608
788
  const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
609
789
  const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
@@ -618,6 +798,7 @@ export async function runGSDDoctor(basePath, options) {
618
798
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
619
799
  fixable: true,
620
800
  });
801
+ dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
621
802
  if (shouldFix("all_tasks_done_missing_slice_summary"))
622
803
  await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
623
804
  }
@@ -631,6 +812,7 @@ export async function runGSDDoctor(basePath, options) {
631
812
  file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
632
813
  fixable: true,
633
814
  });
815
+ dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
634
816
  if (shouldFix("all_tasks_done_missing_slice_uat"))
635
817
  await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
636
818
  }
@@ -644,6 +826,7 @@ export async function runGSDDoctor(basePath, options) {
644
826
  file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
645
827
  fixable: true,
646
828
  });
829
+ dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
647
830
  if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
648
831
  await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
649
832
  }
@@ -658,6 +841,12 @@ export async function runGSDDoctor(basePath, options) {
658
841
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
659
842
  fixable: true,
660
843
  });
844
+ if (!allTasksDone) {
845
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
846
+ if (shouldFix("slice_checked_missing_summary")) {
847
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
848
+ }
849
+ }
661
850
  }
662
851
  if (slice.done && !hasSliceUat) {
663
852
  issues.push({
@@ -696,13 +885,16 @@ export async function runGSDDoctor(basePath, options) {
696
885
  });
697
886
  }
698
887
  }
699
- if (fix && fixesApplied.length > 0) {
888
+ if (fix && !dryRun && fixesApplied.length > 0) {
700
889
  await updateStateFile(basePath, fixesApplied);
701
890
  }
702
- return {
891
+ const report = {
703
892
  ok: issues.every(issue => issue.severity !== "error"),
704
893
  basePath,
705
894
  issues,
706
895
  fixesApplied,
896
+ timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
707
897
  };
898
+ await appendDoctorHistory(basePath, report);
899
+ return report;
708
900
  }
@@ -1,9 +1,10 @@
1
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
1
2
  export function registerExitCommand(pi, deps = {}) {
2
3
  pi.registerCommand("exit", {
3
4
  description: "Exit GSD gracefully",
4
5
  handler: async (_args, ctx) => {
5
6
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
6
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
7
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule(import.meta.url, "./auto.js")).stopAuto;
7
8
  await stopAuto(ctx, pi, "Graceful exit");
8
9
  ctx.shutdown();
9
10
  },
@@ -5,7 +5,7 @@ import { join, basename } from "node:path";
5
5
  import { exec } from "node:child_process";
6
6
  import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk, } from "./metrics.js";
7
7
  import { gsdRoot } from "./paths.js";
8
- import { formatDuration, fileLink } from "../shared/mod.js";
8
+ import { formatDuration, fileLink } from "../shared/format-utils.js";
9
9
  import { getErrorMessage } from "./error-utils.js";
10
10
  /**
11
11
  * Open a file in the user's default browser.