gsd-pi 2.37.1 → 2.38.0-dev.63ad7e5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resources/extensions/browser-tools/package.json +3 -1
  10. package/dist/resources/extensions/cmux/index.js +55 -1
  11. package/dist/resources/extensions/context7/package.json +1 -1
  12. package/dist/resources/extensions/env-utils.js +29 -0
  13. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  14. package/dist/resources/extensions/google-search/package.json +3 -1
  15. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +68 -97
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +143 -96
  24. package/dist/resources/extensions/gsd/captures.js +9 -1
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  27. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  28. package/dist/resources/extensions/gsd/commands.js +22 -2
  29. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  30. package/dist/resources/extensions/gsd/detection.js +1 -2
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  32. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  33. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  34. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  35. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  36. package/dist/resources/extensions/gsd/doctor.js +184 -11
  37. package/dist/resources/extensions/gsd/export.js +1 -1
  38. package/dist/resources/extensions/gsd/files.js +43 -2
  39. package/dist/resources/extensions/gsd/forensics.js +1 -1
  40. package/dist/resources/extensions/gsd/index.js +2 -1
  41. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  42. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  43. package/dist/resources/extensions/gsd/package.json +1 -1
  44. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  45. package/dist/resources/extensions/gsd/preferences-types.js +2 -2
  46. package/dist/resources/extensions/gsd/preferences-validation.js +43 -11
  47. package/dist/resources/extensions/gsd/preferences.js +5 -5
  48. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  49. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  50. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  51. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  52. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  54. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  55. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  56. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  57. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  58. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  59. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  60. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  61. package/dist/resources/extensions/gsd/state.js +1 -1
  62. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  63. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  64. package/dist/resources/extensions/gsd/worktree.js +35 -16
  65. package/dist/resources/extensions/remote-questions/status.js +2 -1
  66. package/dist/resources/extensions/remote-questions/store.js +2 -1
  67. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  68. package/dist/resources/extensions/subagent/index.js +12 -3
  69. package/dist/resources/extensions/subagent/isolation.js +2 -1
  70. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  71. package/dist/resources/extensions/universal-config/package.json +1 -1
  72. package/dist/welcome-screen.d.ts +12 -0
  73. package/dist/welcome-screen.js +53 -0
  74. package/package.json +2 -1
  75. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  76. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  78. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/models.generated.js +172 -0
  80. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  82. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  83. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  84. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  85. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  86. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  88. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  89. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  90. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  91. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  92. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  93. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  95. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  96. package/packages/pi-ai/dist/types.d.ts +2 -2
  97. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/types.js.map +1 -1
  99. package/packages/pi-ai/package.json +1 -0
  100. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  101. package/packages/pi-ai/src/models.generated.ts +172 -0
  102. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  103. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  104. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  105. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  106. package/packages/pi-ai/src/types.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  109. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  112. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  115. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  116. package/pkg/package.json +1 -1
  117. package/src/resources/extensions/cmux/index.ts +57 -1
  118. package/src/resources/extensions/env-utils.ts +31 -0
  119. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  120. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  121. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  122. package/src/resources/extensions/gsd/auto-loop.ts +88 -133
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  124. package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
  125. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  126. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  127. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  128. package/src/resources/extensions/gsd/auto.ts +139 -101
  129. package/src/resources/extensions/gsd/captures.ts +10 -1
  130. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  131. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  132. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  133. package/src/resources/extensions/gsd/commands.ts +24 -2
  134. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  135. package/src/resources/extensions/gsd/detection.ts +2 -2
  136. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  137. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  138. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  139. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  140. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  141. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  142. package/src/resources/extensions/gsd/doctor.ts +177 -13
  143. package/src/resources/extensions/gsd/export.ts +1 -1
  144. package/src/resources/extensions/gsd/files.ts +47 -2
  145. package/src/resources/extensions/gsd/forensics.ts +1 -1
  146. package/src/resources/extensions/gsd/index.ts +3 -1
  147. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  148. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  149. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  150. package/src/resources/extensions/gsd/preferences-types.ts +5 -5
  151. package/src/resources/extensions/gsd/preferences-validation.ts +42 -11
  152. package/src/resources/extensions/gsd/preferences.ts +5 -5
  153. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  154. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  156. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  157. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  158. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  159. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  160. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  161. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  162. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  163. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  164. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  165. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  166. package/src/resources/extensions/gsd/state.ts +1 -1
  167. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  168. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  169. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
  170. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  171. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  172. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  173. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  174. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  175. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  176. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  177. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  178. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  179. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  180. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  181. package/src/resources/extensions/gsd/types.ts +43 -1
  182. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  183. package/src/resources/extensions/gsd/worktree.ts +35 -15
  184. package/src/resources/extensions/remote-questions/status.ts +3 -1
  185. package/src/resources/extensions/remote-questions/store.ts +3 -1
  186. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  187. package/src/resources/extensions/subagent/index.ts +12 -3
  188. package/src/resources/extensions/subagent/isolation.ts +3 -1
  189. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  190. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  191. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  192. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  193. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  194. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  195. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  196. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  197. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  198. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  199. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  200. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  201. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -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
 
@@ -350,10 +351,60 @@ export async function selectDoctorScope(basePath: string, requestedScope?: strin
350
351
  return state.registry[0]?.id;
351
352
  }
352
353
 
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> {
354
+ // ── Helper: circular dependency detection ──────────────────────────────────
355
+ function detectCircularDependencies(slices: RoadmapSliceEntry[]): string[][] {
356
+ const known = new Set(slices.map(s => s.id));
357
+ const adj = new Map<string, string[]>();
358
+ for (const s of slices) adj.set(s.id, s.depends.filter(d => known.has(d)));
359
+ const state = new Map<string, "unvisited" | "visiting" | "done">();
360
+ for (const s of slices) state.set(s.id, "unvisited");
361
+ const cycles: string[][] = [];
362
+ function dfs(id: string, path: string[]): void {
363
+ const st = state.get(id);
364
+ if (st === "done") return;
365
+ if (st === "visiting") { cycles.push([...path.slice(path.indexOf(id)), id]); return; }
366
+ state.set(id, "visiting");
367
+ for (const dep of adj.get(id) ?? []) dfs(dep, [...path, id]);
368
+ state.set(id, "done");
369
+ }
370
+ for (const s of slices) if (state.get(s.id) === "unvisited") dfs(s.id, []);
371
+ return cycles;
372
+ }
373
+
374
+ // ── Helper: doctor run history ──────────────────────────────────────────────
375
+ interface DoctorHistoryEntry { ts: string; ok: boolean; errors: number; warnings: number; fixes: number; codes: string[] }
376
+
377
+ async function appendDoctorHistory(basePath: string, report: DoctorReport): Promise<void> {
378
+ try {
379
+ const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
380
+ const entry = JSON.stringify({
381
+ ts: new Date().toISOString(),
382
+ ok: report.ok,
383
+ errors: report.issues.filter(i => i.severity === "error").length,
384
+ warnings: report.issues.filter(i => i.severity === "warning").length,
385
+ fixes: report.fixesApplied.length,
386
+ codes: [...new Set(report.issues.map(i => i.code))],
387
+ } satisfies DoctorHistoryEntry);
388
+ const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
389
+ await saveFile(historyPath, existing + entry + "\n");
390
+ } catch { /* non-fatal */ }
391
+ }
392
+
393
+ /** Read the last N doctor history entries. Returns most-recent-first. */
394
+ export async function readDoctorHistory(basePath: string, lastN = 50): Promise<DoctorHistoryEntry[]> {
395
+ try {
396
+ const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
397
+ if (!existsSync(historyPath)) return [];
398
+ const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
399
+ return lines.slice(-lastN).reverse().map(l => JSON.parse(l) as DoctorHistoryEntry);
400
+ } catch { return []; }
401
+ }
402
+
403
+ 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
404
  const issues: DoctorIssue[] = [];
355
405
  const fixesApplied: string[] = [];
356
406
  const fix = options?.fix === true;
407
+ const dryRun = options?.dryRun === true;
357
408
  const fixLevel = options?.fixLevel ?? "all";
358
409
 
359
410
  // Issue codes that represent completion state transitions — creating summary
@@ -364,11 +415,18 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
364
415
 
365
416
  /** Whether a given issue code should be auto-fixed at the current fixLevel. */
366
417
  const shouldFix = (code: DoctorIssueCode): boolean => {
367
- if (!fix) return false;
418
+ if (!fix || dryRun) return false;
368
419
  if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code)) return false;
369
420
  return true;
370
421
  };
371
422
 
423
+ /** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
424
+ const dryRunCanFix = (code: DoctorIssueCode, message: string): void => {
425
+ if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
426
+ fixesApplied.push(`[dry-run] would fix: ${message}`);
427
+ }
428
+ };
429
+
372
430
  const prefs = loadEffectiveGSDPreferences();
373
431
  if (prefs) {
374
432
  const prefIssues = validatePreferenceShape(prefs.preferences);
@@ -385,21 +443,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
385
443
  }
386
444
  }
387
445
 
388
- // Git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files)
446
+ // Git health checks timed
447
+ const t0git = Date.now();
389
448
  const isolationMode: "none" | "worktree" | "branch" = options?.isolationMode ??
390
449
  (prefs?.preferences?.git?.isolation === "none" ? "none" :
391
450
  prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
392
451
  await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
452
+ const gitMs = Date.now() - t0git;
393
453
 
394
- // Runtime health checks (crash locks, completed-units, hook state, activity logs, STATE.md, gitignore)
454
+ // Runtime health checks timed
455
+ const t0runtime = Date.now();
395
456
  await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
457
+ const runtimeMs = Date.now() - t0runtime;
396
458
 
397
- // Environment health checks (#1221: missing tools, port conflicts, stale deps, disk space)
398
- await checkEnvironmentHealth(basePath, issues, { includeRemote: !options?.scope });
459
+ // Environment health checks timed
460
+ const t0env = Date.now();
461
+ await checkEnvironmentHealth(basePath, issues, {
462
+ includeRemote: !options?.scope,
463
+ includeBuild: options?.includeBuild,
464
+ includeTests: options?.includeTests,
465
+ });
466
+ const envMs = Date.now() - t0env;
399
467
 
400
468
  const milestonesPath = milestonesDir(basePath);
401
469
  if (!existsSync(milestonesPath)) {
402
- return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
470
+ const report: DoctorReport = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
471
+ await appendDoctorHistory(basePath, report);
472
+ return report;
403
473
  }
404
474
 
405
475
  const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
@@ -465,6 +535,43 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
465
535
  if (!roadmapContent) continue;
466
536
  const roadmap = parseRoadmap(roadmapContent);
467
537
 
538
+ // ── Circular dependency detection ──────────────────────────────────────
539
+ for (const cycle of detectCircularDependencies(roadmap.slices)) {
540
+ issues.push({
541
+ severity: "error",
542
+ code: "circular_slice_dependency",
543
+ scope: "milestone",
544
+ unitId: milestoneId,
545
+ message: `Circular dependency detected: ${cycle.join(" → ")}`,
546
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
547
+ fixable: false,
548
+ });
549
+ }
550
+
551
+ // ── Orphaned slice directories ─────────────────────────────────────────
552
+ try {
553
+ const slicesDir = join(milestonePath, "slices");
554
+ if (existsSync(slicesDir)) {
555
+ const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
556
+ for (const entry of readdirSync(slicesDir)) {
557
+ try {
558
+ if (!lstatSync(join(slicesDir, entry)).isDirectory()) continue;
559
+ } catch { continue; }
560
+ if (!knownSliceIds.has(entry)) {
561
+ issues.push({
562
+ severity: "warning",
563
+ code: "orphaned_slice_directory",
564
+ scope: "milestone",
565
+ unitId: milestoneId,
566
+ message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
567
+ file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
568
+ fixable: false,
569
+ });
570
+ }
571
+ }
572
+ }
573
+ } catch { /* non-fatal */ }
574
+
468
575
  for (const slice of roadmap.slices) {
469
576
  const unitId = `${milestoneId}/${slice.id}`;
470
577
  if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
@@ -539,6 +646,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
539
646
  continue;
540
647
  }
541
648
 
649
+ // ── Duplicate task IDs ───────────────────────────────────────────────
650
+ const taskIdCounts = new Map<string, number>();
651
+ for (const task of plan.tasks) taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
652
+ for (const [taskId, count] of taskIdCounts) {
653
+ if (count > 1) {
654
+ issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
655
+ message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
656
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
657
+ }
658
+ }
659
+
660
+ // ── Task files on disk not in plan ────────────────────────────────────
661
+ try {
662
+ if (tasksDir) {
663
+ const planTaskIds = new Set(plan.tasks.map(t => t.id));
664
+ for (const f of readdirSync(tasksDir)) {
665
+ if (!f.endsWith("-SUMMARY.md")) continue;
666
+ const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
667
+ if (!planTaskIds.has(diskTaskId)) {
668
+ issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
669
+ message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
670
+ file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
671
+ }
672
+ }
673
+ }
674
+ } catch { /* non-fatal */ }
675
+
542
676
  let allTasksDone = plan.tasks.length > 0;
543
677
  for (const task of plan.tasks) {
544
678
  const taskUnitId = `${unitId}/${task.id}`;
@@ -555,6 +689,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
555
689
  file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
556
690
  fixable: true,
557
691
  });
692
+ dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
558
693
  if (shouldFix("task_done_missing_summary")) {
559
694
  const stubPath = join(
560
695
  basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
@@ -618,6 +753,22 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
618
753
  }
619
754
  }
620
755
 
756
+ // ── Future timestamp check ─────────────────────────────────────
757
+ if (task.done && hasSummary && summaryPath) {
758
+ try {
759
+ const rawSummary = await loadFile(summaryPath);
760
+ const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
761
+ if (m) {
762
+ const ts = new Date(m[1].trim());
763
+ if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
764
+ issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
765
+ message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
766
+ file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
767
+ }
768
+ }
769
+ } catch { /* non-fatal */ }
770
+ }
771
+
621
772
  allTasksDone = allTasksDone && task.done;
622
773
  }
623
774
 
@@ -646,6 +797,13 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
646
797
  }
647
798
  }
648
799
 
800
+ // ── Stale REPLAN: exists but all tasks done ────────────────────────
801
+ if (replanPath && allTasksDone) {
802
+ issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
803
+ message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
804
+ file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
805
+ }
806
+
649
807
  const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
650
808
  const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
651
809
  const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
@@ -661,6 +819,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
661
819
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
662
820
  fixable: true,
663
821
  });
822
+ dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
664
823
  if (shouldFix("all_tasks_done_missing_slice_summary")) await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
665
824
  }
666
825
 
@@ -674,6 +833,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
674
833
  file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
675
834
  fixable: true,
676
835
  });
836
+ dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
677
837
  if (shouldFix("all_tasks_done_missing_slice_uat")) await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
678
838
  }
679
839
 
@@ -687,6 +847,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
687
847
  file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
688
848
  fixable: true,
689
849
  });
850
+ dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
690
851
  if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
691
852
  await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
692
853
  }
@@ -744,14 +905,17 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
744
905
  }
745
906
  }
746
907
 
747
- if (fix && fixesApplied.length > 0) {
908
+ if (fix && !dryRun && fixesApplied.length > 0) {
748
909
  await updateStateFile(basePath, fixesApplied);
749
910
  }
750
911
 
751
- return {
912
+ const report: DoctorReport = {
752
913
  ok: issues.every(issue => issue.severity !== "error"),
753
914
  basePath,
754
915
  issues,
755
916
  fixesApplied,
917
+ timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
756
918
  };
919
+ await appendDoctorHistory(basePath, report);
920
+ return report;
757
921
  }
@@ -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,
@@ -15,11 +15,12 @@ import type {
15
15
  Summary, SummaryFrontmatter, SummaryRequires, FileModified,
16
16
  Continue, ContinueFrontmatter, ContinueStatus,
17
17
  RequirementCounts,
18
+ TaskIO,
18
19
  SecretsManifest, SecretsManifestEntry, SecretsManifestEntryStatus,
19
20
  ManifestStatus,
20
21
  } from './types.js';
21
22
 
22
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
23
+ import { checkExistingEnvKeys } from '../env-utils.js';
23
24
  import { parseRoadmapSlices } from './roadmap-slices.js';
24
25
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
25
26
  import { debugTime, debugCount } from './debug-logger.js';
@@ -724,6 +725,50 @@ export function countMustHavesMentionedInSummary(
724
725
  return count;
725
726
  }
726
727
 
728
+ // ─── Task Plan IO Extractor ────────────────────────────────────────────────
729
+
730
+ /**
731
+ * Extract input and output file paths from a task plan's `## Inputs` and
732
+ * `## Expected Output` sections. Looks for backtick-wrapped file paths on
733
+ * each line (e.g. `` `src/foo.ts` ``).
734
+ *
735
+ * Returns empty arrays for missing/empty sections — callers should treat
736
+ * tasks with no IO as ambiguous (sequential fallback trigger).
737
+ */
738
+ export function parseTaskPlanIO(content: string): { inputFiles: string[]; outputFiles: string[] } {
739
+ const backtickPathRegex = /`([^`]+)`/g;
740
+
741
+ function extractPaths(sectionText: string | null): string[] {
742
+ if (!sectionText) return [];
743
+ const paths: string[] = [];
744
+ for (const line of sectionText.split("\n")) {
745
+ const trimmed = line.trim();
746
+ if (!trimmed || trimmed.startsWith("#")) continue;
747
+ let match: RegExpExecArray | null;
748
+ backtickPathRegex.lastIndex = 0;
749
+ while ((match = backtickPathRegex.exec(trimmed)) !== null) {
750
+ const candidate = match[1];
751
+ // Filter out things that look like code tokens rather than file paths
752
+ // (e.g. `true`, `false`, `npm run test`). A file path has at least one
753
+ // dot or slash.
754
+ if (candidate.includes("/") || candidate.includes(".")) {
755
+ paths.push(candidate);
756
+ }
757
+ }
758
+ }
759
+ return paths;
760
+ }
761
+
762
+ const [, body] = splitFrontmatter(content);
763
+ const inputSection = extractSection(body, "Inputs");
764
+ const outputSection = extractSection(body, "Expected Output");
765
+
766
+ return {
767
+ inputFiles: extractPaths(inputSection),
768
+ outputFiles: extractPaths(outputSection),
769
+ };
770
+ }
771
+
727
772
  // ─── UAT Type Extractor ────────────────────────────────────────────────────
728
773
 
729
774
  /**
@@ -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 ────────────────────────────────────────────────────────────────────
@@ -60,6 +60,8 @@ import { join } from "node:path";
60
60
  import { existsSync, readFileSync } from "node:fs";
61
61
  import { homedir } from "node:os";
62
62
  import { shortcutDesc } from "../shared/mod.js";
63
+
64
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
63
65
  import { Text } from "@gsd/pi-tui";
64
66
  import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
65
67
  import { toPosixPath } from "../shared/mod.js";
@@ -73,7 +75,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
73
75
 
74
76
  function warnDeprecatedAgentInstructions(): void {
75
77
  const paths = [
76
- join(homedir(), ".gsd", "agent-instructions.md"),
78
+ join(gsdHome, "agent-instructions.md"),
77
79
  join(process.cwd(), ".gsd", "agent-instructions.md"),
78
80
  ];
79
81
  for (const p of paths) {
@@ -3,7 +3,7 @@
3
3
  // Zero Pi dependencies — uses only exported helpers from files.ts.
4
4
 
5
5
  import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
6
- import { normalizeStringArray } from '../../shared/mod.js';
6
+ import { normalizeStringArray } from '../../shared/format-utils.js';
7
7
 
8
8
  import type {
9
9
  PlanningRoadmap,
@@ -235,6 +235,33 @@ export function validateTaskPlanContent(file: string, content: string): Validati
235
235
  }
236
236
  }
237
237
 
238
+ // Rule: Inputs and Expected Output should contain backtick-wrapped file paths
239
+ const inputsSection = getSection(content, "Inputs", 2);
240
+ const outputSection = getSection(content, "Expected Output", 2);
241
+ const backtickPathPattern = /`[^`]*[./][^`]*`/;
242
+
243
+ if (outputSection === null || !backtickPathPattern.test(outputSection)) {
244
+ issues.push({
245
+ severity: "warning",
246
+ scope: "task-plan",
247
+ file,
248
+ ruleId: "missing_output_file_paths",
249
+ message: "Task plan `## Expected Output` is missing or has no backtick-wrapped file paths.",
250
+ suggestion: "List concrete output file paths in backticks (e.g. `src/types.ts`). These are machine-parsed to derive task dependencies.",
251
+ });
252
+ }
253
+
254
+ if (inputsSection !== null && inputsSection.trim().length > 0 && !backtickPathPattern.test(inputsSection)) {
255
+ issues.push({
256
+ severity: "info",
257
+ scope: "task-plan",
258
+ file,
259
+ ruleId: "missing_input_file_paths",
260
+ message: "Task plan `## Inputs` has content but no backtick-wrapped file paths.",
261
+ suggestion: "List input file paths in backticks (e.g. `src/config.json`). These are machine-parsed to derive task dependencies.",
262
+ });
263
+ }
264
+
238
265
  // ── Observability rules (gated by runtime relevance) ──
239
266
 
240
267
  const relevant = textSuggestsObservabilityRelevant(content);
@@ -295,18 +295,6 @@ export function resolveInlineLevel(): InlineLevel {
295
295
  }
296
296
  }
297
297
 
298
- /**
299
- * Resolve the compression strategy from the active token profile.
300
- * budget/balanced -> "compress", quality -> "truncate".
301
- * Explicit preference always wins.
302
- */
303
- export function resolveCompressionStrategy(): import("./types.js").CompressionStrategy {
304
- const prefs = loadEffectiveGSDPreferences();
305
- if (prefs?.preferences.compression_strategy) return prefs.preferences.compression_strategy;
306
- const profile = resolveEffectiveProfile();
307
- return profile === "quality" ? "truncate" : "compress";
308
- }
309
-
310
298
  /**
311
299
  * Resolve the context selection mode from the active token profile.
312
300
  * budget -> "smart", balanced/quality -> "full".
@@ -16,8 +16,8 @@ import type {
16
16
  InlineLevel,
17
17
  PhaseSkipPreferences,
18
18
  ParallelConfig,
19
- CompressionStrategy,
20
19
  ContextSelectionMode,
20
+ ReactiveExecutionConfig,
21
21
  } from "./types.js";
22
22
  import type { DynamicRoutingConfig } from "./model-router.js";
23
23
 
@@ -83,15 +83,15 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
83
83
  "verification_auto_fix",
84
84
  "verification_max_retries",
85
85
  "search_provider",
86
- "compression_strategy",
87
86
  "context_selection",
88
87
  "widget_mode",
88
+ "reactive_execution",
89
89
  ]);
90
90
 
91
91
  /** Canonical list of all dispatch unit types. */
92
92
  export const KNOWN_UNIT_TYPES = [
93
93
  "research-milestone", "plan-milestone", "research-slice", "plan-slice",
94
- "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
94
+ "execute-task", "reactive-execute", "complete-slice", "replan-slice", "reassess-roadmap",
95
95
  "run-uat", "complete-milestone",
96
96
  ] as const;
97
97
  export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];
@@ -209,12 +209,12 @@ export interface GSDPreferences {
209
209
  verification_max_retries?: number;
210
210
  /** Search provider preference. "brave"/"tavily"/"ollama" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */
211
211
  search_provider?: "brave" | "tavily" | "ollama" | "native" | "auto";
212
- /** Compression strategy for context that exceeds budget. "truncate" (default) drops sections, "compress" applies heuristic compression first. */
213
- compression_strategy?: CompressionStrategy;
214
212
  /** Context selection mode for file inlining. "full" inlines entire files, "smart" uses semantic chunking. Default derived from token profile. */
215
213
  context_selection?: ContextSelectionMode;
216
214
  /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
217
215
  widget_mode?: "full" | "small" | "min" | "off";
216
+ /** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
217
+ reactive_execution?: ReactiveExecutionConfig;
218
218
  }
219
219
 
220
220
  export interface LoadedGSDPreferences {
@@ -10,7 +10,7 @@ import type { GitPreferences } from "./git-service.js";
10
10
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile, PhaseSkipPreferences } from "./types.js";
11
11
  import type { DynamicRoutingConfig } from "./model-router.js";
12
12
  import { VALID_BRANCH_NAME } from "./git-service.js";
13
- import { normalizeStringArray } from "../shared/mod.js";
13
+ import { normalizeStringArray } from "../shared/format-utils.js";
14
14
 
15
15
  import {
16
16
  KNOWN_PREFERENCE_KEYS,
@@ -496,6 +496,47 @@ export function validatePreferences(preferences: GSDPreferences): {
496
496
  }
497
497
  }
498
498
 
499
+ // ─── Reactive Execution ─────────────────────────────────────────────────
500
+ if (preferences.reactive_execution !== undefined) {
501
+ if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
502
+ const re = preferences.reactive_execution as unknown as Record<string, unknown>;
503
+ const validRe: Record<string, unknown> = {};
504
+
505
+ if (re.enabled !== undefined) {
506
+ if (typeof re.enabled === "boolean") validRe.enabled = re.enabled;
507
+ else errors.push("reactive_execution.enabled must be a boolean");
508
+ }
509
+ if (re.max_parallel !== undefined) {
510
+ const mp = typeof re.max_parallel === "number" ? re.max_parallel : Number(re.max_parallel);
511
+ if (Number.isFinite(mp) && mp >= 1 && mp <= 8) {
512
+ validRe.max_parallel = Math.floor(mp);
513
+ } else {
514
+ errors.push("reactive_execution.max_parallel must be a number between 1 and 8");
515
+ }
516
+ }
517
+ if (re.isolation_mode !== undefined) {
518
+ if (re.isolation_mode === "same-tree") {
519
+ validRe.isolation_mode = "same-tree";
520
+ } else {
521
+ errors.push('reactive_execution.isolation_mode must be "same-tree"');
522
+ }
523
+ }
524
+
525
+ const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
526
+ for (const key of Object.keys(re)) {
527
+ if (!knownReKeys.has(key)) {
528
+ warnings.push(`unknown reactive_execution key "${key}" — ignored`);
529
+ }
530
+ }
531
+
532
+ if (Object.keys(validRe).length > 0) {
533
+ validated.reactive_execution = validRe as unknown as import("./types.js").ReactiveExecutionConfig;
534
+ }
535
+ } else {
536
+ errors.push("reactive_execution must be an object");
537
+ }
538
+ }
539
+
499
540
  // ─── Verification Preferences ───────────────────────────────────────────
500
541
  if (preferences.verification_commands !== undefined) {
501
542
  if (Array.isArray(preferences.verification_commands)) {
@@ -645,16 +686,6 @@ export function validatePreferences(preferences: GSDPreferences): {
645
686
  }
646
687
  }
647
688
 
648
- // ─── Compression Strategy ───────────────────────────────────────────
649
- if (preferences.compression_strategy !== undefined) {
650
- const validStrategies = new Set(["truncate", "compress"]);
651
- if (typeof preferences.compression_strategy === "string" && validStrategies.has(preferences.compression_strategy)) {
652
- validated.compression_strategy = preferences.compression_strategy as GSDPreferences["compression_strategy"];
653
- } else {
654
- errors.push(`compression_strategy must be one of: truncate, compress`);
655
- }
656
- }
657
-
658
689
  // ─── Context Selection ──────────────────────────────────────────────
659
690
  if (preferences.context_selection !== undefined) {
660
691
  const validModes = new Set(["full", "smart"]);
@@ -13,11 +13,13 @@
13
13
  import { existsSync, readFileSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
+
17
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
18
  import { gsdRoot } from "./paths.js";
17
19
  import { parse as parseYaml } from "yaml";
18
20
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
19
21
  import type { DynamicRoutingConfig } from "./model-router.js";
20
- import { normalizeStringArray } from "../shared/mod.js";
22
+ import { normalizeStringArray } from "../shared/format-utils.js";
21
23
  import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
22
24
 
23
25
  import {
@@ -75,21 +77,20 @@ export {
75
77
  resolveProfileDefaults,
76
78
  resolveEffectiveProfile,
77
79
  resolveInlineLevel,
78
- resolveCompressionStrategy,
79
80
  resolveContextSelection,
80
81
  resolveSearchProviderFromPreferences,
81
82
  } from "./preferences-models.js";
82
83
 
83
84
  // ─── Path Constants & Getters ───────────────────────────────────────────────
84
85
 
85
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
86
+ const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
86
87
  const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
87
88
  function projectPreferencesPath(): string {
88
89
  return join(gsdRoot(process.cwd()), "preferences.md");
89
90
  }
90
91
  // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
91
92
  // Check uppercase as a fallback so those files aren't silently ignored.
92
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
93
+ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
93
94
  function projectPreferencesPathUppercase(): string {
94
95
  return join(gsdRoot(process.cwd()), "PREFERENCES.md");
95
96
  }
@@ -267,7 +268,6 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
267
268
  verification_auto_fix: override.verification_auto_fix ?? base.verification_auto_fix,
268
269
  verification_max_retries: override.verification_max_retries ?? base.verification_max_retries,
269
270
  search_provider: override.search_provider ?? base.search_provider,
270
- compression_strategy: override.compression_strategy ?? base.compression_strategy,
271
271
  context_selection: override.context_selection ?? base.context_selection,
272
272
  auto_visualize: override.auto_visualize ?? base.auto_visualize,
273
273
  auto_report: override.auto_report ?? base.auto_report,