gsd-pi 2.37.1-dev.d3ace49 → 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 (168) 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/github-sync/cli.js +284 -0
  13. package/dist/resources/extensions/github-sync/index.js +73 -0
  14. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  15. package/dist/resources/extensions/github-sync/sync.js +424 -0
  16. package/dist/resources/extensions/github-sync/templates.js +118 -0
  17. package/dist/resources/extensions/github-sync/types.js +7 -0
  18. package/dist/resources/extensions/google-search/package.json +3 -1
  19. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  20. package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
  21. package/dist/resources/extensions/gsd/auto-loop.js +149 -170
  22. package/dist/resources/extensions/gsd/auto-post-unit.js +92 -70
  23. package/dist/resources/extensions/gsd/auto-prompts.js +7 -31
  24. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  25. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  26. package/dist/resources/extensions/gsd/auto.js +143 -96
  27. package/dist/resources/extensions/gsd/captures.js +9 -1
  28. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  29. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  30. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  31. package/dist/resources/extensions/gsd/commands.js +22 -2
  32. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  33. package/dist/resources/extensions/gsd/detection.js +1 -2
  34. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  35. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  36. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  37. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  38. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  39. package/dist/resources/extensions/gsd/doctor.js +184 -11
  40. package/dist/resources/extensions/gsd/export.js +1 -1
  41. package/dist/resources/extensions/gsd/files.js +2 -2
  42. package/dist/resources/extensions/gsd/forensics.js +1 -1
  43. package/dist/resources/extensions/gsd/git-service.js +8 -1
  44. package/dist/resources/extensions/gsd/index.js +2 -1
  45. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  46. package/dist/resources/extensions/gsd/package.json +1 -1
  47. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  49. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  50. package/dist/resources/extensions/gsd/preferences.js +8 -5
  51. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  52. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  53. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  54. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  55. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  56. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  57. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  58. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  59. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  60. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  61. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  62. package/dist/resources/extensions/gsd/state.js +1 -1
  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 +1 -1
  75. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  77. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  80. package/pkg/package.json +1 -1
  81. package/src/resources/extensions/cmux/index.ts +57 -1
  82. package/src/resources/extensions/env-utils.ts +31 -0
  83. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  84. package/src/resources/extensions/github-sync/cli.ts +364 -0
  85. package/src/resources/extensions/github-sync/index.ts +93 -0
  86. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  87. package/src/resources/extensions/github-sync/sync.ts +556 -0
  88. package/src/resources/extensions/github-sync/templates.ts +183 -0
  89. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  90. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  91. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  92. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  93. package/src/resources/extensions/github-sync/types.ts +47 -0
  94. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  95. package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
  96. package/src/resources/extensions/gsd/auto-loop.ts +207 -252
  97. package/src/resources/extensions/gsd/auto-post-unit.ts +69 -41
  98. package/src/resources/extensions/gsd/auto-prompts.ts +7 -33
  99. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  100. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  101. package/src/resources/extensions/gsd/auto.ts +139 -101
  102. package/src/resources/extensions/gsd/captures.ts +10 -1
  103. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  104. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  105. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  106. package/src/resources/extensions/gsd/commands.ts +24 -2
  107. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  108. package/src/resources/extensions/gsd/detection.ts +2 -2
  109. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  110. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  111. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  112. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  113. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  114. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  115. package/src/resources/extensions/gsd/doctor.ts +177 -13
  116. package/src/resources/extensions/gsd/export.ts +1 -1
  117. package/src/resources/extensions/gsd/files.ts +2 -2
  118. package/src/resources/extensions/gsd/forensics.ts +1 -1
  119. package/src/resources/extensions/gsd/git-service.ts +13 -1
  120. package/src/resources/extensions/gsd/index.ts +3 -1
  121. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  122. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  123. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  124. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  125. package/src/resources/extensions/gsd/preferences.ts +8 -5
  126. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  128. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  129. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  130. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  131. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  132. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  133. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  134. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  135. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  136. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  137. package/src/resources/extensions/gsd/state.ts +1 -1
  138. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  139. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +16 -37
  140. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  141. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  142. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  143. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  144. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  145. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  146. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  147. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  148. package/src/resources/extensions/gsd/types.ts +0 -1
  149. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  150. package/src/resources/extensions/gsd/worktree.ts +35 -15
  151. package/src/resources/extensions/remote-questions/status.ts +3 -1
  152. package/src/resources/extensions/remote-questions/store.ts +3 -1
  153. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  154. package/src/resources/extensions/subagent/index.ts +12 -3
  155. package/src/resources/extensions/subagent/isolation.ts +3 -1
  156. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  157. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  158. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  159. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  160. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  161. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  162. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  163. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  164. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  165. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  166. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  167. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  168. 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) {
@@ -113,15 +121,25 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
113
121
  const summaryContent = await loadFile(summaryPath);
114
122
  if (summaryContent) {
115
123
  const summary = parseSummary(summaryContent);
124
+ // Look up GitHub issue number for commit linking
125
+ let ghIssueNumber: number | undefined;
126
+ try {
127
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
128
+ ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
129
+ } catch {
130
+ // GitHub sync not available — skip
131
+ }
132
+
116
133
  taskContext = {
117
134
  taskId: `${sid}/${tid}`,
118
135
  taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
119
136
  oneLiner: summary.oneLiner || undefined,
120
137
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
138
+ issueNumber: ghIssueNumber,
121
139
  };
122
140
  }
123
- } catch {
124
- // Non-fatal
141
+ } catch (e) {
142
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
125
143
  }
126
144
  }
127
145
  }
@@ -131,12 +149,20 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
131
149
  if (commitMsg) {
132
150
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
133
151
  }
134
- } catch {
135
- // Non-fatal
152
+ } catch (e) {
153
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
136
154
  }
137
155
 
138
- // Doctor: fix mechanical bookkeeping
156
+ // GitHub sync (non-blocking, opt-in)
139
157
  try {
158
+ const { runGitHubSync } = await import("../github-sync/sync.js");
159
+ await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
160
+ } catch (e) {
161
+ debugLog("postUnit", { phase: "github-sync", error: String(e) });
162
+ }
163
+
164
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
165
+ if (!opts?.skipDoctor) try {
140
166
  const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
141
167
  const doctorScope = scopeParts.join("/");
142
168
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
@@ -168,24 +194,26 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
168
194
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
169
195
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
170
196
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
171
- } catch {
172
- // Non-fatal
197
+ } catch (e) {
198
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
173
199
  }
174
200
  }
175
201
  }
176
- } catch {
177
- // Non-fatal
202
+ } catch (e) {
203
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
178
204
  }
179
205
 
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
206
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
207
+ if (!opts?.skipStateRebuild) {
208
+ const now = Date.now();
209
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
210
+ try {
211
+ await rebuildState(s.basePath);
212
+ s.lastStateRebuildAt = now;
213
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
214
+ } catch (e) {
215
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
216
+ }
189
217
  }
190
218
  }
191
219
 
@@ -193,16 +221,16 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
193
221
  try {
194
222
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
195
223
  pruneDeadProcesses();
196
- } catch {
197
- // Non-fatal
224
+ } catch (e) {
225
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
198
226
  }
199
227
 
200
- // Sync worktree state back to project root
201
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
228
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
229
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
202
230
  try {
203
231
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
204
- } catch {
205
- // Non-fatal
232
+ } catch (e) {
233
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
206
234
  }
207
235
  }
208
236
 
@@ -210,10 +238,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
210
238
  if (s.currentUnit.type === "rewrite-docs") {
211
239
  try {
212
240
  await resolveAllOverrides(s.basePath);
213
- resetRewriteCircuitBreaker();
241
+ s.rewriteAttemptCount = 0;
214
242
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
215
- } catch {
216
- // Non-fatal
243
+ } catch (e) {
244
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
217
245
  }
218
246
  }
219
247
 
@@ -226,8 +254,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
226
254
  const { clearReactiveState } = await import("./reactive-graph.js");
227
255
  clearReactiveState(s.basePath, mid, sid);
228
256
  }
229
- } catch {
230
- // Non-fatal
257
+ } catch (e) {
258
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
231
259
  }
232
260
  }
233
261
 
@@ -280,8 +308,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
280
308
  if (triggerArtifactVerified) {
281
309
  invalidateAllCaches();
282
310
  }
283
- } catch {
284
- // Non-fatal
311
+ } catch (e) {
312
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
285
313
  }
286
314
  } else {
287
315
  // Hook unit completed — finalize its runtime record
@@ -292,8 +320,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
292
320
  lastProgressKind: "hook-completed",
293
321
  });
294
322
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
295
- } catch {
296
- // Non-fatal
323
+ } catch (e) {
324
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
297
325
  }
298
326
  }
299
327
  }
@@ -429,8 +457,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
429
457
  }
430
458
  }
431
459
  }
432
- } catch {
433
- // Triage check failure is non-fatal
460
+ } catch (e) {
461
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
434
462
  }
435
463
  }
436
464
 
@@ -475,8 +503,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
475
503
  );
476
504
 
477
505
  return "continue";
478
- } catch {
479
- // Non-fatal proceed to normal dispatch
506
+ } catch (e) {
507
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
480
508
  }
481
509
  }
482
510
 
@@ -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
  /**