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
@@ -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
+ }
@@ -28,10 +28,12 @@ function modelToProviderId(model) {
28
28
  const prefix = model.split("/")[0].toLowerCase();
29
29
  // Map known prefixes to registry IDs
30
30
  const prefixMap = {
31
+ "anthropic-vertex": "anthropic-vertex",
31
32
  openrouter: "openrouter",
32
33
  groq: "groq",
33
34
  mistral: "mistral",
34
35
  google: "google",
36
+ "google-vertex": "google-vertex",
35
37
  anthropic: "anthropic",
36
38
  openai: "openai",
37
39
  "github-copilot": "github-copilot",
@@ -67,11 +69,19 @@ function collectConfiguredModelProviders() {
67
69
  }
68
70
  const modelEntries = typeof models === "object" ? Object.values(models) : [];
69
71
  for (const entry of modelEntries) {
70
- const modelId = typeof entry === "string" ? entry
71
- : typeof entry === "object" && entry !== null && "model" in entry
72
- ? String(entry.model)
73
- : null;
74
- 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);
75
85
  const pid = modelToProviderId(modelId);
76
86
  if (pid)
77
87
  providers.add(pid);
@@ -138,7 +148,9 @@ function checkLlmProviders() {
138
148
  const results = [];
139
149
  for (const providerId of required) {
140
150
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
141
- const label = info?.label ?? providerId;
151
+ const label = providerId === "anthropic-vertex"
152
+ ? "Anthropic Vertex"
153
+ : info?.label ?? providerId;
142
154
  const lookup = resolveKey(providerId);
143
155
  if (!lookup.found) {
144
156
  // Check if a cross-provider can serve this provider's models
@@ -157,16 +169,20 @@ function checkLlmProviders() {
157
169
  });
158
170
  continue;
159
171
  }
160
- const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
172
+ const envVar = providerId === "anthropic-vertex"
173
+ ? "ANTHROPIC_VERTEX_PROJECT_ID"
174
+ : info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
161
175
  results.push({
162
176
  name: providerId,
163
177
  label,
164
178
  category: "llm",
165
179
  status: "error",
166
- message: `${label} — no API key found`,
167
- detail: info?.hasOAuth
168
- ? `Run /gsd keys to authenticate`
169
- : `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`,
170
186
  required: true,
171
187
  });
172
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.
@@ -6,8 +6,8 @@ import { promises as fs } from 'node:fs';
6
6
  import { resolve } from 'node:path';
7
7
  import { atomicWriteAsync } from './atomic-write.js';
8
8
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
9
- import { findMilestoneIds } from './guided-flow.js';
10
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
9
+ import { findMilestoneIds } from './milestone-ids.js';
10
+ import { checkExistingEnvKeys } from '../env-utils.js';
11
11
  import { parseRoadmapSlices } from './roadmap-slices.js';
12
12
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
13
13
  import { debugTime, debugCount } from './debug-logger.js';
@@ -692,6 +692,10 @@ export function extractUatType(content) {
692
692
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
693
693
  if (rawValue.startsWith('artifact-driven'))
694
694
  return 'artifact-driven';
695
+ if (rawValue.startsWith('browser-executable'))
696
+ return 'browser-executable';
697
+ if (rawValue.startsWith('runtime-executable'))
698
+ return 'runtime-executable';
695
699
  if (rawValue.startsWith('live-runtime'))
696
700
  return 'live-runtime';
697
701
  if (rawValue.startsWith('human-experience'))
@@ -21,7 +21,7 @@ import { deriveState } from "./state.js";
21
21
  import { isAutoActive } from "./auto.js";
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  import { gsdRoot } from "./paths.js";
24
- import { formatDuration } from "../shared/mod.js";
24
+ import { formatDuration } from "../shared/format-utils.js";
25
25
  import { getAutoWorktreePath } from "./auto-worktree.js";
26
26
  // ─── Entry Point ──────────────────────────────────────────────────────────────
27
27
  export async function handleForensics(args, ctx, pi) {
@@ -14,7 +14,7 @@ import { gsdRoot } from "./paths.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
  import { detectWorktreeName, SLICE_BRANCH_RE, } from "./worktree.js";
17
- import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAll, nativeResetPaths, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
17
+ import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
18
18
  import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
19
19
  import { getErrorMessage } from "./error-utils.js";
20
20
  export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
@@ -36,12 +36,19 @@ export function buildTaskCommitMessage(ctx) {
36
36
  : description;
37
37
  const subject = `${type}(${scope}): ${truncated}`;
38
38
  // Build body with key files if available
39
+ const bodyParts = [];
39
40
  if (ctx.keyFiles && ctx.keyFiles.length > 0) {
40
41
  const fileLines = ctx.keyFiles
41
42
  .slice(0, 8) // cap at 8 files to keep commit concise
42
43
  .map(f => `- ${f}`)
43
44
  .join("\n");
44
- return `${subject}\n\n${fileLines}`;
45
+ bodyParts.push(fileLines);
46
+ }
47
+ if (ctx.issueNumber) {
48
+ bodyParts.push(`Resolves #${ctx.issueNumber}`);
49
+ }
50
+ if (bodyParts.length > 0) {
51
+ return `${subject}\n\n${bodyParts.join("\n\n")}`;
45
52
  }
46
53
  return subject;
47
54
  }
@@ -254,7 +261,9 @@ export class GitServiceImpl {
254
261
  }
255
262
  this._runtimeFilesCleanedUp = true;
256
263
  }
257
- // Stage everything, then unstage excluded paths.
264
+ // Stage everything using pathspec exclusions so excluded paths are never
265
+ // hashed by git. The old approach of `git add -A` followed by unstaging
266
+ // hangs indefinitely on repos with large untracked artifact trees (#1605).
258
267
  //
259
268
  // Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
260
269
  // When .gsd/milestones/ files are already tracked in the index (projects
@@ -264,15 +273,9 @@ export class GitServiceImpl {
264
273
  // the second half of a milestone's artifacts are never committed (#1326).
265
274
  //
266
275
  // If .gsd/ IS in .gitignore (the default for external state projects),
267
- // git add -A already skips it and the reset is a harmless no-op.
268
- nativeAddAll(this.basePath);
269
- const runtimeExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
270
- for (const exclusion of runtimeExclusions) {
271
- try {
272
- nativeResetPaths(this.basePath, [exclusion]);
273
- }
274
- catch { /* path not staged — ignore */ }
275
- }
276
+ // git add -A already skips it and the exclusions are harmless no-ops.
277
+ const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
278
+ nativeAddAllWithExclusions(this.basePath, allExclusions);
276
279
  }
277
280
  /** Tracks whether runtime file cleanup has run this session. */
278
281
  _runtimeFilesCleanedUp = false;