gsd-pi 2.37.1 → 2.38.0-dev.e40f839

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 (155) 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-dispatch.js +67 -1
  16. package/dist/resources/extensions/gsd/auto-loop.js +7 -1
  17. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  18. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  19. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  20. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  22. package/dist/resources/extensions/gsd/captures.js +9 -1
  23. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  24. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  25. package/dist/resources/extensions/gsd/commands.js +22 -2
  26. package/dist/resources/extensions/gsd/detection.js +1 -2
  27. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  28. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  29. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  30. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  31. package/dist/resources/extensions/gsd/doctor.js +184 -11
  32. package/dist/resources/extensions/gsd/export.js +1 -1
  33. package/dist/resources/extensions/gsd/files.js +43 -2
  34. package/dist/resources/extensions/gsd/forensics.js +1 -1
  35. package/dist/resources/extensions/gsd/index.js +2 -1
  36. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  37. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  38. package/dist/resources/extensions/gsd/package.json +1 -1
  39. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  40. package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
  41. package/dist/resources/extensions/gsd/preferences.js +4 -3
  42. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  43. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  44. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  45. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  46. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  47. package/dist/resources/extensions/gsd/state.js +1 -1
  48. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  49. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  50. package/dist/resources/extensions/gsd/worktree.js +35 -16
  51. package/dist/resources/extensions/remote-questions/status.js +2 -1
  52. package/dist/resources/extensions/remote-questions/store.js +2 -1
  53. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  54. package/dist/resources/extensions/subagent/index.js +12 -3
  55. package/dist/resources/extensions/subagent/isolation.js +2 -1
  56. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  57. package/dist/resources/extensions/universal-config/package.json +1 -1
  58. package/dist/welcome-screen.d.ts +12 -0
  59. package/dist/welcome-screen.js +53 -0
  60. package/package.json +2 -1
  61. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  62. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  63. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  64. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  65. package/packages/pi-ai/dist/models.generated.js +172 -0
  66. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  67. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  68. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  69. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  70. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  71. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  72. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  73. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  74. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  75. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  76. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  78. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  79. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  81. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  82. package/packages/pi-ai/dist/types.d.ts +2 -2
  83. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  84. package/packages/pi-ai/dist/types.js.map +1 -1
  85. package/packages/pi-ai/package.json +1 -0
  86. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  87. package/packages/pi-ai/src/models.generated.ts +172 -0
  88. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  89. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  90. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  91. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  92. package/packages/pi-ai/src/types.ts +2 -0
  93. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  98. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  99. package/packages/pi-coding-agent/package.json +1 -1
  100. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  101. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  102. package/pkg/package.json +1 -1
  103. package/src/resources/extensions/cmux/index.ts +57 -1
  104. package/src/resources/extensions/env-utils.ts +31 -0
  105. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  106. package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
  107. package/src/resources/extensions/gsd/auto-loop.ts +13 -1
  108. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  109. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  110. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  111. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  113. package/src/resources/extensions/gsd/captures.ts +10 -1
  114. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  115. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  116. package/src/resources/extensions/gsd/commands.ts +24 -2
  117. package/src/resources/extensions/gsd/detection.ts +2 -2
  118. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  119. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  120. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  121. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  122. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  123. package/src/resources/extensions/gsd/doctor.ts +177 -13
  124. package/src/resources/extensions/gsd/export.ts +1 -1
  125. package/src/resources/extensions/gsd/files.ts +47 -2
  126. package/src/resources/extensions/gsd/forensics.ts +1 -1
  127. package/src/resources/extensions/gsd/index.ts +3 -1
  128. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  129. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  130. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  131. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  132. package/src/resources/extensions/gsd/preferences.ts +5 -3
  133. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  134. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  135. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  136. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  137. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  138. package/src/resources/extensions/gsd/state.ts +1 -1
  139. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  140. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  141. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  142. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  143. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  144. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  145. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  146. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  147. package/src/resources/extensions/gsd/types.ts +43 -0
  148. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  149. package/src/resources/extensions/gsd/worktree.ts +35 -15
  150. package/src/resources/extensions/remote-questions/status.ts +3 -1
  151. package/src/resources/extensions/remote-questions/store.ts +3 -1
  152. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  153. package/src/resources/extensions/subagent/index.ts +12 -3
  154. package/src/resources/extensions/subagent/isolation.ts +3 -1
  155. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
@@ -10,6 +10,8 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { gsdRoot } from "./paths.js";
13
+
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
13
15
  import { enableDebug } from "./debug-logger.js";
14
16
  import { deriveState } from "./state.js";
15
17
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
@@ -159,7 +161,7 @@ async function guardRemoteSession(
159
161
 
160
162
  export function registerGSDCommand(pi: ExtensionAPI): void {
161
163
  pi.registerCommand("gsd", {
162
- description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
164
+ description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update",
163
165
  getArgumentCompletions: (prefix: string) => {
164
166
  const subcommands = [
165
167
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -210,7 +212,11 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
210
212
  { cmd: "templates", desc: "List available workflow templates" },
211
213
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
212
214
  ];
215
+ const hasTrailingSpace = prefix.endsWith(" ");
213
216
  const parts = prefix.trim().split(/\s+/);
217
+ if (hasTrailingSpace && parts.length >= 1) {
218
+ parts.push("");
219
+ }
214
220
 
215
221
  if (parts.length <= 1) {
216
222
  return subcommands
@@ -478,7 +484,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
478
484
  if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
479
485
  const idPrefix = parts[2] ?? "";
480
486
  try {
481
- const extDir = join(homedir(), ".gsd", "agent", "extensions");
487
+ const extDir = join(gsdHome, "agent", "extensions");
482
488
  const ids: { id: string; name: string }[] = [];
483
489
  for (const entry of readdirSync(extDir, { withFileTypes: true })) {
484
490
  if (!entry.isDirectory()) continue;
@@ -509,6 +515,10 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
509
515
  { cmd: "fix", desc: "Auto-fix detected issues" },
510
516
  { cmd: "heal", desc: "AI-driven deep healing" },
511
517
  { cmd: "audit", desc: "Run health audit without fixing" },
518
+ { cmd: "--dry-run", desc: "Show what --fix would change without applying" },
519
+ { cmd: "--json", desc: "Output report as JSON (CI/tooling friendly)" },
520
+ { cmd: "--build", desc: "Include slow build health check (npm run build)" },
521
+ { cmd: "--test", desc: "Include slow test health check (npm test)" },
512
522
  ];
513
523
 
514
524
  if (parts.length <= 2) {
@@ -536,6 +546,18 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
536
546
  .map((p) => ({ value: `dispatch ${p.cmd}`, label: p.cmd, description: p.desc }));
537
547
  }
538
548
 
549
+ if (parts[0] === "rate" && parts.length <= 2) {
550
+ const tierPrefix = parts[1] ?? "";
551
+ const tiers = [
552
+ { cmd: "over", desc: "Model was overqualified for this task" },
553
+ { cmd: "ok", desc: "Model was appropriate for this task" },
554
+ { cmd: "under", desc: "Model was underqualified for this task" },
555
+ ];
556
+ return tiers
557
+ .filter((t) => t.cmd.startsWith(tierPrefix))
558
+ .map((t) => ({ value: `rate ${t.cmd}`, label: t.cmd, description: t.desc }));
559
+ }
560
+
539
561
  return [];
540
562
  },
541
563
 
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { gsdRoot } from "./paths.js";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Types ──────────────────────────────────────────────────────────────────────
15
17
 
16
18
  export interface ProjectDetection {
@@ -400,7 +402,6 @@ function detectVerificationCommands(
400
402
  * Check if global GSD setup exists (has ~/.gsd/ with preferences).
401
403
  */
402
404
  export function hasGlobalSetup(): boolean {
403
- const gsdHome = join(homedir(), ".gsd");
404
405
  return (
405
406
  existsSync(join(gsdHome, "preferences.md")) ||
406
407
  existsSync(join(gsdHome, "PREFERENCES.md"))
@@ -412,7 +413,6 @@ export function hasGlobalSetup(): boolean {
412
413
  * Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
413
414
  */
414
415
  export function isFirstEverLaunch(): boolean {
415
- const gsdHome = join(homedir(), ".gsd");
416
416
  if (!existsSync(gsdHome)) return true;
417
417
 
418
418
  // If we have preferences, not first launch
@@ -657,6 +657,81 @@ export async function checkRuntimeHealth(
657
657
  } catch {
658
658
  // Non-fatal — external state check failed
659
659
  }
660
+
661
+ // ── Metrics ledger integrity ───────────────────────────────────────────
662
+ try {
663
+ const metricsPath = join(root, "metrics.json");
664
+ if (existsSync(metricsPath)) {
665
+ try {
666
+ const raw = readFileSync(metricsPath, "utf-8");
667
+ const ledger = JSON.parse(raw);
668
+ if (ledger.version !== 1 || !Array.isArray(ledger.units)) {
669
+ issues.push({
670
+ severity: "warning",
671
+ code: "metrics_ledger_corrupt",
672
+ scope: "project",
673
+ unitId: "project",
674
+ message: "metrics.json has an unexpected structure (version !== 1 or units is not an array) — metrics data may be unreliable",
675
+ file: ".gsd/metrics.json",
676
+ fixable: false,
677
+ });
678
+ }
679
+ } catch {
680
+ issues.push({
681
+ severity: "warning",
682
+ code: "metrics_ledger_corrupt",
683
+ scope: "project",
684
+ unitId: "project",
685
+ message: "metrics.json is not valid JSON — metrics data may be corrupt",
686
+ file: ".gsd/metrics.json",
687
+ fixable: false,
688
+ });
689
+ }
690
+ }
691
+ } catch {
692
+ // Non-fatal — metrics check failed
693
+ }
694
+
695
+ // ── Large planning file detection ──────────────────────────────────────
696
+ // Files over 100KB can cause LLM context pressure. Report the worst offenders.
697
+ try {
698
+ const MAX_FILE_BYTES = 100 * 1024; // 100KB
699
+ const milestonesPath = milestonesDir(basePath);
700
+ if (existsSync(milestonesPath)) {
701
+ const largeFiles: Array<{ path: string; sizeKB: number }> = [];
702
+ function scanForLargeFiles(dir: string, depth = 0): void {
703
+ if (depth > 6) return;
704
+ try {
705
+ for (const entry of readdirSync(dir)) {
706
+ const full = join(dir, entry);
707
+ try {
708
+ const s = statSync(full);
709
+ if (s.isDirectory()) { scanForLargeFiles(full, depth + 1); continue; }
710
+ if (entry.endsWith(".md") && s.size > MAX_FILE_BYTES) {
711
+ largeFiles.push({ path: full.replace(basePath + "/", ""), sizeKB: Math.round(s.size / 1024) });
712
+ }
713
+ } catch { /* skip entry */ }
714
+ }
715
+ } catch { /* skip dir */ }
716
+ }
717
+ scanForLargeFiles(milestonesPath);
718
+ if (largeFiles.length > 0) {
719
+ largeFiles.sort((a, b) => b.sizeKB - a.sizeKB);
720
+ const worst = largeFiles[0]!;
721
+ issues.push({
722
+ severity: "warning",
723
+ code: "large_planning_file",
724
+ scope: "project",
725
+ unitId: "project",
726
+ message: `${largeFiles.length} planning file(s) exceed 100KB — largest: ${worst.path} (${worst.sizeKB}KB). Large files cause LLM context pressure.`,
727
+ file: worst.path,
728
+ fixable: false,
729
+ });
730
+ }
731
+ }
732
+ } catch {
733
+ // Non-fatal — large file scan failed
734
+ }
660
735
  }
661
736
 
662
737
  /**
@@ -407,6 +407,63 @@ function checkGitRemote(basePath: string): EnvironmentCheckResult | null {
407
407
  return { name: "git_remote", status: "ok", message: "Git remote reachable" };
408
408
  }
409
409
 
410
+ /**
411
+ * Check if the project build passes (opt-in slow check, use --build flag).
412
+ * Runs npm run build and reports failure as env_build.
413
+ */
414
+ function checkBuildHealth(basePath: string): EnvironmentCheckResult | null {
415
+ const pkgPath = join(basePath, "package.json");
416
+ if (!existsSync(pkgPath)) return null;
417
+
418
+ try {
419
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
420
+ const buildScript = pkg.scripts?.build;
421
+ if (!buildScript) return null;
422
+
423
+ const result = tryExec("npm run build 2>&1", basePath);
424
+ if (result === null) {
425
+ return {
426
+ name: "build",
427
+ status: "error",
428
+ message: "Build failed — npm run build exited non-zero",
429
+ detail: "Fix build errors before dispatching work",
430
+ };
431
+ }
432
+ return { name: "build", status: "ok", message: "Build passes" };
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Check if tests pass (opt-in slow check, use --test flag).
440
+ * Runs npm test and reports failures as env_test.
441
+ */
442
+ function checkTestHealth(basePath: string): EnvironmentCheckResult | null {
443
+ const pkgPath = join(basePath, "package.json");
444
+ if (!existsSync(pkgPath)) return null;
445
+
446
+ try {
447
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
448
+ const testScript = pkg.scripts?.test;
449
+ // Skip if no test script or the default placeholder
450
+ if (!testScript || testScript.includes("no test specified")) return null;
451
+
452
+ const result = tryExec("npm test 2>&1", basePath);
453
+ if (result === null) {
454
+ return {
455
+ name: "test",
456
+ status: "warning",
457
+ message: "Tests failing — npm test exited non-zero",
458
+ detail: "Fix failing tests before shipping",
459
+ };
460
+ }
461
+ return { name: "test", status: "ok", message: "Tests pass" };
462
+ } catch {
463
+ return null;
464
+ }
465
+ }
466
+
410
467
  // ── Public API ─────────────────────────────────────────────────────────────
411
468
 
412
469
  /**
@@ -454,6 +511,26 @@ export function runFullEnvironmentChecks(basePath: string): EnvironmentCheckResu
454
511
  return results;
455
512
  }
456
513
 
514
+ /**
515
+ * Run slow opt-in checks (build and/or test).
516
+ * These are never run on the pre-dispatch gate — only on explicit /gsd doctor --build/--test.
517
+ */
518
+ export function runSlowEnvironmentChecks(
519
+ basePath: string,
520
+ options?: { includeBuild?: boolean; includeTests?: boolean },
521
+ ): EnvironmentCheckResult[] {
522
+ const results: EnvironmentCheckResult[] = [];
523
+ if (options?.includeBuild) {
524
+ const buildCheck = checkBuildHealth(basePath);
525
+ if (buildCheck) results.push(buildCheck);
526
+ }
527
+ if (options?.includeTests) {
528
+ const testCheck = checkTestHealth(basePath);
529
+ if (testCheck) results.push(testCheck);
530
+ }
531
+ return results;
532
+ }
533
+
457
534
  /**
458
535
  * Convert environment check results to DoctorIssue format for the doctor pipeline.
459
536
  */
@@ -477,12 +554,16 @@ export function environmentResultsToDoctorIssues(results: EnvironmentCheckResult
477
554
  export async function checkEnvironmentHealth(
478
555
  basePath: string,
479
556
  issues: DoctorIssue[],
480
- options?: { includeRemote?: boolean },
557
+ options?: { includeRemote?: boolean; includeBuild?: boolean; includeTests?: boolean },
481
558
  ): Promise<void> {
482
559
  const results = options?.includeRemote
483
560
  ? runFullEnvironmentChecks(basePath)
484
561
  : runEnvironmentChecks(basePath);
485
562
 
563
+ if (options?.includeBuild || options?.includeTests) {
564
+ results.push(...runSlowEnvironmentChecks(basePath, options));
565
+ }
566
+
486
567
  issues.push(...environmentResultsToDoctorIssues(results));
487
568
  }
488
569
 
@@ -76,3 +76,23 @@ export function formatDoctorIssuesForPrompt(issues: DoctorIssue[]): string {
76
76
  return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
77
77
  }).join("\n");
78
78
  }
79
+
80
+ /**
81
+ * Serialize a doctor report to JSON — suitable for CI/tooling integration.
82
+ * Usage: /gsd doctor --json
83
+ */
84
+ export function formatDoctorReportJson(report: DoctorReport): string {
85
+ return JSON.stringify(
86
+ {
87
+ ok: report.ok,
88
+ basePath: report.basePath,
89
+ generatedAt: new Date().toISOString(),
90
+ summary: summarizeDoctorIssues(report.issues),
91
+ issues: report.issues,
92
+ fixesApplied: report.fixesApplied,
93
+ ...(report.timing ? { timing: report.timing } : {}),
94
+ },
95
+ null,
96
+ 2,
97
+ );
98
+ }
@@ -14,6 +14,7 @@
14
14
  import { existsSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { AuthStorage } from "@gsd/pi-coding-agent";
17
+ import { getEnvApiKey } from "@gsd/pi-ai";
17
18
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
19
  import { getAuthPath, PROVIDER_REGISTRY, type ProviderCategory } from "./key-manager.js";
19
20
 
@@ -56,6 +57,7 @@ function modelToProviderId(model: string): string | null {
56
57
  google: "google",
57
58
  anthropic: "anthropic",
58
59
  openai: "openai",
60
+ "github-copilot": "github-copilot",
59
61
  };
60
62
  if (prefixMap[prefix]) return prefixMap[prefix];
61
63
  }
@@ -139,7 +141,15 @@ function resolveKey(providerId: string): KeyLookup {
139
141
  }
140
142
  }
141
143
 
142
- // Check environment variable
144
+ // Check environment variable using the authoritative env var resolution
145
+ // (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
146
+ // COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
147
+ if (getEnvApiKey(providerId)) {
148
+ return { found: true, source: "env", backedOff: false };
149
+ }
150
+
151
+ // Fall back to PROVIDER_REGISTRY env var for providers not covered by getEnvApiKey
152
+ // (e.g., search providers like Brave, Tavily; tool providers like Jina, Context7)
143
153
  if (info?.envVar && process.env[info.envVar]) {
144
154
  return { found: true, source: "env", backedOff: false };
145
155
  }
@@ -149,6 +159,16 @@ function resolveKey(providerId: string): KeyLookup {
149
159
 
150
160
  // ── Individual check groups ────────────────────────────────────────────────────
151
161
 
162
+ /**
163
+ * Providers that can serve models normally associated with another provider.
164
+ * Key = the provider whose models can be served, Value = alternative providers to check.
165
+ * e.g. GitHub Copilot subscriptions can access Claude and GPT models.
166
+ */
167
+ const PROVIDER_ROUTES: Record<string, string[]> = {
168
+ anthropic: ["github-copilot"],
169
+ openai: ["github-copilot"],
170
+ };
171
+
152
172
  function checkLlmProviders(): ProviderCheckResult[] {
153
173
  const required = collectConfiguredModelProviders();
154
174
  const results: ProviderCheckResult[] = [];
@@ -159,6 +179,23 @@ function checkLlmProviders(): ProviderCheckResult[] {
159
179
  const lookup = resolveKey(providerId);
160
180
 
161
181
  if (!lookup.found) {
182
+ // Check if a cross-provider can serve this provider's models
183
+ const routes = PROVIDER_ROUTES[providerId];
184
+ const routeProvider = routes?.find(routeId => resolveKey(routeId).found);
185
+ if (routeProvider) {
186
+ const routeInfo = PROVIDER_REGISTRY.find(p => p.id === routeProvider);
187
+ const routeLabel = routeInfo?.label ?? routeProvider;
188
+ results.push({
189
+ name: providerId,
190
+ label,
191
+ category: "llm",
192
+ status: "ok",
193
+ message: `${label} — available via ${routeLabel}`,
194
+ required: true,
195
+ });
196
+ continue;
197
+ }
198
+
162
199
  const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
163
200
  results.push({
164
201
  name: providerId,
@@ -53,7 +53,20 @@ export type DoctorIssueCode =
53
53
  | "stranded_lock_directory"
54
54
  // Git / worktree integrity checks
55
55
  | "integration_branch_missing"
56
- | "worktree_directory_orphaned";
56
+ | "worktree_directory_orphaned"
57
+ // GSD state structural checks
58
+ | "circular_slice_dependency"
59
+ | "orphaned_slice_directory"
60
+ | "duplicate_task_id"
61
+ | "task_file_not_in_plan"
62
+ | "stale_replan_file"
63
+ | "future_timestamp"
64
+ // Runtime data integrity
65
+ | "metrics_ledger_corrupt"
66
+ | "large_planning_file"
67
+ // Slow environment checks (opt-in via --build / --test flags)
68
+ | "env_build"
69
+ | "env_test";
57
70
 
58
71
  /**
59
72
  * Issue codes that represent expected completion-transition states.
@@ -83,6 +96,8 @@ export interface DoctorReport {
83
96
  basePath: string;
84
97
  issues: DoctorIssue[];
85
98
  fixesApplied: string[];
99
+ /** Per-domain check durations in milliseconds. Present on explicit /gsd doctor runs. */
100
+ timing?: { git: number; runtime: number; environment: number; gsdState: number };
86
101
  }
87
102
 
88
103
  export interface DoctorSummary {