gsd-pi 2.37.1-dev.d3ace49 → 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 (150) 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/resources/extensions/browser-tools/package.json +3 -1
  8. package/dist/resources/extensions/cmux/index.js +55 -1
  9. package/dist/resources/extensions/context7/package.json +1 -1
  10. package/dist/resources/extensions/env-utils.js +29 -0
  11. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  12. package/dist/resources/extensions/google-search/package.json +3 -1
  13. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  14. package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
  15. package/dist/resources/extensions/gsd/auto-loop.js +68 -97
  16. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -71
  17. package/dist/resources/extensions/gsd/auto-prompts.js +7 -31
  18. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  19. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  20. package/dist/resources/extensions/gsd/auto.js +143 -96
  21. package/dist/resources/extensions/gsd/captures.js +9 -1
  22. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  23. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  24. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  25. package/dist/resources/extensions/gsd/commands.js +22 -2
  26. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  27. package/dist/resources/extensions/gsd/detection.js +1 -2
  28. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  29. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  30. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  31. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  32. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  33. package/dist/resources/extensions/gsd/doctor.js +184 -11
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +2 -2
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/index.js +2 -1
  38. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  39. package/dist/resources/extensions/gsd/package.json +1 -1
  40. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  41. package/dist/resources/extensions/gsd/preferences-types.js +0 -1
  42. package/dist/resources/extensions/gsd/preferences-validation.js +1 -11
  43. package/dist/resources/extensions/gsd/preferences.js +5 -5
  44. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  45. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  46. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  47. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  48. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  49. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  50. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  51. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  52. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  53. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  54. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  55. package/dist/resources/extensions/gsd/state.js +1 -1
  56. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  57. package/dist/resources/extensions/gsd/worktree.js +35 -16
  58. package/dist/resources/extensions/remote-questions/status.js +2 -1
  59. package/dist/resources/extensions/remote-questions/store.js +2 -1
  60. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  61. package/dist/resources/extensions/subagent/index.js +12 -3
  62. package/dist/resources/extensions/subagent/isolation.js +2 -1
  63. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  64. package/dist/resources/extensions/universal-config/package.json +1 -1
  65. package/dist/welcome-screen.d.ts +12 -0
  66. package/dist/welcome-screen.js +53 -0
  67. package/package.json +1 -1
  68. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  70. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  71. package/packages/pi-coding-agent/package.json +1 -1
  72. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  73. package/pkg/package.json +1 -1
  74. package/src/resources/extensions/cmux/index.ts +57 -1
  75. package/src/resources/extensions/env-utils.ts +31 -0
  76. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  77. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  78. package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
  79. package/src/resources/extensions/gsd/auto-loop.ts +88 -133
  80. package/src/resources/extensions/gsd/auto-post-unit.ts +52 -42
  81. package/src/resources/extensions/gsd/auto-prompts.ts +7 -33
  82. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  83. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  84. package/src/resources/extensions/gsd/auto.ts +139 -101
  85. package/src/resources/extensions/gsd/captures.ts +10 -1
  86. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  87. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  88. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  89. package/src/resources/extensions/gsd/commands.ts +24 -2
  90. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  91. package/src/resources/extensions/gsd/detection.ts +2 -2
  92. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  93. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  94. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  95. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  96. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  97. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  98. package/src/resources/extensions/gsd/doctor.ts +177 -13
  99. package/src/resources/extensions/gsd/export.ts +1 -1
  100. package/src/resources/extensions/gsd/files.ts +2 -2
  101. package/src/resources/extensions/gsd/forensics.ts +1 -1
  102. package/src/resources/extensions/gsd/index.ts +3 -1
  103. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  104. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  105. package/src/resources/extensions/gsd/preferences-types.ts +0 -4
  106. package/src/resources/extensions/gsd/preferences-validation.ts +1 -11
  107. package/src/resources/extensions/gsd/preferences.ts +5 -5
  108. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  109. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  110. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  111. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  112. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  113. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  114. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  115. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  116. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  117. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  118. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  119. package/src/resources/extensions/gsd/state.ts +1 -1
  120. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  121. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
  122. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  123. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  124. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  125. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  126. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  127. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  128. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  129. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  130. package/src/resources/extensions/gsd/types.ts +0 -1
  131. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  132. package/src/resources/extensions/gsd/worktree.ts +35 -15
  133. package/src/resources/extensions/remote-questions/status.ts +3 -1
  134. package/src/resources/extensions/remote-questions/store.ts +3 -1
  135. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  136. package/src/resources/extensions/subagent/index.ts +12 -3
  137. package/src/resources/extensions/subagent/isolation.ts +3 -1
  138. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  139. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  140. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  141. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  142. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  143. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  144. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  145. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  146. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  147. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  148. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  149. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  150. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -33,7 +33,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
33
33
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
34
34
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
35
35
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
36
- import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
37
36
  import { isDbAvailable } from "./gsd-db.js";
38
37
  import { consumeSignal } from "./session-status-io.js";
39
38
  import {
@@ -56,6 +55,13 @@ import { join } from "node:path";
56
55
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
57
56
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
58
57
 
58
+ export interface PreVerificationOpts {
59
+ skipSettleDelay?: boolean;
60
+ skipDoctor?: boolean;
61
+ skipStateRebuild?: boolean;
62
+ skipWorktreeSync?: boolean;
63
+ }
64
+
59
65
  export interface PostUnitContext {
60
66
  s: AutoSession;
61
67
  ctx: ExtensionContext;
@@ -73,7 +79,7 @@ export interface PostUnitContext {
73
79
  *
74
80
  * Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
75
81
  */
76
- export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"dispatched" | "continue"> {
82
+ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreVerificationOpts): Promise<"dispatched" | "continue"> {
77
83
  const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
78
84
 
79
85
  // ── Parallel worker signal check ──
@@ -95,8 +101,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
95
101
  // Invalidate all caches
96
102
  invalidateAllCaches();
97
103
 
98
- // Small delay to let files settle
99
- await new Promise(r => setTimeout(r, 500));
104
+ // Small delay to let files settle (skipped for sidecars where latency matters more)
105
+ if (!opts?.skipSettleDelay) {
106
+ await new Promise(r => setTimeout(r, 100));
107
+ }
100
108
 
101
109
  // Auto-commit
102
110
  if (s.currentUnit) {
@@ -120,8 +128,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
120
128
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
121
129
  };
122
130
  }
123
- } catch {
124
- // Non-fatal
131
+ } catch (e) {
132
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
125
133
  }
126
134
  }
127
135
  }
@@ -131,12 +139,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
131
139
  if (commitMsg) {
132
140
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
133
141
  }
134
- } catch {
135
- // Non-fatal
142
+ } catch (e) {
143
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
136
144
  }
137
145
 
138
- // Doctor: fix mechanical bookkeeping
139
- try {
146
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
147
+ if (!opts?.skipDoctor) try {
140
148
  const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
141
149
  const doctorScope = scopeParts.join("/");
142
150
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
@@ -168,24 +176,26 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
168
176
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
169
177
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
170
178
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
171
- } catch {
172
- // Non-fatal
179
+ } catch (e) {
180
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
173
181
  }
174
182
  }
175
183
  }
176
- } catch {
177
- // Non-fatal
184
+ } catch (e) {
185
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
178
186
  }
179
187
 
180
- // Throttled STATE.md rebuild
181
- const now = Date.now();
182
- if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
183
- try {
184
- await rebuildState(s.basePath);
185
- s.lastStateRebuildAt = now;
186
- autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
187
- } catch {
188
- // Non-fatal
188
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
189
+ if (!opts?.skipStateRebuild) {
190
+ const now = Date.now();
191
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
192
+ try {
193
+ await rebuildState(s.basePath);
194
+ s.lastStateRebuildAt = now;
195
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
196
+ } catch (e) {
197
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
198
+ }
189
199
  }
190
200
  }
191
201
 
@@ -193,16 +203,16 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
193
203
  try {
194
204
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
195
205
  pruneDeadProcesses();
196
- } catch {
197
- // Non-fatal
206
+ } catch (e) {
207
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
198
208
  }
199
209
 
200
- // Sync worktree state back to project root
201
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
210
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
211
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
202
212
  try {
203
213
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
204
- } catch {
205
- // Non-fatal
214
+ } catch (e) {
215
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
206
216
  }
207
217
  }
208
218
 
@@ -210,10 +220,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
210
220
  if (s.currentUnit.type === "rewrite-docs") {
211
221
  try {
212
222
  await resolveAllOverrides(s.basePath);
213
- resetRewriteCircuitBreaker();
223
+ s.rewriteAttemptCount = 0;
214
224
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
215
- } catch {
216
- // Non-fatal
225
+ } catch (e) {
226
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
217
227
  }
218
228
  }
219
229
 
@@ -226,8 +236,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
226
236
  const { clearReactiveState } = await import("./reactive-graph.js");
227
237
  clearReactiveState(s.basePath, mid, sid);
228
238
  }
229
- } catch {
230
- // Non-fatal
239
+ } catch (e) {
240
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
231
241
  }
232
242
  }
233
243
 
@@ -280,8 +290,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
280
290
  if (triggerArtifactVerified) {
281
291
  invalidateAllCaches();
282
292
  }
283
- } catch {
284
- // Non-fatal
293
+ } catch (e) {
294
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
285
295
  }
286
296
  } else {
287
297
  // Hook unit completed — finalize its runtime record
@@ -292,8 +302,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
292
302
  lastProgressKind: "hook-completed",
293
303
  });
294
304
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
295
- } catch {
296
- // Non-fatal
305
+ } catch (e) {
306
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
297
307
  }
298
308
  }
299
309
  }
@@ -429,8 +439,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
429
439
  }
430
440
  }
431
441
  }
432
- } catch {
433
- // Triage check failure is non-fatal
442
+ } catch (e) {
443
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
434
444
  }
435
445
  }
436
446
 
@@ -475,8 +485,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
475
485
  );
476
486
 
477
487
  return "continue";
478
- } catch {
479
- // Non-fatal proceed to normal dispatch
488
+ } catch (e) {
489
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
480
490
  }
481
491
  }
482
492
 
@@ -21,10 +21,7 @@ import type { GSDPreferences } from "./preferences.js";
21
21
  import { join } from "node:path";
22
22
  import { existsSync } from "node:fs";
23
23
  import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
24
- import { compressToTarget } from "./prompt-compressor.js";
25
- import { distillSummaries } from "./summary-distiller.js";
26
24
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
27
- import { chunkByRelevance, formatChunks } from "./semantic-chunker.js";
28
25
 
29
26
  // ─── Executor Constraints ─────────────────────────────────────────────────────
30
27
 
@@ -159,16 +156,10 @@ export async function inlineFileSmart(
159
156
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
160
157
  }
161
158
 
162
- // Use semantic chunking for large files
163
- const result = chunkByRelevance(content, query, { maxChunks: 5, minScore: 0.05 });
164
-
165
- // If chunking didn't save much (< 20%), just include full content
166
- if (result.savingsPercent < 20) {
167
- return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
168
- }
169
-
170
- const formatted = formatChunks(result, relPath);
171
- return `### ${label} (${result.omittedChunks} sections omitted for relevance)\nSource: \`${relPath}\`\n\n${formatted}`;
159
+ // For large files, truncate at section boundary
160
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
161
+ const truncated = truncateAtSectionBoundary(content, threshold).content;
162
+ return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
172
163
  }
173
164
 
174
165
  /**
@@ -202,20 +193,6 @@ export async function inlineDependencySummaries(
202
193
 
203
194
  const result = sections.join("\n\n");
204
195
  if (budgetChars !== undefined && result.length > budgetChars) {
205
- // For 3+ summaries, try distillation first (preserves more information)
206
- if (sections.length >= 3) {
207
- const rawSummaries = sections.map(s => {
208
- // Extract content after the header line
209
- const lines = s.split("\n");
210
- const contentStart = lines.findIndex(l => l.startsWith("Source:"));
211
- return contentStart >= 0 ? lines.slice(contentStart + 1).join("\n").trim() : s;
212
- });
213
- const distilled = distillSummaries(rawSummaries, budgetChars);
214
- if (distilled.content.length <= budgetChars) {
215
- return distilled.content;
216
- }
217
- }
218
- // Fall back to section-boundary truncation
219
196
  const { truncateAtSectionBoundary } = await import("./context-budget.js");
220
197
  return truncateAtSectionBoundary(result, budgetChars).content;
221
198
  }
@@ -900,15 +877,12 @@ export async function buildExecuteTaskPrompt(
900
877
  const budgets = computeBudgets(contextWindow);
901
878
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
902
879
 
903
- // Compress carry-forward section when it exceeds 40% of inline context budget.
904
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
880
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
905
881
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
906
882
  let finalCarryForward = carryForwardSection;
907
883
  if (carryForwardSection.length > carryForwardBudget) {
908
- const { resolveCompressionStrategy } = await import("./preferences.js");
909
- if (resolveCompressionStrategy() === "compress") {
910
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
911
- }
884
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
885
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
912
886
  }
913
887
 
914
888
  return loadPrompt("execute-task", {
@@ -20,7 +20,7 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
- import { ensureGsdSymlink } from "./repo-identity.js";
23
+ import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
24
24
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
25
25
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
26
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
@@ -130,6 +130,16 @@ export async function bootstrapAutoSession(
130
130
  }
131
131
 
132
132
  try {
133
+ // Validate GSD_PROJECT_ID early so the user gets immediate feedback
134
+ const customProjectId = process.env.GSD_PROJECT_ID;
135
+ if (customProjectId && !validateProjectId(customProjectId)) {
136
+ ctx.ui.notify(
137
+ `GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`,
138
+ "error",
139
+ );
140
+ return releaseLockAndReturn();
141
+ }
142
+
133
143
  // Ensure git repo exists
134
144
  if (!nativeIsRepo(base)) {
135
145
  const mainBranch =
@@ -429,10 +439,16 @@ export async function bootstrapAutoSession(
429
439
  s.originalBasePath = base;
430
440
 
431
441
  const isUnderGsdWorktrees = (p: string): boolean => {
442
+ // Direct layout: /.gsd/worktrees/
432
443
  const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
433
444
  if (p.includes(marker)) return true;
434
445
  const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
435
- return p.endsWith(worktreesSuffix);
446
+ if (p.endsWith(worktreesSuffix)) return true;
447
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
448
+ const symlinkRe = new RegExp(
449
+ `\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`,
450
+ );
451
+ return symlinkRe.test(p);
436
452
  };
437
453
 
438
454
  if (
@@ -22,6 +22,8 @@ import { join, sep as pathSep } from "node:path";
22
22
  import { homedir } from "node:os";
23
23
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
24
24
 
25
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
26
+
25
27
  // ─── Project Root → Worktree Sync ─────────────────────────────────────────
26
28
 
27
29
  /**
@@ -111,7 +113,7 @@ export function syncStateToProjectRoot(
111
113
  */
112
114
  export function readResourceVersion(): string | null {
113
115
  const agentDir =
114
- process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
116
+ process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
115
117
  const manifestPath = join(agentDir, "managed-resources.json");
116
118
  try {
117
119
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -153,9 +155,18 @@ export function checkResourcesStale(
153
155
  * Returns the corrected base path.
154
156
  */
155
157
  export function escapeStaleWorktree(base: string): string {
156
- const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
157
- const idx = base.indexOf(marker);
158
- if (idx === -1) return base;
158
+ // Direct layout: /.gsd/worktrees/
159
+ const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
160
+ let idx = base.indexOf(directMarker);
161
+ if (idx === -1) {
162
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
163
+ const symlinkRe = new RegExp(
164
+ `\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`,
165
+ );
166
+ const match = base.match(symlinkRe);
167
+ if (!match || match.index === undefined) return base;
168
+ idx = match.index;
169
+ }
159
170
 
160
171
  // base is inside .gsd/worktrees/<something> — extract the project root
161
172
  const projectRoot = base.slice(0, idx);
@@ -536,129 +536,167 @@ export async function stopAuto(
536
536
  if (!s.active && !s.paused) return;
537
537
  const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
538
538
  const reasonSuffix = reason ? ` — ${reason}` : "";
539
- clearUnitTimeout();
540
- if (lockBase()) clearLock(lockBase());
541
- if (lockBase()) releaseSessionLock(lockBase());
542
- clearSkillSnapshot();
543
- resetSkillTelemetry();
544
539
 
545
- // Remove SIGTERM handler registered at auto-mode start
546
- deregisterSigtermHandler();
540
+ try {
541
+ // ── Step 1: Timers and locks ──
542
+ try {
543
+ clearUnitTimeout();
544
+ if (lockBase()) clearLock(lockBase());
545
+ if (lockBase()) releaseSessionLock(lockBase());
546
+ } catch (e) {
547
+ debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
548
+ }
547
549
 
548
- // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
549
- if (s.currentMilestoneId) {
550
- const notifyCtx = ctx
551
- ? { notify: ctx.ui.notify.bind(ctx.ui) }
552
- : { notify: () => {} };
553
- buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
554
- preserveBranch: true,
555
- });
556
- }
550
+ // ── Step 2: Skill state ──
551
+ try {
552
+ clearSkillSnapshot();
553
+ resetSkillTelemetry();
554
+ } catch (e) {
555
+ debugLog("stop-cleanup-skills", { error: e instanceof Error ? e.message : String(e) });
556
+ }
557
557
 
558
- // ── DB cleanup: close the SQLite connection ──
559
- if (isDbAvailable()) {
558
+ // ── Step 3: SIGTERM handler ──
560
559
  try {
561
- const { closeDatabase } = await import("./gsd-db.js");
562
- closeDatabase();
560
+ deregisterSigtermHandler();
563
561
  } catch (e) {
564
- debugLog("db-close-failed", {
565
- error: e instanceof Error ? e.message : String(e),
566
- });
562
+ debugLog("stop-cleanup-sigterm", { error: e instanceof Error ? e.message : String(e) });
567
563
  }
568
- }
569
564
 
570
- if (s.originalBasePath) {
571
- s.basePath = s.originalBasePath;
565
+ // ── Step 4: Auto-worktree exit ──
572
566
  try {
573
- process.chdir(s.basePath);
574
- } catch {
575
- /* best-effort */
567
+ if (s.currentMilestoneId) {
568
+ const notifyCtx = ctx
569
+ ? { notify: ctx.ui.notify.bind(ctx.ui) }
570
+ : { notify: () => {} };
571
+ buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
572
+ preserveBranch: true,
573
+ });
574
+ }
575
+ } catch (e) {
576
+ debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
576
577
  }
577
- }
578
578
 
579
- const ledger = getLedger();
580
- if (ledger && ledger.units.length > 0) {
581
- const totals = getProjectTotals(ledger.units);
582
- ctx?.ui.notify(
583
- `Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
584
- "info",
585
- );
586
- } else {
587
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
588
- }
579
+ // ── Step 5: DB cleanup ──
580
+ if (isDbAvailable()) {
581
+ try {
582
+ const { closeDatabase } = await import("./gsd-db.js");
583
+ closeDatabase();
584
+ } catch (e) {
585
+ debugLog("db-close-failed", {
586
+ error: e instanceof Error ? e.message : String(e),
587
+ });
588
+ }
589
+ }
589
590
 
590
- if (s.basePath) {
591
+ // ── Step 6: Restore basePath and chdir ──
591
592
  try {
592
- await rebuildState(s.basePath);
593
+ if (s.originalBasePath) {
594
+ s.basePath = s.originalBasePath;
595
+ try {
596
+ process.chdir(s.basePath);
597
+ } catch {
598
+ /* best-effort */
599
+ }
600
+ }
593
601
  } catch (e) {
594
- debugLog("stop-rebuild-state-failed", {
595
- error: e instanceof Error ? e.message : String(e),
596
- });
602
+ debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
597
603
  }
598
- }
599
604
 
600
- clearCmuxSidebar(loadedPreferences);
601
- logCmuxEvent(
602
- loadedPreferences,
603
- `Auto-mode stopped${reasonSuffix || ""}.`,
604
- reason?.startsWith("Blocked:") ? "warning" : "info",
605
- );
605
+ // ── Step 7: Ledger notification ──
606
+ try {
607
+ const ledger = getLedger();
608
+ if (ledger && ledger.units.length > 0) {
609
+ const totals = getProjectTotals(ledger.units);
610
+ ctx?.ui.notify(
611
+ `Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
612
+ "info",
613
+ );
614
+ } else {
615
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
616
+ }
617
+ } catch (e) {
618
+ debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
619
+ }
606
620
 
607
- if (isDebugEnabled()) {
608
- const logPath = writeDebugSummary();
609
- if (logPath) {
610
- ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
621
+ // ── Step 8: Rebuild state ──
622
+ if (s.basePath) {
623
+ try {
624
+ await rebuildState(s.basePath);
625
+ } catch (e) {
626
+ debugLog("stop-rebuild-state-failed", {
627
+ error: e instanceof Error ? e.message : String(e),
628
+ });
629
+ }
611
630
  }
612
- }
613
631
 
614
- resetMetrics();
615
- resetRoutingHistory();
616
- resetHookState();
617
- if (s.basePath) clearPersistedHookState(s.basePath);
632
+ // ── Step 9: Cmux sidebar / event log ──
633
+ try {
634
+ clearCmuxSidebar(loadedPreferences);
635
+ logCmuxEvent(
636
+ loadedPreferences,
637
+ `Auto-mode stopped${reasonSuffix || ""}.`,
638
+ reason?.startsWith("Blocked:") ? "warning" : "info",
639
+ );
640
+ } catch (e) {
641
+ debugLog("stop-cleanup-cmux", { error: e instanceof Error ? e.message : String(e) });
642
+ }
618
643
 
619
- // Remove paused-session metadata if present (#1383)
620
- try {
621
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
622
- if (existsSync(pausedPath)) unlinkSync(pausedPath);
623
- } catch { /* non-fatal */ }
644
+ // ── Step 10: Debug summary ──
645
+ try {
646
+ if (isDebugEnabled()) {
647
+ const logPath = writeDebugSummary();
648
+ if (logPath) {
649
+ ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
650
+ }
651
+ }
652
+ } catch (e) {
653
+ debugLog("stop-cleanup-debug", { error: e instanceof Error ? e.message : String(e) });
654
+ }
624
655
 
625
- s.active = false;
626
- s.paused = false;
627
- s.stepMode = false;
628
- s.unitDispatchCount.clear();
629
- s.unitRecoveryCount.clear();
630
- clearInFlightTools();
631
- s.lastBudgetAlertLevel = 0;
632
- s.lastStateRebuildAt = 0;
633
- s.unitLifetimeDispatches.clear();
634
- s.currentUnit = null;
635
- s.autoModeStartModel = null;
636
- s.currentMilestoneId = null;
637
- s.originalBasePath = "";
638
- s.completedUnits = [];
639
- s.pendingQuickTasks = [];
640
- clearSliceProgressCache();
641
- clearActivityLogState();
642
- resetProactiveHealing();
643
- s.pendingCrashRecovery = null;
644
- s.pendingVerificationRetry = null;
645
- s.verificationRetryCount.clear();
646
- s.pausedSessionFile = null;
647
- ctx?.ui.setStatus("gsd-auto", undefined);
648
- ctx?.ui.setWidget("gsd-progress", undefined);
649
- ctx?.ui.setFooter(undefined);
656
+ // ── Step 11: Reset metrics, routing, hooks ──
657
+ try {
658
+ resetMetrics();
659
+ resetRoutingHistory();
660
+ resetHookState();
661
+ if (s.basePath) clearPersistedHookState(s.basePath);
662
+ } catch (e) {
663
+ debugLog("stop-cleanup-metrics", { error: e instanceof Error ? e.message : String(e) });
664
+ }
650
665
 
651
- if (pi && ctx && s.originalModelId && s.originalModelProvider) {
652
- const original = ctx.modelRegistry.find(
653
- s.originalModelProvider,
654
- s.originalModelId,
655
- );
656
- if (original) await pi.setModel(original);
657
- s.originalModelId = null;
658
- s.originalModelProvider = null;
659
- }
666
+ // ── Step 12: Remove paused-session metadata (#1383) ──
667
+ try {
668
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
669
+ if (existsSync(pausedPath)) unlinkSync(pausedPath);
670
+ } catch { /* non-fatal */ }
660
671
 
661
- s.cmdCtx = null;
672
+ // ── Step 13: Restore original model (before reset clears IDs) ──
673
+ try {
674
+ if (pi && ctx && s.originalModelId && s.originalModelProvider) {
675
+ const original = ctx.modelRegistry.find(
676
+ s.originalModelProvider,
677
+ s.originalModelId,
678
+ );
679
+ if (original) await pi.setModel(original);
680
+ }
681
+ } catch (e) {
682
+ debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
683
+ }
684
+ } finally {
685
+ // ── Critical invariants: these MUST execute regardless of errors ──
686
+ // External cleanup (not covered by session reset)
687
+ clearInFlightTools();
688
+ clearSliceProgressCache();
689
+ clearActivityLogState();
690
+ resetProactiveHealing();
691
+
692
+ // UI cleanup
693
+ ctx?.ui.setStatus("gsd-auto", undefined);
694
+ ctx?.ui.setWidget("gsd-progress", undefined);
695
+ ctx?.ui.setFooter(undefined);
696
+
697
+ // Reset all session state in one call
698
+ s.reset();
699
+ }
662
700
  }
663
701
 
664
702
  /**
@@ -59,8 +59,17 @@ const VALID_CLASSIFICATIONS: readonly string[] = [
59
59
  */
60
60
  export function resolveCapturesPath(basePath: string): string {
61
61
  const resolved = resolve(basePath);
62
+ // Direct layout: /.gsd/worktrees/
62
63
  const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
63
- const idx = resolved.indexOf(worktreeMarker);
64
+ let idx = resolved.indexOf(worktreeMarker);
65
+ if (idx === -1) {
66
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
67
+ const symlinkRe = new RegExp(
68
+ `\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`,
69
+ );
70
+ const match = resolved.match(symlinkRe);
71
+ if (match && match.index !== undefined) idx = match.index;
72
+ }
64
73
  if (idx !== -1) {
65
74
  // basePath is inside a worktree — resolve to project root
66
75
  const projectRoot = resolved.slice(0, idx);