gsd-pi 2.37.1 → 2.38.0-dev.361f5e3

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 (219) 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/github-sync/cli.js +284 -0
  15. package/dist/resources/extensions/github-sync/index.js +73 -0
  16. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  17. package/dist/resources/extensions/github-sync/sync.js +424 -0
  18. package/dist/resources/extensions/github-sync/templates.js +118 -0
  19. package/dist/resources/extensions/github-sync/types.js +7 -0
  20. package/dist/resources/extensions/google-search/package.json +3 -1
  21. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  22. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  23. package/dist/resources/extensions/gsd/auto-loop.js +149 -170
  24. package/dist/resources/extensions/gsd/auto-post-unit.js +104 -68
  25. package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
  26. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  27. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  28. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  29. package/dist/resources/extensions/gsd/auto.js +143 -96
  30. package/dist/resources/extensions/gsd/captures.js +9 -1
  31. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  32. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  33. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  34. package/dist/resources/extensions/gsd/commands.js +22 -2
  35. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  36. package/dist/resources/extensions/gsd/detection.js +1 -2
  37. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  38. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  39. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  40. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  41. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  42. package/dist/resources/extensions/gsd/doctor.js +184 -11
  43. package/dist/resources/extensions/gsd/export.js +1 -1
  44. package/dist/resources/extensions/gsd/files.js +43 -2
  45. package/dist/resources/extensions/gsd/forensics.js +1 -1
  46. package/dist/resources/extensions/gsd/git-service.js +8 -1
  47. package/dist/resources/extensions/gsd/index.js +2 -1
  48. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  49. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  50. package/dist/resources/extensions/gsd/package.json +1 -1
  51. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  52. package/dist/resources/extensions/gsd/preferences-types.js +3 -2
  53. package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
  54. package/dist/resources/extensions/gsd/preferences.js +8 -5
  55. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  56. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  57. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  59. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  61. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  62. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  63. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  64. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  65. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  66. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  67. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  68. package/dist/resources/extensions/gsd/state.js +1 -1
  69. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  70. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  71. package/dist/resources/extensions/gsd/worktree.js +35 -16
  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 +2 -1
  82. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  83. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  84. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  85. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  86. package/packages/pi-ai/dist/models.generated.js +172 -0
  87. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  88. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  89. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  90. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  91. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  92. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  93. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  94. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  95. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  96. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  97. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  99. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  100. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  101. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  102. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  103. package/packages/pi-ai/dist/types.d.ts +2 -2
  104. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/types.js.map +1 -1
  106. package/packages/pi-ai/package.json +1 -0
  107. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  108. package/packages/pi-ai/src/models.generated.ts +172 -0
  109. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  110. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  111. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  112. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  113. package/packages/pi-ai/src/types.ts +2 -0
  114. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  116. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  119. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  120. package/packages/pi-coding-agent/package.json +1 -1
  121. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  122. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  123. package/pkg/package.json +1 -1
  124. package/src/resources/extensions/cmux/index.ts +57 -1
  125. package/src/resources/extensions/env-utils.ts +31 -0
  126. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  127. package/src/resources/extensions/github-sync/cli.ts +364 -0
  128. package/src/resources/extensions/github-sync/index.ts +93 -0
  129. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  130. package/src/resources/extensions/github-sync/sync.ts +556 -0
  131. package/src/resources/extensions/github-sync/templates.ts +183 -0
  132. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  133. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  134. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  135. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  136. package/src/resources/extensions/github-sync/types.ts +47 -0
  137. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  138. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  139. package/src/resources/extensions/gsd/auto-loop.ts +207 -252
  140. package/src/resources/extensions/gsd/auto-post-unit.ts +81 -39
  141. package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
  142. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  143. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  144. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  145. package/src/resources/extensions/gsd/auto.ts +139 -101
  146. package/src/resources/extensions/gsd/captures.ts +10 -1
  147. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  148. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  149. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  150. package/src/resources/extensions/gsd/commands.ts +24 -2
  151. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  152. package/src/resources/extensions/gsd/detection.ts +2 -2
  153. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  154. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  155. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  156. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  157. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  158. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  159. package/src/resources/extensions/gsd/doctor.ts +177 -13
  160. package/src/resources/extensions/gsd/export.ts +1 -1
  161. package/src/resources/extensions/gsd/files.ts +47 -2
  162. package/src/resources/extensions/gsd/forensics.ts +1 -1
  163. package/src/resources/extensions/gsd/git-service.ts +13 -1
  164. package/src/resources/extensions/gsd/index.ts +3 -1
  165. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  166. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  167. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  168. package/src/resources/extensions/gsd/preferences-types.ts +9 -5
  169. package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
  170. package/src/resources/extensions/gsd/preferences.ts +8 -5
  171. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  172. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  173. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  174. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  175. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  177. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  178. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  179. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  180. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  181. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  182. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  183. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  184. package/src/resources/extensions/gsd/state.ts +1 -1
  185. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  186. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  187. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +16 -37
  188. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  189. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  190. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  191. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  192. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  193. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  194. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  195. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  196. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  197. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  198. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  199. package/src/resources/extensions/gsd/types.ts +43 -1
  200. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  201. package/src/resources/extensions/gsd/worktree.ts +35 -15
  202. package/src/resources/extensions/remote-questions/status.ts +3 -1
  203. package/src/resources/extensions/remote-questions/store.ts +3 -1
  204. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  205. package/src/resources/extensions/subagent/index.ts +12 -3
  206. package/src/resources/extensions/subagent/isolation.ts +3 -1
  207. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  208. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  209. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  210. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  211. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  212. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  213. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  214. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  215. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  216. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  217. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  218. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  219. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -22,7 +22,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
22
22
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
23
23
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
24
24
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
25
- import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
26
25
  import { isDbAvailable } from "./gsd-db.js";
27
26
  import { consumeSignal } from "./session-status-io.js";
28
27
  import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, } from "./post-unit-hooks.js";
@@ -36,7 +35,7 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
36
35
  *
37
36
  * Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
38
37
  */
39
- export async function postUnitPreVerification(pctx) {
38
+ export async function postUnitPreVerification(pctx, opts) {
40
39
  const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
41
40
  // ── Parallel worker signal check ──
42
41
  const milestoneLock = process.env.GSD_MILESTONE_LOCK;
@@ -55,8 +54,10 @@ export async function postUnitPreVerification(pctx) {
55
54
  }
56
55
  // Invalidate all caches
57
56
  invalidateAllCaches();
58
- // Small delay to let files settle
59
- await new Promise(r => setTimeout(r, 500));
57
+ // Small delay to let files settle (skipped for sidecars where latency matters more)
58
+ if (!opts?.skipSettleDelay) {
59
+ await new Promise(r => setTimeout(r, 100));
60
+ }
60
61
  // Auto-commit
61
62
  if (s.currentUnit) {
62
63
  try {
@@ -71,16 +72,26 @@ export async function postUnitPreVerification(pctx) {
71
72
  const summaryContent = await loadFile(summaryPath);
72
73
  if (summaryContent) {
73
74
  const summary = parseSummary(summaryContent);
75
+ // Look up GitHub issue number for commit linking
76
+ let ghIssueNumber;
77
+ try {
78
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
79
+ ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
80
+ }
81
+ catch {
82
+ // GitHub sync not available — skip
83
+ }
74
84
  taskContext = {
75
85
  taskId: `${sid}/${tid}`,
76
86
  taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
77
87
  oneLiner: summary.oneLiner || undefined,
78
88
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
89
+ issueNumber: ghIssueNumber,
79
90
  };
80
91
  }
81
92
  }
82
- catch {
83
- // Non-fatal
93
+ catch (e) {
94
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
84
95
  }
85
96
  }
86
97
  }
@@ -90,57 +101,68 @@ export async function postUnitPreVerification(pctx) {
90
101
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
91
102
  }
92
103
  }
93
- catch {
94
- // Non-fatal
104
+ catch (e) {
105
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
95
106
  }
96
- // Doctor: fix mechanical bookkeeping
107
+ // GitHub sync (non-blocking, opt-in)
97
108
  try {
98
- const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
99
- const doctorScope = scopeParts.join("/");
100
- const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
101
- const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" : "task";
102
- const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
103
- if (report.fixesApplied.length > 0) {
104
- ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
105
- }
106
- // Proactive health tracking
107
- const summary = summarizeDoctorIssues(report.issues);
108
- recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
109
- // Check if we should escalate to LLM-assisted heal
110
- if (summary.errors > 0) {
111
- const unresolvedErrors = report.issues
112
- .filter(i => i.severity === "error" && !i.fixable)
113
- .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
114
- const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
115
- if (escalation.shouldEscalate) {
116
- ctx.ui.notify(`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`, "warning");
117
- try {
118
- const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
119
- const { dispatchDoctorHeal } = await import("./commands-handlers.js");
120
- const actionable = report.issues.filter(i => i.severity === "error");
121
- const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
122
- const structuredIssues = formatDoctorIssuesForPrompt(actionable);
123
- dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
124
- }
125
- catch {
126
- // Non-fatal
127
- }
128
- }
129
- }
109
+ const { runGitHubSync } = await import("../github-sync/sync.js");
110
+ await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
130
111
  }
131
- catch {
132
- // Non-fatal
112
+ catch (e) {
113
+ debugLog("postUnit", { phase: "github-sync", error: String(e) });
133
114
  }
134
- // Throttled STATE.md rebuild
135
- const now = Date.now();
136
- if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
115
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
116
+ if (!opts?.skipDoctor)
137
117
  try {
138
- await rebuildState(s.basePath);
139
- s.lastStateRebuildAt = now;
140
- autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
118
+ const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
119
+ const doctorScope = scopeParts.join("/");
120
+ const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
121
+ const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" : "task";
122
+ const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
123
+ if (report.fixesApplied.length > 0) {
124
+ ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
125
+ }
126
+ // Proactive health tracking
127
+ const summary = summarizeDoctorIssues(report.issues);
128
+ recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
129
+ // Check if we should escalate to LLM-assisted heal
130
+ if (summary.errors > 0) {
131
+ const unresolvedErrors = report.issues
132
+ .filter(i => i.severity === "error" && !i.fixable)
133
+ .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
134
+ const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
135
+ if (escalation.shouldEscalate) {
136
+ ctx.ui.notify(`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`, "warning");
137
+ try {
138
+ const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
139
+ const { dispatchDoctorHeal } = await import("./commands-handlers.js");
140
+ const actionable = report.issues.filter(i => i.severity === "error");
141
+ const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
142
+ const structuredIssues = formatDoctorIssuesForPrompt(actionable);
143
+ dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
144
+ }
145
+ catch (e) {
146
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
147
+ }
148
+ }
149
+ }
150
+ }
151
+ catch (e) {
152
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
141
153
  }
142
- catch {
143
- // Non-fatal
154
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
155
+ if (!opts?.skipStateRebuild) {
156
+ const now = Date.now();
157
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
158
+ try {
159
+ await rebuildState(s.basePath);
160
+ s.lastStateRebuildAt = now;
161
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
162
+ }
163
+ catch (e) {
164
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
165
+ }
144
166
  }
145
167
  }
146
168
  // Prune dead bg-shell processes
@@ -148,27 +170,41 @@ export async function postUnitPreVerification(pctx) {
148
170
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
149
171
  pruneDeadProcesses();
150
172
  }
151
- catch {
152
- // Non-fatal
173
+ catch (e) {
174
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
153
175
  }
154
- // Sync worktree state back to project root
155
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
176
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
177
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
156
178
  try {
157
179
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
158
180
  }
159
- catch {
160
- // Non-fatal
181
+ catch (e) {
182
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
161
183
  }
162
184
  }
163
185
  // Rewrite-docs completion
164
186
  if (s.currentUnit.type === "rewrite-docs") {
165
187
  try {
166
188
  await resolveAllOverrides(s.basePath);
167
- resetRewriteCircuitBreaker();
189
+ s.rewriteAttemptCount = 0;
168
190
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
169
191
  }
170
- catch {
171
- // Non-fatal
192
+ catch (e) {
193
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
194
+ }
195
+ }
196
+ // Reactive state cleanup on slice completion
197
+ if (s.currentUnit.type === "complete-slice") {
198
+ try {
199
+ const parts = s.currentUnit.id.split("/");
200
+ const [mid, sid] = parts;
201
+ if (mid && sid) {
202
+ const { clearReactiveState } = await import("./reactive-graph.js");
203
+ clearReactiveState(s.basePath, mid, sid);
204
+ }
205
+ }
206
+ catch (e) {
207
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
172
208
  }
173
209
  }
174
210
  // Post-triage: execute actionable resolutions
@@ -210,8 +246,8 @@ export async function postUnitPreVerification(pctx) {
210
246
  invalidateAllCaches();
211
247
  }
212
248
  }
213
- catch {
214
- // Non-fatal
249
+ catch (e) {
250
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
215
251
  }
216
252
  }
217
253
  else {
@@ -224,8 +260,8 @@ export async function postUnitPreVerification(pctx) {
224
260
  });
225
261
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
226
262
  }
227
- catch {
228
- // Non-fatal
263
+ catch (e) {
264
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
229
265
  }
230
266
  }
231
267
  }
@@ -338,8 +374,8 @@ export async function postUnitPostVerification(pctx) {
338
374
  }
339
375
  }
340
376
  }
341
- catch {
342
- // Triage check failure is non-fatal
377
+ catch (e) {
378
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
343
379
  }
344
380
  }
345
381
  // ── Quick-task dispatch ──
@@ -373,8 +409,8 @@ export async function postUnitPostVerification(pctx) {
373
409
  ctx.ui.notify(`Executing quick-task: ${capture.id} — "${capture.text}"`, "info");
374
410
  return "continue";
375
411
  }
376
- catch {
377
- // Non-fatal proceed to normal dispatch
412
+ catch (e) {
413
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
378
414
  }
379
415
  }
380
416
  // Step mode → show wizard instead of dispatch
@@ -12,10 +12,7 @@ import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferen
12
12
  import { join } from "node:path";
13
13
  import { existsSync } from "node:fs";
14
14
  import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
15
- import { compressToTarget } from "./prompt-compressor.js";
16
- import { distillSummaries } from "./summary-distiller.js";
17
15
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
18
- import { chunkByRelevance, formatChunks } from "./semantic-chunker.js";
19
16
  // ─── Executor Constraints ─────────────────────────────────────────────────────
20
17
  /**
21
18
  * Format executor context constraints for injection into the plan-slice prompt.
@@ -126,14 +123,10 @@ export async function inlineFileSmart(absPath, relPath, label, query, threshold
126
123
  if (content.length <= threshold || !query) {
127
124
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
128
125
  }
129
- // Use semantic chunking for large files
130
- const result = chunkByRelevance(content, query, { maxChunks: 5, minScore: 0.05 });
131
- // If chunking didn't save much (< 20%), just include full content
132
- if (result.savingsPercent < 20) {
133
- return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
134
- }
135
- const formatted = formatChunks(result, relPath);
136
- return `### ${label} (${result.omittedChunks} sections omitted for relevance)\nSource: \`${relPath}\`\n\n${formatted}`;
126
+ // For large files, truncate at section boundary
127
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
128
+ const truncated = truncateAtSectionBoundary(content, threshold).content;
129
+ return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
137
130
  }
138
131
  /**
139
132
  * Load and inline dependency slice summaries (full content, not just paths).
@@ -165,20 +158,6 @@ export async function inlineDependencySummaries(mid, sid, base, budgetChars) {
165
158
  }
166
159
  const result = sections.join("\n\n");
167
160
  if (budgetChars !== undefined && result.length > budgetChars) {
168
- // For 3+ summaries, try distillation first (preserves more information)
169
- if (sections.length >= 3) {
170
- const rawSummaries = sections.map(s => {
171
- // Extract content after the header line
172
- const lines = s.split("\n");
173
- const contentStart = lines.findIndex(l => l.startsWith("Source:"));
174
- return contentStart >= 0 ? lines.slice(contentStart + 1).join("\n").trim() : s;
175
- });
176
- const distilled = distillSummaries(rawSummaries, budgetChars);
177
- if (distilled.content.length <= budgetChars) {
178
- return distilled.content;
179
- }
180
- }
181
- // Fall back to section-boundary truncation
182
161
  const { truncateAtSectionBoundary } = await import("./context-budget.js");
183
162
  return truncateAtSectionBoundary(result, budgetChars).content;
184
163
  }
@@ -414,6 +393,35 @@ export async function getPriorTaskSummaryPaths(mid, sid, currentTid, base) {
414
393
  })
415
394
  .map(f => `${sRel}/tasks/${f}`);
416
395
  }
396
+ /**
397
+ * Get carry-forward summary paths scoped to a task's derived dependencies.
398
+ *
399
+ * Instead of all prior tasks (order-based), returns only summaries for task
400
+ * IDs in `dependsOn`. Used by reactive-execute to give each subagent only
401
+ * the context it actually needs — not sibling tasks from a parallel batch.
402
+ *
403
+ * Falls back to order-based when dependsOn is empty (root tasks still get
404
+ * any available prior summaries for continuity).
405
+ */
406
+ export async function getDependencyTaskSummaryPaths(mid, sid, currentTid, dependsOn, base) {
407
+ // If no dependencies, fall back to order-based for root tasks
408
+ if (dependsOn.length === 0) {
409
+ return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
410
+ }
411
+ const tDir = resolveTasksDir(base, mid, sid);
412
+ if (!tDir)
413
+ return [];
414
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
415
+ const sRel = relSlicePath(base, mid, sid);
416
+ const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
417
+ return summaryFiles
418
+ .filter((f) => {
419
+ // Extract task ID from filename: "T02-SUMMARY.md" → "T02"
420
+ const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
421
+ return depSet.has(tid);
422
+ })
423
+ .map((f) => `${sRel}/tasks/${f}`);
424
+ }
417
425
  // ─── Adaptive Replanning Checks ────────────────────────────────────────────
418
426
  /**
419
427
  * Check if the most recently completed slice needs reassessment.
@@ -688,8 +696,11 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
688
696
  });
689
697
  }
690
698
  export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, level) {
691
- const inlineLevel = level ?? resolveInlineLevel();
692
- const priorSummaries = await getPriorTaskSummaryPaths(mid, sid, tid, base);
699
+ const opts = typeof level === "object" && level !== null && !Array.isArray(level)
700
+ ? level
701
+ : { level: level };
702
+ const inlineLevel = opts.level ?? resolveInlineLevel();
703
+ const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
693
704
  const priorLines = priorSummaries.length > 0
694
705
  ? priorSummaries.map(p => `- \`${p}\``).join("\n")
695
706
  : "- (no prior tasks)";
@@ -745,15 +756,12 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
745
756
  const contextWindow = resolveExecutorContextWindow(undefined, prefs?.preferences);
746
757
  const budgets = computeBudgets(contextWindow);
747
758
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
748
- // Compress carry-forward section when it exceeds 40% of inline context budget.
749
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
759
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
750
760
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
751
761
  let finalCarryForward = carryForwardSection;
752
762
  if (carryForwardSection.length > carryForwardBudget) {
753
- const { resolveCompressionStrategy } = await import("./preferences.js");
754
- if (resolveCompressionStrategy() === "compress") {
755
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
756
- }
763
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
764
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
757
765
  }
758
766
  return loadPrompt("execute-task", {
759
767
  overridesSection,
@@ -1090,6 +1098,63 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1090
1098
  commitInstruction: reassessCommitInstruction,
1091
1099
  });
1092
1100
  }
1101
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1102
+ export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base) {
1103
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1104
+ // Build graph for context
1105
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1106
+ const graph = deriveTaskGraph(taskIO);
1107
+ const metrics = graphMetrics(graph);
1108
+ // Build graph context section
1109
+ const graphLines = [];
1110
+ for (const node of graph) {
1111
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1112
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1113
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1114
+ if (node.outputFiles.length > 0) {
1115
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1116
+ }
1117
+ }
1118
+ const graphContext = [
1119
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1120
+ "",
1121
+ ...graphLines,
1122
+ ].join("\n");
1123
+ // Build individual subagent prompts for each ready task
1124
+ const subagentSections = [];
1125
+ const readyTaskListLines = [];
1126
+ for (const tid of readyTaskIds) {
1127
+ const node = graph.find((n) => n.id === tid);
1128
+ const tTitle = node?.title ?? tid;
1129
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1130
+ // Build dependency-scoped carry-forward paths for this task
1131
+ const depPaths = await getDependencyTaskSummaryPaths(mid, sid, tid, node?.dependsOn ?? [], base);
1132
+ // Build a full execute-task prompt with dependency-based carry-forward
1133
+ const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, { carryForwardPaths: depPaths });
1134
+ subagentSections.push([
1135
+ `### ${tid}: ${tTitle}`,
1136
+ "",
1137
+ "Use this as the prompt for a `subagent` call:",
1138
+ "",
1139
+ "```",
1140
+ taskPrompt,
1141
+ "```",
1142
+ ].join("\n"));
1143
+ }
1144
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1145
+ return loadPrompt("reactive-execute", {
1146
+ workingDirectory: base,
1147
+ milestoneId: mid,
1148
+ milestoneTitle: midTitle,
1149
+ sliceId: sid,
1150
+ sliceTitle: sTitle,
1151
+ graphContext,
1152
+ readyTaskCount: String(readyTaskIds.length),
1153
+ readyTaskList: readyTaskListLines.join("\n"),
1154
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1155
+ inlinedTemplates,
1156
+ });
1157
+ }
1093
1158
  export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, overrides) {
1094
1159
  const sid = activeSlice?.id;
1095
1160
  const sTitle = activeSlice?.title ?? "";
@@ -11,7 +11,7 @@ import { clearUnitRuntimeRecord } from "./unit-runtime.js";
11
11
  import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
12
12
  import { isValidationTerminal } from "./state.js";
13
13
  import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeResetHard, } from "./native-git-bridge.js";
14
- import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
14
+ import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
15
15
  import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
17
17
  import { dirname, join } from "node:path";
@@ -73,6 +73,9 @@ export function resolveExpectedArtifactPath(unitType, unitId, base) {
73
73
  }
74
74
  case "rewrite-docs":
75
75
  return null;
76
+ case "reactive-execute":
77
+ // Reactive execute produces multiple task summaries — verified separately
78
+ return null;
76
79
  default:
77
80
  return null;
78
81
  }
@@ -105,6 +108,39 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
105
108
  const content = readFileSync(overridesPath, "utf-8");
106
109
  return !content.includes("**Scope:** active");
107
110
  }
111
+ // Reactive-execute: verify that each dispatched task's summary exists.
112
+ // The unitId encodes the batch: "{mid}/{sid}/reactive+T02,T03"
113
+ if (unitType === "reactive-execute") {
114
+ const parts = unitId.split("/");
115
+ const mid = parts[0];
116
+ const sidAndBatch = parts[1];
117
+ const batchPart = parts[2]; // "reactive+T02,T03"
118
+ if (!mid || !sidAndBatch || !batchPart)
119
+ return false;
120
+ const sid = sidAndBatch;
121
+ const plusIdx = batchPart.indexOf("+");
122
+ if (plusIdx === -1) {
123
+ // Legacy format "reactive" without batch IDs — fall back to "any summary"
124
+ const tDir = resolveTasksDir(base, mid, sid);
125
+ if (!tDir)
126
+ return false;
127
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
128
+ return summaryFiles.length > 0;
129
+ }
130
+ const batchIds = batchPart.slice(plusIdx + 1).split(",").filter(Boolean);
131
+ if (batchIds.length === 0)
132
+ return false;
133
+ const tDir = resolveTasksDir(base, mid, sid);
134
+ if (!tDir)
135
+ return false;
136
+ const existingSummaries = new Set(resolveTaskFiles(tDir, "SUMMARY").map((f) => f.replace(/-SUMMARY\.md$/i, "").toUpperCase()));
137
+ // Every dispatched task must have a summary file
138
+ for (const tid of batchIds) {
139
+ if (!existingSummaries.has(tid.toUpperCase()))
140
+ return false;
141
+ }
142
+ return true;
143
+ }
108
144
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
109
145
  // For unit types with no verifiable artifact (null path), the parent directory
110
146
  // is missing on disk — treat as stale completion state so the key gets evicted (#313).
@@ -11,7 +11,7 @@
11
11
  import { deriveState } from "./state.js";
12
12
  import { loadFile, getManifestStatus } from "./files.js";
13
13
  import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
14
- import { ensureGsdSymlink } from "./repo-identity.js";
14
+ import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
15
15
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
16
16
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
17
17
  import { gsdRoot, resolveMilestoneFile } from "./paths.js";
@@ -63,6 +63,12 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
63
63
  return false;
64
64
  }
65
65
  try {
66
+ // Validate GSD_PROJECT_ID early so the user gets immediate feedback
67
+ const customProjectId = process.env.GSD_PROJECT_ID;
68
+ if (customProjectId && !validateProjectId(customProjectId)) {
69
+ ctx.ui.notify(`GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`, "error");
70
+ return releaseLockAndReturn();
71
+ }
66
72
  // Ensure git repo exists
67
73
  if (!nativeIsRepo(base)) {
68
74
  const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
@@ -303,11 +309,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
303
309
  // ── Auto-worktree setup ──
304
310
  s.originalBasePath = base;
305
311
  const isUnderGsdWorktrees = (p) => {
312
+ // Direct layout: /.gsd/worktrees/
306
313
  const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
307
314
  if (p.includes(marker))
308
315
  return true;
309
316
  const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
310
- return p.endsWith(worktreesSuffix);
317
+ if (p.endsWith(worktreesSuffix))
318
+ return true;
319
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
320
+ const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`);
321
+ return symlinkRe.test(p);
311
322
  };
312
323
  if (s.currentMilestoneId &&
313
324
  shouldUseWorktreeIsolation() &&
@@ -13,6 +13,7 @@ import { existsSync, readFileSync, unlinkSync, readdirSync, } from "node:fs";
13
13
  import { join, sep as pathSep } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
16
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
17
  // ─── Project Root → Worktree Sync ─────────────────────────────────────────
17
18
  /**
18
19
  * Sync milestone artifacts from project root INTO worktree before deriveState.
@@ -75,7 +76,7 @@ export function syncStateToProjectRoot(worktreePath, projectRoot, milestoneId) {
75
76
  * doesn't falsely trigger staleness (#804).
76
77
  */
77
78
  export function readResourceVersion() {
78
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
79
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
79
80
  const manifestPath = join(agentDir, "managed-resources.json");
80
81
  try {
81
82
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -115,10 +116,17 @@ export function checkResourcesStale(versionOnStart) {
115
116
  * Returns the corrected base path.
116
117
  */
117
118
  export function escapeStaleWorktree(base) {
118
- const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
119
- const idx = base.indexOf(marker);
120
- if (idx === -1)
121
- return base;
119
+ // Direct layout: /.gsd/worktrees/
120
+ const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
121
+ let idx = base.indexOf(directMarker);
122
+ if (idx === -1) {
123
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
124
+ const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`);
125
+ const match = base.match(symlinkRe);
126
+ if (!match || match.index === undefined)
127
+ return base;
128
+ idx = match.index;
129
+ }
122
130
  // base is inside .gsd/worktrees/<something> — extract the project root
123
131
  const projectRoot = base.slice(0, idx);
124
132
  try {