gsd-pi 2.37.1-dev.d3ace49 → 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 (188) hide show
  1. package/dist/app-paths.js +1 -1
  2. package/dist/cli.js +9 -0
  3. package/dist/extension-discovery.d.ts +5 -3
  4. package/dist/extension-discovery.js +14 -9
  5. package/dist/extension-registry.js +2 -2
  6. package/dist/remote-questions-config.js +2 -2
  7. package/dist/resource-loader.js +34 -1
  8. package/dist/resources/extensions/browser-tools/package.json +3 -1
  9. package/dist/resources/extensions/cmux/index.js +55 -1
  10. package/dist/resources/extensions/context7/package.json +1 -1
  11. package/dist/resources/extensions/env-utils.js +29 -0
  12. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  13. package/dist/resources/extensions/github-sync/cli.js +284 -0
  14. package/dist/resources/extensions/github-sync/index.js +73 -0
  15. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  16. package/dist/resources/extensions/github-sync/sync.js +424 -0
  17. package/dist/resources/extensions/github-sync/templates.js +118 -0
  18. package/dist/resources/extensions/github-sync/types.js +7 -0
  19. package/dist/resources/extensions/google-search/package.json +3 -1
  20. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  21. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  22. package/dist/resources/extensions/gsd/auto-loop.js +597 -588
  23. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  24. package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
  25. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  26. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  27. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  28. package/dist/resources/extensions/gsd/auto.js +143 -96
  29. package/dist/resources/extensions/gsd/captures.js +9 -1
  30. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  31. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  32. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  33. package/dist/resources/extensions/gsd/commands.js +24 -3
  34. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  35. package/dist/resources/extensions/gsd/detection.js +1 -2
  36. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  37. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  38. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  39. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  40. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  41. package/dist/resources/extensions/gsd/doctor.js +204 -12
  42. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  43. package/dist/resources/extensions/gsd/export.js +1 -1
  44. package/dist/resources/extensions/gsd/files.js +6 -2
  45. package/dist/resources/extensions/gsd/forensics.js +1 -1
  46. package/dist/resources/extensions/gsd/git-service.js +15 -12
  47. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  48. package/dist/resources/extensions/gsd/index.js +24 -20
  49. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  50. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  51. package/dist/resources/extensions/gsd/package.json +1 -1
  52. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  53. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  54. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  55. package/dist/resources/extensions/gsd/preferences.js +8 -5
  56. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  57. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  59. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  60. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  62. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  63. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  64. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  65. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  66. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  67. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  68. package/dist/resources/extensions/gsd/state.js +1 -1
  69. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  70. package/dist/resources/extensions/gsd/worktree.js +35 -16
  71. package/dist/resources/extensions/mcp-client/index.js +14 -1
  72. package/dist/resources/extensions/remote-questions/status.js +2 -1
  73. package/dist/resources/extensions/remote-questions/store.js +2 -1
  74. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  75. package/dist/resources/extensions/subagent/index.js +12 -3
  76. package/dist/resources/extensions/subagent/isolation.js +2 -1
  77. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  78. package/dist/resources/extensions/universal-config/package.json +1 -1
  79. package/dist/welcome-screen.d.ts +12 -0
  80. package/dist/welcome-screen.js +53 -0
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  83. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  84. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  87. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  90. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  91. package/packages/pi-coding-agent/package.json +1 -1
  92. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  93. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  94. package/pkg/package.json +1 -1
  95. package/src/resources/extensions/cmux/index.ts +57 -1
  96. package/src/resources/extensions/env-utils.ts +31 -0
  97. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  98. package/src/resources/extensions/github-sync/cli.ts +364 -0
  99. package/src/resources/extensions/github-sync/index.ts +93 -0
  100. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  101. package/src/resources/extensions/github-sync/sync.ts +556 -0
  102. package/src/resources/extensions/github-sync/templates.ts +183 -0
  103. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  104. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  105. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  106. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  107. package/src/resources/extensions/github-sync/types.ts +47 -0
  108. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  109. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  110. package/src/resources/extensions/gsd/auto-loop.ts +484 -546
  111. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  112. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  113. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  114. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  115. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  116. package/src/resources/extensions/gsd/auto.ts +139 -101
  117. package/src/resources/extensions/gsd/captures.ts +10 -1
  118. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  119. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  120. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  121. package/src/resources/extensions/gsd/commands.ts +26 -4
  122. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  123. package/src/resources/extensions/gsd/detection.ts +2 -2
  124. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  125. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  126. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  127. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  128. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  129. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  130. package/src/resources/extensions/gsd/doctor.ts +199 -14
  131. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  132. package/src/resources/extensions/gsd/export.ts +1 -1
  133. package/src/resources/extensions/gsd/files.ts +5 -3
  134. package/src/resources/extensions/gsd/forensics.ts +1 -1
  135. package/src/resources/extensions/gsd/git-service.ts +20 -10
  136. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  137. package/src/resources/extensions/gsd/index.ts +24 -17
  138. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  139. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  140. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  141. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  142. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  143. package/src/resources/extensions/gsd/preferences.ts +8 -5
  144. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  145. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  146. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  147. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  148. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  149. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  150. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  151. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  152. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  153. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  154. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  155. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  156. package/src/resources/extensions/gsd/state.ts +1 -1
  157. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  158. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  159. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  160. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  161. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  162. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  163. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  164. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  165. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  166. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  167. package/src/resources/extensions/gsd/types.ts +0 -1
  168. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  169. package/src/resources/extensions/gsd/worktree.ts +35 -15
  170. package/src/resources/extensions/mcp-client/index.ts +17 -1
  171. package/src/resources/extensions/remote-questions/status.ts +3 -1
  172. package/src/resources/extensions/remote-questions/store.ts +3 -1
  173. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  174. package/src/resources/extensions/subagent/index.ts +12 -3
  175. package/src/resources/extensions/subagent/isolation.ts +3 -1
  176. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  177. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  178. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  179. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  180. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  181. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  182. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  183. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  184. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  185. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  186. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  187. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  188. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -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
+ }
@@ -51,10 +51,12 @@ function modelToProviderId(model: string): string | null {
51
51
  const prefix = model.split("/")[0].toLowerCase();
52
52
  // Map known prefixes to registry IDs
53
53
  const prefixMap: Record<string, string> = {
54
+ "anthropic-vertex": "anthropic-vertex",
54
55
  openrouter: "openrouter",
55
56
  groq: "groq",
56
57
  mistral: "mistral",
57
58
  google: "google",
59
+ "google-vertex": "google-vertex",
58
60
  anthropic: "anthropic",
59
61
  openai: "openai",
60
62
  "github-copilot": "github-copilot",
@@ -88,11 +90,20 @@ function collectConfiguredModelProviders(): Set<string> {
88
90
 
89
91
  const modelEntries = typeof models === "object" ? Object.values(models) : [];
90
92
  for (const entry of modelEntries) {
91
- const modelId = typeof entry === "string" ? entry
92
- : typeof entry === "object" && entry !== null && "model" in entry
93
- ? String((entry as { model: unknown }).model)
94
- : null;
95
- if (modelId) {
93
+ if (typeof entry === "string") {
94
+ const pid = modelToProviderId(entry);
95
+ if (pid) providers.add(pid);
96
+ continue;
97
+ }
98
+
99
+ if (typeof entry === "object" && entry !== null && "model" in entry) {
100
+ const configuredProvider = "provider" in entry ? (entry as { provider?: unknown }).provider : undefined;
101
+ if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) {
102
+ providers.add(configuredProvider);
103
+ continue;
104
+ }
105
+
106
+ const modelId = String((entry as { model: unknown }).model);
96
107
  const pid = modelToProviderId(modelId);
97
108
  if (pid) providers.add(pid);
98
109
  }
@@ -175,7 +186,9 @@ function checkLlmProviders(): ProviderCheckResult[] {
175
186
 
176
187
  for (const providerId of required) {
177
188
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
178
- const label = info?.label ?? providerId;
189
+ const label = providerId === "anthropic-vertex"
190
+ ? "Anthropic Vertex"
191
+ : info?.label ?? providerId;
179
192
  const lookup = resolveKey(providerId);
180
193
 
181
194
  if (!lookup.found) {
@@ -196,14 +209,18 @@ function checkLlmProviders(): ProviderCheckResult[] {
196
209
  continue;
197
210
  }
198
211
 
199
- const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
212
+ const envVar = providerId === "anthropic-vertex"
213
+ ? "ANTHROPIC_VERTEX_PROJECT_ID"
214
+ : info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
200
215
  results.push({
201
216
  name: providerId,
202
217
  label,
203
218
  category: "llm",
204
219
  status: "error",
205
- message: `${label} — no API key found`,
206
- detail: info?.hasOAuth
220
+ message: `${label} — not configured`,
221
+ detail: providerId === "anthropic-vertex"
222
+ ? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
223
+ : info?.hasOAuth
207
224
  ? `Run /gsd keys to authenticate`
208
225
  : `Set ${envVar} or run /gsd keys`,
209
226
  required: true,
@@ -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 {
@@ -1,14 +1,15 @@
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
 
4
4
  import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
5
- import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
5
+ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
6
6
  import { deriveState, isMilestoneComplete } from "./state.js";
7
7
  import { invalidateAllCaches } from "./cache.js";
8
8
  import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
9
9
 
10
- import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
10
+ import type { DoctorIssue, DoctorIssueCode, DoctorReport } from "./doctor-types.js";
11
11
  import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
12
+ import type { RoadmapSliceEntry } from "./types.js";
12
13
  import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
13
14
  import { checkEnvironmentHealth } from "./doctor-environment.js";
14
15
  import { runProviderChecks } from "./doctor-providers.js";
@@ -17,7 +18,7 @@ import { runProviderChecks } from "./doctor-providers.js";
17
18
  // All public types and functions from extracted modules are re-exported here
18
19
  // so that existing imports from "./doctor.js" continue to work unchanged.
19
20
  export type { DoctorSeverity, DoctorIssueCode, DoctorIssue, DoctorReport, DoctorSummary } from "./doctor-types.js";
20
- export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt } from "./doctor-format.js";
21
+ export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
21
22
  export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport, type EnvironmentCheckResult } from "./doctor-environment.js";
22
23
  export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport, type ProgressScore, type ProgressLevel } from "./progress-score.js";
23
24
 
@@ -279,9 +280,24 @@ async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sli
279
280
  }
280
281
  }
281
282
 
283
+ async function markSliceUndoneInRoadmap(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
284
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
285
+ if (!roadmapPath) return;
286
+ const content = await loadFile(roadmapPath);
287
+ if (!content) return;
288
+ const updated = content.replace(
289
+ new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"),
290
+ `$1[ ] **${sliceId}:`,
291
+ );
292
+ if (updated !== content) {
293
+ await saveFile(roadmapPath, updated);
294
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
295
+ }
296
+ }
297
+
282
298
  function matchesScope(unitId: string, scope?: string): boolean {
283
299
  if (!scope) return true;
284
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
300
+ return unitId === scope || unitId.startsWith(`${scope}/`);
285
301
  }
286
302
 
287
303
  function auditRequirements(content: string | null): DoctorIssue[] {
@@ -350,10 +366,60 @@ export async function selectDoctorScope(basePath: string, requestedScope?: strin
350
366
  return state.registry[0]?.id;
351
367
  }
352
368
 
353
- export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch" }): Promise<import("./doctor-types.js").DoctorReport> {
369
+ // ── Helper: circular dependency detection ──────────────────────────────────
370
+ function detectCircularDependencies(slices: RoadmapSliceEntry[]): string[][] {
371
+ const known = new Set(slices.map(s => s.id));
372
+ const adj = new Map<string, string[]>();
373
+ for (const s of slices) adj.set(s.id, s.depends.filter(d => known.has(d)));
374
+ const state = new Map<string, "unvisited" | "visiting" | "done">();
375
+ for (const s of slices) state.set(s.id, "unvisited");
376
+ const cycles: string[][] = [];
377
+ function dfs(id: string, path: string[]): void {
378
+ const st = state.get(id);
379
+ if (st === "done") return;
380
+ if (st === "visiting") { cycles.push([...path.slice(path.indexOf(id)), id]); return; }
381
+ state.set(id, "visiting");
382
+ for (const dep of adj.get(id) ?? []) dfs(dep, [...path, id]);
383
+ state.set(id, "done");
384
+ }
385
+ for (const s of slices) if (state.get(s.id) === "unvisited") dfs(s.id, []);
386
+ return cycles;
387
+ }
388
+
389
+ // ── Helper: doctor run history ──────────────────────────────────────────────
390
+ interface DoctorHistoryEntry { ts: string; ok: boolean; errors: number; warnings: number; fixes: number; codes: string[] }
391
+
392
+ async function appendDoctorHistory(basePath: string, report: DoctorReport): Promise<void> {
393
+ try {
394
+ const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
395
+ const entry = JSON.stringify({
396
+ ts: new Date().toISOString(),
397
+ ok: report.ok,
398
+ errors: report.issues.filter(i => i.severity === "error").length,
399
+ warnings: report.issues.filter(i => i.severity === "warning").length,
400
+ fixes: report.fixesApplied.length,
401
+ codes: [...new Set(report.issues.map(i => i.code))],
402
+ } satisfies DoctorHistoryEntry);
403
+ const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
404
+ await saveFile(historyPath, existing + entry + "\n");
405
+ } catch { /* non-fatal */ }
406
+ }
407
+
408
+ /** Read the last N doctor history entries. Returns most-recent-first. */
409
+ export async function readDoctorHistory(basePath: string, lastN = 50): Promise<DoctorHistoryEntry[]> {
410
+ try {
411
+ const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
412
+ if (!existsSync(historyPath)) return [];
413
+ const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
414
+ return lines.slice(-lastN).reverse().map(l => JSON.parse(l) as DoctorHistoryEntry);
415
+ } catch { return []; }
416
+ }
417
+
418
+ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; dryRun?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch"; includeBuild?: boolean; includeTests?: boolean }): Promise<DoctorReport> {
354
419
  const issues: DoctorIssue[] = [];
355
420
  const fixesApplied: string[] = [];
356
421
  const fix = options?.fix === true;
422
+ const dryRun = options?.dryRun === true;
357
423
  const fixLevel = options?.fixLevel ?? "all";
358
424
 
359
425
  // Issue codes that represent completion state transitions — creating summary
@@ -364,11 +430,18 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
364
430
 
365
431
  /** Whether a given issue code should be auto-fixed at the current fixLevel. */
366
432
  const shouldFix = (code: DoctorIssueCode): boolean => {
367
- if (!fix) return false;
433
+ if (!fix || dryRun) return false;
368
434
  if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code)) return false;
369
435
  return true;
370
436
  };
371
437
 
438
+ /** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
439
+ const dryRunCanFix = (code: DoctorIssueCode, message: string): void => {
440
+ if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
441
+ fixesApplied.push(`[dry-run] would fix: ${message}`);
442
+ }
443
+ };
444
+
372
445
  const prefs = loadEffectiveGSDPreferences();
373
446
  if (prefs) {
374
447
  const prefIssues = validatePreferenceShape(prefs.preferences);
@@ -385,21 +458,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
385
458
  }
386
459
  }
387
460
 
388
- // Git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files)
461
+ // Git health checks timed
462
+ const t0git = Date.now();
389
463
  const isolationMode: "none" | "worktree" | "branch" = options?.isolationMode ??
390
464
  (prefs?.preferences?.git?.isolation === "none" ? "none" :
391
465
  prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
392
466
  await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
467
+ const gitMs = Date.now() - t0git;
393
468
 
394
- // Runtime health checks (crash locks, completed-units, hook state, activity logs, STATE.md, gitignore)
469
+ // Runtime health checks timed
470
+ const t0runtime = Date.now();
395
471
  await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
472
+ const runtimeMs = Date.now() - t0runtime;
396
473
 
397
- // Environment health checks (#1221: missing tools, port conflicts, stale deps, disk space)
398
- await checkEnvironmentHealth(basePath, issues, { includeRemote: !options?.scope });
474
+ // Environment health checks timed
475
+ const t0env = Date.now();
476
+ await checkEnvironmentHealth(basePath, issues, {
477
+ includeRemote: !options?.scope,
478
+ includeBuild: options?.includeBuild,
479
+ includeTests: options?.includeTests,
480
+ });
481
+ const envMs = Date.now() - t0env;
399
482
 
400
483
  const milestonesPath = milestonesDir(basePath);
401
484
  if (!existsSync(milestonesPath)) {
402
- return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
485
+ const report: DoctorReport = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
486
+ await appendDoctorHistory(basePath, report);
487
+ return report;
403
488
  }
404
489
 
405
490
  const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
@@ -465,6 +550,43 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
465
550
  if (!roadmapContent) continue;
466
551
  const roadmap = parseRoadmap(roadmapContent);
467
552
 
553
+ // ── Circular dependency detection ──────────────────────────────────────
554
+ for (const cycle of detectCircularDependencies(roadmap.slices)) {
555
+ issues.push({
556
+ severity: "error",
557
+ code: "circular_slice_dependency",
558
+ scope: "milestone",
559
+ unitId: milestoneId,
560
+ message: `Circular dependency detected: ${cycle.join(" → ")}`,
561
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
562
+ fixable: false,
563
+ });
564
+ }
565
+
566
+ // ── Orphaned slice directories ─────────────────────────────────────────
567
+ try {
568
+ const slicesDir = join(milestonePath, "slices");
569
+ if (existsSync(slicesDir)) {
570
+ const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
571
+ for (const entry of readdirSync(slicesDir)) {
572
+ try {
573
+ if (!lstatSync(join(slicesDir, entry)).isDirectory()) continue;
574
+ } catch { continue; }
575
+ if (!knownSliceIds.has(entry)) {
576
+ issues.push({
577
+ severity: "warning",
578
+ code: "orphaned_slice_directory",
579
+ scope: "milestone",
580
+ unitId: milestoneId,
581
+ message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
582
+ file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
583
+ fixable: false,
584
+ });
585
+ }
586
+ }
587
+ }
588
+ } catch { /* non-fatal */ }
589
+
468
590
  for (const slice of roadmap.slices) {
469
591
  const unitId = `${milestoneId}/${slice.id}`;
470
592
  if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
@@ -539,6 +661,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
539
661
  continue;
540
662
  }
541
663
 
664
+ // ── Duplicate task IDs ───────────────────────────────────────────────
665
+ const taskIdCounts = new Map<string, number>();
666
+ for (const task of plan.tasks) taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
667
+ for (const [taskId, count] of taskIdCounts) {
668
+ if (count > 1) {
669
+ issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
670
+ message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
671
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
672
+ }
673
+ }
674
+
675
+ // ── Task files on disk not in plan ────────────────────────────────────
676
+ try {
677
+ if (tasksDir) {
678
+ const planTaskIds = new Set(plan.tasks.map(t => t.id));
679
+ for (const f of readdirSync(tasksDir)) {
680
+ if (!f.endsWith("-SUMMARY.md")) continue;
681
+ const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
682
+ if (!planTaskIds.has(diskTaskId)) {
683
+ issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
684
+ message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
685
+ file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
686
+ }
687
+ }
688
+ }
689
+ } catch { /* non-fatal */ }
690
+
542
691
  let allTasksDone = plan.tasks.length > 0;
543
692
  for (const task of plan.tasks) {
544
693
  const taskUnitId = `${unitId}/${task.id}`;
@@ -555,6 +704,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
555
704
  file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
556
705
  fixable: true,
557
706
  });
707
+ dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
558
708
  if (shouldFix("task_done_missing_summary")) {
559
709
  const stubPath = join(
560
710
  basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
@@ -618,6 +768,22 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
618
768
  }
619
769
  }
620
770
 
771
+ // ── Future timestamp check ─────────────────────────────────────
772
+ if (task.done && hasSummary && summaryPath) {
773
+ try {
774
+ const rawSummary = await loadFile(summaryPath);
775
+ const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
776
+ if (m) {
777
+ const ts = new Date(m[1].trim());
778
+ if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
779
+ issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
780
+ message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
781
+ file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
782
+ }
783
+ }
784
+ } catch { /* non-fatal */ }
785
+ }
786
+
621
787
  allTasksDone = allTasksDone && task.done;
622
788
  }
623
789
 
@@ -646,6 +812,13 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
646
812
  }
647
813
  }
648
814
 
815
+ // ── Stale REPLAN: exists but all tasks done ────────────────────────
816
+ if (replanPath && allTasksDone) {
817
+ issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
818
+ message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
819
+ file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
820
+ }
821
+
649
822
  const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
650
823
  const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
651
824
  const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
@@ -661,6 +834,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
661
834
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
662
835
  fixable: true,
663
836
  });
837
+ dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
664
838
  if (shouldFix("all_tasks_done_missing_slice_summary")) await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
665
839
  }
666
840
 
@@ -674,6 +848,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
674
848
  file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
675
849
  fixable: true,
676
850
  });
851
+ dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
677
852
  if (shouldFix("all_tasks_done_missing_slice_uat")) await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
678
853
  }
679
854
 
@@ -687,6 +862,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
687
862
  file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
688
863
  fixable: true,
689
864
  });
865
+ dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
690
866
  if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
691
867
  await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
692
868
  }
@@ -702,6 +878,12 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
702
878
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
703
879
  fixable: true,
704
880
  });
881
+ if (!allTasksDone) {
882
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
883
+ if (shouldFix("slice_checked_missing_summary")) {
884
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
885
+ }
886
+ }
705
887
  }
706
888
 
707
889
  if (slice.done && !hasSliceUat) {
@@ -744,14 +926,17 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
744
926
  }
745
927
  }
746
928
 
747
- if (fix && fixesApplied.length > 0) {
929
+ if (fix && !dryRun && fixesApplied.length > 0) {
748
930
  await updateStateFile(basePath, fixesApplied);
749
931
  }
750
932
 
751
- return {
933
+ const report: DoctorReport = {
752
934
  ok: issues.every(issue => issue.severity !== "error"),
753
935
  basePath,
754
936
  issues,
755
937
  fixesApplied,
938
+ timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
756
939
  };
940
+ await appendDoctorHistory(basePath, report);
941
+ return report;
757
942
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
1
+ import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
2
 
3
3
  type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise<void>;
4
4
 
@@ -10,7 +10,7 @@ export function registerExitCommand(
10
10
  description: "Exit GSD gracefully",
11
11
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
12
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
13
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule<typeof import("./auto.js")>(import.meta.url, "./auto.js")).stopAuto;
14
14
  await stopAuto(ctx, pi, "Graceful exit");
15
15
  ctx.shutdown();
16
16
  },
@@ -11,7 +11,7 @@ import {
11
11
  } from "./metrics.js";
12
12
  import type { UnitMetrics } from "./metrics.js";
13
13
  import { gsdRoot } from "./paths.js";
14
- import { formatDuration, fileLink } from "../shared/mod.js";
14
+ import { formatDuration, fileLink } from "../shared/format-utils.js";
15
15
  import { getErrorMessage } from "./error-utils.js";
16
16
 
17
17
  /**
@@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import { atomicWriteAsync } from './atomic-write.js';
9
9
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
10
- import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
10
+ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
11
11
 
12
12
  import type {
13
13
  Roadmap, BoundaryMapEntry,
@@ -20,7 +20,7 @@ import type {
20
20
  ManifestStatus,
21
21
  } from './types.js';
22
22
 
23
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
23
+ import { checkExistingEnvKeys } from '../env-utils.js';
24
24
  import { parseRoadmapSlices } from './roadmap-slices.js';
25
25
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
26
26
  import { debugTime, debugCount } from './debug-logger.js';
@@ -775,7 +775,7 @@ export function parseTaskPlanIO(content: string): { inputFiles: string[]; output
775
775
  * The four UAT classification types recognised by GSD auto-mode.
776
776
  * `undefined` is returned (not this union) when no type can be determined.
777
777
  */
778
- export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed';
778
+ export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed' | 'browser-executable' | 'runtime-executable';
779
779
 
780
780
  /**
781
781
  * Extract the UAT type from a UAT file's raw content.
@@ -799,6 +799,8 @@ export function extractUatType(content: string): UatType | undefined {
799
799
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
800
800
 
801
801
  if (rawValue.startsWith('artifact-driven')) return 'artifact-driven';
802
+ if (rawValue.startsWith('browser-executable')) return 'browser-executable';
803
+ if (rawValue.startsWith('runtime-executable')) return 'runtime-executable';
802
804
  if (rawValue.startsWith('live-runtime')) return 'live-runtime';
803
805
  if (rawValue.startsWith('human-experience')) return 'human-experience';
804
806
  if (rawValue.startsWith('mixed')) return 'mixed';
@@ -27,7 +27,7 @@ import { deriveState } from "./state.js";
27
27
  import { isAutoActive } from "./auto.js";
28
28
  import { loadPrompt } from "./prompt-loader.js";
29
29
  import { gsdRoot } from "./paths.js";
30
- import { formatDuration } from "../shared/mod.js";
30
+ import { formatDuration } from "../shared/format-utils.js";
31
31
  import { getAutoWorktreePath } from "./auto-worktree.js";
32
32
 
33
33
  // ─── Types ────────────────────────────────────────────────────────────────────