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
@@ -5,9 +5,9 @@
5
5
  * pattern with a while loop. The agent_end event resolves a promise instead
6
6
  * of recursing.
7
7
  *
8
- * MAINTENANCE RULE: The only module-level mutable state here is `_activeSession`,
9
- * used by the agent_end bridge. Promise state itself lives on AutoSession so
10
- * concurrent auto sessions cannot corrupt each other.
8
+ * MAINTENANCE RULE: Module-level mutable state is limited to `_currentResolve`
9
+ * (per-unit one-shot resolver) and `_sessionSwitchInFlight` (guard for
10
+ * session rotation). No queue stale agent_end events are dropped.
11
11
  */
12
12
  import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
13
13
  import { debugLog } from "./debug-logger.js";
@@ -18,71 +18,66 @@ import { debugLog } from "./debug-logger.js";
18
18
  * generous headroom including retries and sidecar work.
19
19
  */
20
20
  const MAX_LOOP_ITERATIONS = 500;
21
- // ─── Session-scoped promise state ───────────────────────────────────────────
21
+ /** Data-driven budget threshold notifications (75/80/90%). The 100% case is
22
+ * handled inline because it requires break/pause/stop control flow. */
23
+ const BUDGET_THRESHOLDS = [
24
+ { pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
25
+ { pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
26
+ { pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
27
+ ];
28
+ // ─── Per-unit one-shot promise state ────────────────────────────────────────
22
29
  //
23
- // pendingResolve and pendingAgentEndQueue live on AutoSession (not module-level)
24
- // so concurrent sessions cannot corrupt each other's promises.
25
- /**
26
- * The singleton session reference used by resolveAgentEnd. Set by autoLoop
27
- * on entry so that the agent_end handler in index.ts can resolve the correct
28
- * session's promise without needing a direct reference to `s`.
29
- */
30
- let _activeSession = null;
30
+ // A single module-level resolve function scoped to the current unit execution.
31
+ // No queue if an agent_end arrives with no pending resolver, it is dropped
32
+ // (logged as warning). This is simpler and safer than the previous session-
33
+ // scoped pendingResolve + pendingAgentEndQueue pattern.
34
+ let _currentResolve = null;
35
+ let _sessionSwitchInFlight = false;
31
36
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
32
37
  /**
33
38
  * Called from the agent_end event handler in index.ts to resolve the
34
39
  * in-flight unit promise. One-shot: the resolver is nulled before calling
35
40
  * to prevent double-resolution from model fallback retries.
36
41
  *
37
- * If no pendingResolve exists (event arrived between loop iterations),
38
- * the event is queued on the session so the next runUnit can drain it.
42
+ * If no resolver exists (event arrived between loop iterations or during
43
+ * session switch), the event is dropped with a debug warning.
39
44
  */
40
45
  export function resolveAgentEnd(event) {
41
- const s = _activeSession;
42
- if (!s) {
43
- debugLog("resolveAgentEnd", {
44
- status: "no-active-session",
45
- warning: "agent_end with no active loop session",
46
- });
46
+ if (_sessionSwitchInFlight) {
47
+ debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
47
48
  return;
48
49
  }
49
- if (s.pendingResolve) {
50
+ if (_currentResolve) {
50
51
  debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
51
- const r = s.pendingResolve;
52
- s.pendingResolve = null;
52
+ const r = _currentResolve;
53
+ _currentResolve = null;
53
54
  r({ status: "completed", event });
54
55
  }
55
56
  else {
56
- // Queue the event so the next runUnit picks it up immediately
57
57
  debugLog("resolveAgentEnd", {
58
- status: "queued",
59
- queueLength: s.pendingAgentEndQueue.length + 1,
60
- warning: "agent_end arrived between loop iterations — queued for next runUnit",
58
+ status: "no-pending-resolve",
59
+ warning: "agent_end with no pending unit",
61
60
  });
62
- s.pendingAgentEndQueue.push(event);
63
61
  }
64
62
  }
65
63
  export function isSessionSwitchInFlight() {
66
- return _activeSession?.sessionSwitchInFlight ?? false;
64
+ return _sessionSwitchInFlight;
67
65
  }
68
66
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
69
67
  /**
70
- * Reset session promise state. Only exported for test cleanup — production code
71
- * should never call this.
68
+ * Reset module-level promise state. Only exported for test cleanup —
69
+ * production code should never call this.
72
70
  */
73
71
  export function _resetPendingResolve() {
74
- if (_activeSession) {
75
- _activeSession.pendingResolve = null;
76
- _activeSession.pendingAgentEndQueue = [];
77
- }
78
- _activeSession = null;
72
+ _currentResolve = null;
73
+ _sessionSwitchInFlight = false;
79
74
  }
80
75
  /**
81
- * Set the active session for resolveAgentEnd. Only exported for test setup —
82
- * production code sets this via autoLoop entry.
76
+ * No-op for backward compatibility with tests that previously set the
77
+ * active session. The module no longer holds a session reference.
83
78
  */
84
- export function _setActiveSession(session) {
85
- _activeSession = session;
79
+ export function _setActiveSession(_session) {
80
+ // No-op — kept for test backward compatibility
86
81
  }
87
82
  // ─── runUnit ─────────────────────────────────────────────────────────────────
88
83
  /**
@@ -95,39 +90,14 @@ export function _setActiveSession(session) {
95
90
  */
96
91
  export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
97
92
  debugLog("runUnit", { phase: "start", unitType, unitId });
98
- // ── Drain queued events from error-recovery retries ──
99
- // If an agent_end arrived between iterations (e.g. from a model fallback
100
- // sendMessage retry), consume it immediately instead of creating a new promise.
101
- // Cap queue to 3 entries to prevent unbounded growth from stale events.
102
- if (s.pendingAgentEndQueue.length > 3) {
103
- debugLog("runUnit", {
104
- phase: "queue-overflow",
105
- dropped: s.pendingAgentEndQueue.length - 1,
106
- unitType,
107
- unitId,
108
- });
109
- s.pendingAgentEndQueue = [
110
- s.pendingAgentEndQueue[s.pendingAgentEndQueue.length - 1],
111
- ];
112
- }
113
- if (s.pendingAgentEndQueue.length > 0) {
114
- const queued = s.pendingAgentEndQueue.shift();
115
- debugLog("runUnit", {
116
- phase: "drained-queued-event",
117
- unitType,
118
- unitId,
119
- queueRemaining: s.pendingAgentEndQueue.length,
120
- });
121
- return { status: "completed", event: queued };
122
- }
123
93
  // ── Session creation with timeout ──
124
94
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
125
95
  let sessionResult;
126
96
  let sessionTimeoutHandle;
127
- s.sessionSwitchInFlight = true;
97
+ _sessionSwitchInFlight = true;
128
98
  try {
129
99
  const sessionPromise = s.cmdCtx.newSession().finally(() => {
130
- s.sessionSwitchInFlight = false;
100
+ _sessionSwitchInFlight = false;
131
101
  });
132
102
  const timeoutPromise = new Promise((resolve) => {
133
103
  sessionTimeoutHandle = setTimeout(() => resolve({ cancelled: true }), NEW_SESSION_TIMEOUT_MS);
@@ -155,11 +125,12 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
155
125
  if (!s.active) {
156
126
  return { status: "cancelled" };
157
127
  }
158
- // ── Create the agent_end promise (session-scoped) ──
128
+ // ── Create the agent_end promise (per-unit one-shot) ──
159
129
  // This happens after newSession completes so session-switch agent_end events
160
130
  // from the previous session cannot resolve the new unit.
131
+ _sessionSwitchInFlight = false;
161
132
  const unitPromise = new Promise((resolve) => {
162
- s.pendingResolve = resolve;
133
+ _currentResolve = resolve;
163
134
  });
164
135
  // Ensure cwd matches basePath before dispatch (#1389).
165
136
  // async_bash and background jobs can drift cwd away from the worktree.
@@ -195,7 +166,6 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
195
166
  */
196
167
  export async function autoLoop(ctx, pi, s, deps) {
197
168
  debugLog("autoLoop", { phase: "enter" });
198
- _activeSession = s;
199
169
  let iteration = 0;
200
170
  let lastDerivedUnit = "";
201
171
  let sameUnitCount = 0;
@@ -373,7 +343,7 @@ export async function autoLoop(ctx, pi, s, deps) {
373
343
  await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
374
344
  }
375
345
  const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
376
- if (incomplete.length === 0) {
346
+ if (incomplete.length === 0 && state.registry.length > 0) {
377
347
  // All milestones complete — merge milestone branch before stopping
378
348
  if (s.currentMilestoneId) {
379
349
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -382,6 +352,12 @@ export async function autoLoop(ctx, pi, s, deps) {
382
352
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
383
353
  await deps.stopAuto(ctx, pi, "All milestones complete");
384
354
  }
355
+ else if (incomplete.length === 0 && state.registry.length === 0) {
356
+ // Empty registry — no milestones visible, likely a path resolution bug
357
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
358
+ ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
359
+ await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
360
+ }
385
361
  else if (state.phase === "blocked") {
386
362
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
387
363
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -487,29 +463,20 @@ export async function autoLoop(ctx, pi, s, deps) {
487
463
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
488
464
  deps.logCmuxEvent(prefs, msg, "warning");
489
465
  }
490
- else if (newBudgetAlertLevel === 90) {
491
- s.lastBudgetAlertLevel =
492
- newBudgetAlertLevel;
493
- ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
494
- deps.sendDesktopNotification("GSD", `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
495
- deps.logCmuxEvent(prefs, `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
496
- }
497
- else if (newBudgetAlertLevel === 80) {
498
- s.lastBudgetAlertLevel =
499
- newBudgetAlertLevel;
500
- ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
501
- deps.sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
502
- deps.logCmuxEvent(prefs, `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
503
- }
504
- else if (newBudgetAlertLevel === 75) {
505
- s.lastBudgetAlertLevel =
506
- newBudgetAlertLevel;
507
- ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
508
- deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
509
- deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
510
- }
511
- else if (budgetAlertLevel === 0) {
512
- s.lastBudgetAlertLevel = 0;
466
+ else {
467
+ // Data-driven 75/80/90% threshold notifications
468
+ const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel === t.pct);
469
+ if (threshold) {
470
+ s.lastBudgetAlertLevel =
471
+ newBudgetAlertLevel;
472
+ const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
473
+ ctx.ui.notify(msg, threshold.notifyLevel);
474
+ deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
475
+ deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
476
+ }
477
+ else if (budgetAlertLevel === 0) {
478
+ s.lastBudgetAlertLevel = 0;
479
+ }
513
480
  }
514
481
  }
515
482
  else {
@@ -557,6 +524,7 @@ export async function autoLoop(ctx, pi, s, deps) {
557
524
  midTitle: midTitle,
558
525
  state,
559
526
  prefs,
527
+ session: s,
560
528
  });
561
529
  if (dispatchResult.action === "stop") {
562
530
  if (s.currentUnit) {
@@ -916,8 +884,11 @@ export async function autoLoop(ctx, pi, s, deps) {
916
884
  sidecarBroke = true;
917
885
  break;
918
886
  }
919
- // Run pre-verification for the sidecar unit
920
- const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx);
887
+ // Run pre-verification for the sidecar unit (lightweight path)
888
+ const sidecarPreOpts = item.kind === "hook"
889
+ ? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
890
+ : { skipSettleDelay: true, skipStateRebuild: true };
891
+ const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
921
892
  if (sidecarPreResult === "dispatched") {
922
893
  // Pre-verification caused stop/pause
923
894
  debugLog("autoLoop", {
@@ -990,6 +961,6 @@ export async function autoLoop(ctx, pi, s, deps) {
990
961
  }
991
962
  }
992
963
  }
993
- _activeSession = null;
964
+ _currentResolve = null;
994
965
  debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
995
966
  }
@@ -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 {
@@ -79,8 +80,8 @@ export async function postUnitPreVerification(pctx) {
79
80
  };
80
81
  }
81
82
  }
82
- catch {
83
- // Non-fatal
83
+ catch (e) {
84
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
84
85
  }
85
86
  }
86
87
  }
@@ -90,57 +91,60 @@ export async function postUnitPreVerification(pctx) {
90
91
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
91
92
  }
92
93
  }
93
- catch {
94
- // Non-fatal
94
+ catch (e) {
95
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
95
96
  }
96
- // Doctor: fix mechanical bookkeeping
97
- 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
97
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
98
+ if (!opts?.skipDoctor)
99
+ try {
100
+ const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
101
+ const doctorScope = scopeParts.join("/");
102
+ const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
103
+ const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" : "task";
104
+ const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
105
+ if (report.fixesApplied.length > 0) {
106
+ ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
107
+ }
108
+ // Proactive health tracking
109
+ const summary = summarizeDoctorIssues(report.issues);
110
+ recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
111
+ // Check if we should escalate to LLM-assisted heal
112
+ if (summary.errors > 0) {
113
+ const unresolvedErrors = report.issues
114
+ .filter(i => i.severity === "error" && !i.fixable)
115
+ .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
116
+ const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
117
+ if (escalation.shouldEscalate) {
118
+ ctx.ui.notify(`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`, "warning");
119
+ try {
120
+ const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
121
+ const { dispatchDoctorHeal } = await import("./commands-handlers.js");
122
+ const actionable = report.issues.filter(i => i.severity === "error");
123
+ const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
124
+ const structuredIssues = formatDoctorIssuesForPrompt(actionable);
125
+ dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
126
+ }
127
+ catch (e) {
128
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
129
+ }
127
130
  }
128
131
  }
129
132
  }
130
- }
131
- catch {
132
- // Non-fatal
133
- }
134
- // Throttled STATE.md rebuild
135
- const now = Date.now();
136
- if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
137
- try {
138
- await rebuildState(s.basePath);
139
- s.lastStateRebuildAt = now;
140
- autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
133
+ catch (e) {
134
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
141
135
  }
142
- catch {
143
- // Non-fatal
136
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
137
+ if (!opts?.skipStateRebuild) {
138
+ const now = Date.now();
139
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
140
+ try {
141
+ await rebuildState(s.basePath);
142
+ s.lastStateRebuildAt = now;
143
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
144
+ }
145
+ catch (e) {
146
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
147
+ }
144
148
  }
145
149
  }
146
150
  // Prune dead bg-shell processes
@@ -148,27 +152,27 @@ export async function postUnitPreVerification(pctx) {
148
152
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
149
153
  pruneDeadProcesses();
150
154
  }
151
- catch {
152
- // Non-fatal
155
+ catch (e) {
156
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
153
157
  }
154
- // Sync worktree state back to project root
155
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
158
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
159
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
156
160
  try {
157
161
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
158
162
  }
159
- catch {
160
- // Non-fatal
163
+ catch (e) {
164
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
161
165
  }
162
166
  }
163
167
  // Rewrite-docs completion
164
168
  if (s.currentUnit.type === "rewrite-docs") {
165
169
  try {
166
170
  await resolveAllOverrides(s.basePath);
167
- resetRewriteCircuitBreaker();
171
+ s.rewriteAttemptCount = 0;
168
172
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
169
173
  }
170
- catch {
171
- // Non-fatal
174
+ catch (e) {
175
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
172
176
  }
173
177
  }
174
178
  // Reactive state cleanup on slice completion
@@ -181,8 +185,8 @@ export async function postUnitPreVerification(pctx) {
181
185
  clearReactiveState(s.basePath, mid, sid);
182
186
  }
183
187
  }
184
- catch {
185
- // Non-fatal
188
+ catch (e) {
189
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
186
190
  }
187
191
  }
188
192
  // Post-triage: execute actionable resolutions
@@ -224,8 +228,8 @@ export async function postUnitPreVerification(pctx) {
224
228
  invalidateAllCaches();
225
229
  }
226
230
  }
227
- catch {
228
- // Non-fatal
231
+ catch (e) {
232
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
229
233
  }
230
234
  }
231
235
  else {
@@ -238,8 +242,8 @@ export async function postUnitPreVerification(pctx) {
238
242
  });
239
243
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
240
244
  }
241
- catch {
242
- // Non-fatal
245
+ catch (e) {
246
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
243
247
  }
244
248
  }
245
249
  }
@@ -352,8 +356,8 @@ export async function postUnitPostVerification(pctx) {
352
356
  }
353
357
  }
354
358
  }
355
- catch {
356
- // Triage check failure is non-fatal
359
+ catch (e) {
360
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
357
361
  }
358
362
  }
359
363
  // ── Quick-task dispatch ──
@@ -387,8 +391,8 @@ export async function postUnitPostVerification(pctx) {
387
391
  ctx.ui.notify(`Executing quick-task: ${capture.id} — "${capture.text}"`, "info");
388
392
  return "continue";
389
393
  }
390
- catch {
391
- // Non-fatal proceed to normal dispatch
394
+ catch (e) {
395
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
392
396
  }
393
397
  }
394
398
  // 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
  }
@@ -777,15 +756,12 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
777
756
  const contextWindow = resolveExecutorContextWindow(undefined, prefs?.preferences);
778
757
  const budgets = computeBudgets(contextWindow);
779
758
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
780
- // Compress carry-forward section when it exceeds 40% of inline context budget.
781
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
759
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
782
760
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
783
761
  let finalCarryForward = carryForwardSection;
784
762
  if (carryForwardSection.length > carryForwardBudget) {
785
- const { resolveCompressionStrategy } = await import("./preferences.js");
786
- if (resolveCompressionStrategy() === "compress") {
787
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
788
- }
763
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
764
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
789
765
  }
790
766
  return loadPrompt("execute-task", {
791
767
  overridesSection,
@@ -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() &&