gsd-pi 2.37.1 → 2.38.0-dev.63ad7e5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resources/extensions/browser-tools/package.json +3 -1
  10. package/dist/resources/extensions/cmux/index.js +55 -1
  11. package/dist/resources/extensions/context7/package.json +1 -1
  12. package/dist/resources/extensions/env-utils.js +29 -0
  13. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  14. package/dist/resources/extensions/google-search/package.json +3 -1
  15. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +68 -97
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +143 -96
  24. package/dist/resources/extensions/gsd/captures.js +9 -1
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  27. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  28. package/dist/resources/extensions/gsd/commands.js +22 -2
  29. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  30. package/dist/resources/extensions/gsd/detection.js +1 -2
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  32. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  33. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  34. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  35. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  36. package/dist/resources/extensions/gsd/doctor.js +184 -11
  37. package/dist/resources/extensions/gsd/export.js +1 -1
  38. package/dist/resources/extensions/gsd/files.js +43 -2
  39. package/dist/resources/extensions/gsd/forensics.js +1 -1
  40. package/dist/resources/extensions/gsd/index.js +2 -1
  41. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  42. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  43. package/dist/resources/extensions/gsd/package.json +1 -1
  44. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  45. package/dist/resources/extensions/gsd/preferences-types.js +2 -2
  46. package/dist/resources/extensions/gsd/preferences-validation.js +43 -11
  47. package/dist/resources/extensions/gsd/preferences.js +5 -5
  48. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  49. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  50. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  51. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  52. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  54. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  55. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  56. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  57. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  58. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  59. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  60. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  61. package/dist/resources/extensions/gsd/state.js +1 -1
  62. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  63. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  64. package/dist/resources/extensions/gsd/worktree.js +35 -16
  65. package/dist/resources/extensions/remote-questions/status.js +2 -1
  66. package/dist/resources/extensions/remote-questions/store.js +2 -1
  67. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  68. package/dist/resources/extensions/subagent/index.js +12 -3
  69. package/dist/resources/extensions/subagent/isolation.js +2 -1
  70. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  71. package/dist/resources/extensions/universal-config/package.json +1 -1
  72. package/dist/welcome-screen.d.ts +12 -0
  73. package/dist/welcome-screen.js +53 -0
  74. package/package.json +2 -1
  75. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  76. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  78. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/models.generated.js +172 -0
  80. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  82. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  83. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  84. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  85. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  86. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  88. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  89. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  90. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  91. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  92. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  93. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  95. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  96. package/packages/pi-ai/dist/types.d.ts +2 -2
  97. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/types.js.map +1 -1
  99. package/packages/pi-ai/package.json +1 -0
  100. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  101. package/packages/pi-ai/src/models.generated.ts +172 -0
  102. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  103. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  104. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  105. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  106. package/packages/pi-ai/src/types.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  109. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  112. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  115. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  116. package/pkg/package.json +1 -1
  117. package/src/resources/extensions/cmux/index.ts +57 -1
  118. package/src/resources/extensions/env-utils.ts +31 -0
  119. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  120. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  121. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  122. package/src/resources/extensions/gsd/auto-loop.ts +88 -133
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  124. package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
  125. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  126. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  127. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  128. package/src/resources/extensions/gsd/auto.ts +139 -101
  129. package/src/resources/extensions/gsd/captures.ts +10 -1
  130. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  131. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  132. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  133. package/src/resources/extensions/gsd/commands.ts +24 -2
  134. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  135. package/src/resources/extensions/gsd/detection.ts +2 -2
  136. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  137. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  138. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  139. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  140. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  141. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  142. package/src/resources/extensions/gsd/doctor.ts +177 -13
  143. package/src/resources/extensions/gsd/export.ts +1 -1
  144. package/src/resources/extensions/gsd/files.ts +47 -2
  145. package/src/resources/extensions/gsd/forensics.ts +1 -1
  146. package/src/resources/extensions/gsd/index.ts +3 -1
  147. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  148. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  149. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  150. package/src/resources/extensions/gsd/preferences-types.ts +5 -5
  151. package/src/resources/extensions/gsd/preferences-validation.ts +42 -11
  152. package/src/resources/extensions/gsd/preferences.ts +5 -5
  153. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  154. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  156. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  157. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  158. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  159. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  160. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  161. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  162. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  163. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  164. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  165. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  166. package/src/resources/extensions/gsd/state.ts +1 -1
  167. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  168. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  169. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
  170. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  171. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  172. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  173. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  174. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  175. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  176. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  177. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  178. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  179. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  180. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  181. package/src/resources/extensions/gsd/types.ts +43 -1
  182. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  183. package/src/resources/extensions/gsd/worktree.ts +35 -15
  184. package/src/resources/extensions/remote-questions/status.ts +3 -1
  185. package/src/resources/extensions/remote-questions/store.ts +3 -1
  186. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  187. package/src/resources/extensions/subagent/index.ts +12 -3
  188. package/src/resources/extensions/subagent/isolation.ts +3 -1
  189. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  190. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  191. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  192. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  193. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  194. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  195. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  196. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  197. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  198. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  199. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  200. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  201. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -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
 
13
13
  import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
@@ -18,7 +18,7 @@ import type { GSDPreferences } from "./preferences.js";
18
18
  import type { SessionLockStatus } from "./session-lock.js";
19
19
  import type { GSDState } from "./types.js";
20
20
  import type { CloseoutOptions } from "./auto-unit-closeout.js";
21
- import type { PostUnitContext } from "./auto-post-unit.js";
21
+ import type { PostUnitContext, PreVerificationOpts } from "./auto-post-unit.js";
22
22
  import type {
23
23
  VerificationContext,
24
24
  VerificationResult,
@@ -36,6 +36,19 @@ import type { CmuxLogLevel } from "../cmux/index.js";
36
36
  */
37
37
  const MAX_LOOP_ITERATIONS = 500;
38
38
 
39
+ /** Data-driven budget threshold notifications (75/80/90%). The 100% case is
40
+ * handled inline because it requires break/pause/stop control flow. */
41
+ const BUDGET_THRESHOLDS: Array<{
42
+ pct: number;
43
+ label: string;
44
+ notifyLevel: "info" | "warning";
45
+ cmuxLevel: "progress" | "warning";
46
+ }> = [
47
+ { pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
48
+ { pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
49
+ { pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
50
+ ];
51
+
39
52
  // ─── Types ───────────────────────────────────────────────────────────────────
40
53
 
41
54
  /**
@@ -54,17 +67,15 @@ export interface UnitResult {
54
67
  event?: AgentEndEvent;
55
68
  }
56
69
 
57
- // ─── Session-scoped promise state ───────────────────────────────────────────
70
+ // ─── Per-unit one-shot promise state ────────────────────────────────────────
58
71
  //
59
- // pendingResolve and pendingAgentEndQueue live on AutoSession (not module-level)
60
- // so concurrent sessions cannot corrupt each other's promises.
72
+ // A single module-level resolve function scoped to the current unit execution.
73
+ // No queue if an agent_end arrives with no pending resolver, it is dropped
74
+ // (logged as warning). This is simpler and safer than the previous session-
75
+ // scoped pendingResolve + pendingAgentEndQueue pattern.
61
76
 
62
- /**
63
- * The singleton session reference used by resolveAgentEnd. Set by autoLoop
64
- * on entry so that the agent_end handler in index.ts can resolve the correct
65
- * session's promise without needing a direct reference to `s`.
66
- */
67
- let _activeSession: AutoSession | null = null;
77
+ let _currentResolve: ((result: UnitResult) => void) | null = null;
78
+ let _sessionSwitchInFlight = false;
68
79
 
69
80
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
70
81
 
@@ -73,60 +84,48 @@ let _activeSession: AutoSession | null = null;
73
84
  * in-flight unit promise. One-shot: the resolver is nulled before calling
74
85
  * to prevent double-resolution from model fallback retries.
75
86
  *
76
- * If no pendingResolve exists (event arrived between loop iterations),
77
- * the event is queued on the session so the next runUnit can drain it.
87
+ * If no resolver exists (event arrived between loop iterations or during
88
+ * session switch), the event is dropped with a debug warning.
78
89
  */
79
90
  export function resolveAgentEnd(event: AgentEndEvent): void {
80
- const s = _activeSession;
81
- if (!s) {
82
- debugLog("resolveAgentEnd", {
83
- status: "no-active-session",
84
- warning: "agent_end with no active loop session",
85
- });
91
+ if (_sessionSwitchInFlight) {
92
+ debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
86
93
  return;
87
94
  }
88
-
89
- if (s.pendingResolve) {
95
+ if (_currentResolve) {
90
96
  debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
91
- const r = s.pendingResolve;
92
- s.pendingResolve = null;
97
+ const r = _currentResolve;
98
+ _currentResolve = null;
93
99
  r({ status: "completed", event });
94
100
  } else {
95
- // Queue the event so the next runUnit picks it up immediately
96
101
  debugLog("resolveAgentEnd", {
97
- status: "queued",
98
- queueLength: s.pendingAgentEndQueue.length + 1,
99
- warning:
100
- "agent_end arrived between loop iterations — queued for next runUnit",
102
+ status: "no-pending-resolve",
103
+ warning: "agent_end with no pending unit",
101
104
  });
102
- s.pendingAgentEndQueue.push(event);
103
105
  }
104
106
  }
105
107
 
106
108
  export function isSessionSwitchInFlight(): boolean {
107
- return _activeSession?.sessionSwitchInFlight ?? false;
109
+ return _sessionSwitchInFlight;
108
110
  }
109
111
 
110
112
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
111
113
 
112
114
  /**
113
- * Reset session promise state. Only exported for test cleanup — production code
114
- * should never call this.
115
+ * Reset module-level promise state. Only exported for test cleanup —
116
+ * production code should never call this.
115
117
  */
116
118
  export function _resetPendingResolve(): void {
117
- if (_activeSession) {
118
- _activeSession.pendingResolve = null;
119
- _activeSession.pendingAgentEndQueue = [];
120
- }
121
- _activeSession = null;
119
+ _currentResolve = null;
120
+ _sessionSwitchInFlight = false;
122
121
  }
123
122
 
124
123
  /**
125
- * Set the active session for resolveAgentEnd. Only exported for test setup —
126
- * production code sets this via autoLoop entry.
124
+ * No-op for backward compatibility with tests that previously set the
125
+ * active session. The module no longer holds a session reference.
127
126
  */
128
- export function _setActiveSession(session: AutoSession | null): void {
129
- _activeSession = session;
127
+ export function _setActiveSession(_session: AutoSession | null): void {
128
+ // No-op — kept for test backward compatibility
130
129
  }
131
130
 
132
131
  // ─── runUnit ─────────────────────────────────────────────────────────────────
@@ -150,41 +149,15 @@ export async function runUnit(
150
149
  ): Promise<UnitResult> {
151
150
  debugLog("runUnit", { phase: "start", unitType, unitId });
152
151
 
153
- // ── Drain queued events from error-recovery retries ──
154
- // If an agent_end arrived between iterations (e.g. from a model fallback
155
- // sendMessage retry), consume it immediately instead of creating a new promise.
156
- // Cap queue to 3 entries to prevent unbounded growth from stale events.
157
- if (s.pendingAgentEndQueue.length > 3) {
158
- debugLog("runUnit", {
159
- phase: "queue-overflow",
160
- dropped: s.pendingAgentEndQueue.length - 1,
161
- unitType,
162
- unitId,
163
- });
164
- s.pendingAgentEndQueue = [
165
- s.pendingAgentEndQueue[s.pendingAgentEndQueue.length - 1]!,
166
- ];
167
- }
168
- if (s.pendingAgentEndQueue.length > 0) {
169
- const queued = s.pendingAgentEndQueue.shift()!;
170
- debugLog("runUnit", {
171
- phase: "drained-queued-event",
172
- unitType,
173
- unitId,
174
- queueRemaining: s.pendingAgentEndQueue.length,
175
- });
176
- return { status: "completed", event: queued };
177
- }
178
-
179
152
  // ── Session creation with timeout ──
180
153
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
181
154
 
182
155
  let sessionResult: { cancelled: boolean };
183
156
  let sessionTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
184
- s.sessionSwitchInFlight = true;
157
+ _sessionSwitchInFlight = true;
185
158
  try {
186
159
  const sessionPromise = s.cmdCtx!.newSession().finally(() => {
187
- s.sessionSwitchInFlight = false;
160
+ _sessionSwitchInFlight = false;
188
161
  });
189
162
  const timeoutPromise = new Promise<{ cancelled: true }>((resolve) => {
190
163
  sessionTimeoutHandle = setTimeout(
@@ -216,11 +189,12 @@ export async function runUnit(
216
189
  return { status: "cancelled" };
217
190
  }
218
191
 
219
- // ── Create the agent_end promise (session-scoped) ──
192
+ // ── Create the agent_end promise (per-unit one-shot) ──
220
193
  // This happens after newSession completes so session-switch agent_end events
221
194
  // from the previous session cannot resolve the new unit.
195
+ _sessionSwitchInFlight = false;
222
196
  const unitPromise = new Promise<UnitResult>((resolve) => {
223
- s.pendingResolve = resolve;
197
+ _currentResolve = resolve;
224
198
  });
225
199
 
226
200
  // Ensure cwd matches basePath before dispatch (#1389).
@@ -383,6 +357,7 @@ export interface LoopDeps {
383
357
  midTitle: string;
384
358
  state: GSDState;
385
359
  prefs: GSDPreferences | undefined;
360
+ session?: AutoSession;
386
361
  }) => Promise<DispatchAction>;
387
362
  runPreDispatchHooks: (
388
363
  unitType: string,
@@ -500,6 +475,7 @@ export interface LoopDeps {
500
475
  // Post-unit processing
501
476
  postUnitPreVerification: (
502
477
  pctx: PostUnitContext,
478
+ opts?: PreVerificationOpts,
503
479
  ) => Promise<"dispatched" | "continue">;
504
480
  runPostUnitVerification: (
505
481
  vctx: VerificationContext,
@@ -530,7 +506,6 @@ export async function autoLoop(
530
506
  deps: LoopDeps,
531
507
  ): Promise<void> {
532
508
  debugLog("autoLoop", { phase: "enter" });
533
- _activeSession = s;
534
509
  let iteration = 0;
535
510
  let lastDerivedUnit = "";
536
511
  let sameUnitCount = 0;
@@ -787,7 +762,7 @@ export async function autoLoop(
787
762
  (m: { status: string }) =>
788
763
  m.status !== "complete" && m.status !== "parked",
789
764
  );
790
- if (incomplete.length === 0) {
765
+ if (incomplete.length === 0 && state.registry.length > 0) {
791
766
  // All milestones complete — merge milestone branch before stopping
792
767
  if (s.currentMilestoneId) {
793
768
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -804,6 +779,18 @@ export async function autoLoop(
804
779
  "success",
805
780
  );
806
781
  await deps.stopAuto(ctx, pi, "All milestones complete");
782
+ } else if (incomplete.length === 0 && state.registry.length === 0) {
783
+ // Empty registry — no milestones visible, likely a path resolution bug
784
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
785
+ ctx.ui.notify(
786
+ `No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
787
+ "error",
788
+ );
789
+ await deps.stopAuto(
790
+ ctx,
791
+ pi,
792
+ `No milestones found — check basePath resolution`,
793
+ );
807
794
  } else if (state.phase === "blocked") {
808
795
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
809
796
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -965,62 +952,26 @@ export async function autoLoop(
965
952
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
966
953
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
967
954
  deps.logCmuxEvent(prefs, msg, "warning");
968
- } else if (newBudgetAlertLevel === 90) {
969
- s.lastBudgetAlertLevel =
970
- newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
971
- ctx.ui.notify(
972
- `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
973
- "warning",
974
- );
975
- deps.sendDesktopNotification(
976
- "GSD",
977
- `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
978
- "warning",
979
- "budget",
980
- );
981
- deps.logCmuxEvent(
982
- prefs,
983
- `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
984
- "warning",
985
- );
986
- } else if (newBudgetAlertLevel === 80) {
987
- s.lastBudgetAlertLevel =
988
- newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
989
- ctx.ui.notify(
990
- `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
991
- "warning",
992
- );
993
- deps.sendDesktopNotification(
994
- "GSD",
995
- `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
996
- "warning",
997
- "budget",
998
- );
999
- deps.logCmuxEvent(
1000
- prefs,
1001
- `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1002
- "warning",
1003
- );
1004
- } else if (newBudgetAlertLevel === 75) {
1005
- s.lastBudgetAlertLevel =
1006
- newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
1007
- ctx.ui.notify(
1008
- `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1009
- "info",
1010
- );
1011
- deps.sendDesktopNotification(
1012
- "GSD",
1013
- `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1014
- "info",
1015
- "budget",
1016
- );
1017
- deps.logCmuxEvent(
1018
- prefs,
1019
- `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1020
- "progress",
955
+ } else {
956
+ // Data-driven 75/80/90% threshold notifications
957
+ const threshold = BUDGET_THRESHOLDS.find(
958
+ (t) => newBudgetAlertLevel === t.pct,
1021
959
  );
1022
- } else if (budgetAlertLevel === 0) {
1023
- s.lastBudgetAlertLevel = 0;
960
+ if (threshold) {
961
+ s.lastBudgetAlertLevel =
962
+ newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
963
+ const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
964
+ ctx.ui.notify(msg, threshold.notifyLevel);
965
+ deps.sendDesktopNotification(
966
+ "GSD",
967
+ msg,
968
+ threshold.notifyLevel,
969
+ "budget",
970
+ );
971
+ deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
972
+ } else if (budgetAlertLevel === 0) {
973
+ s.lastBudgetAlertLevel = 0;
974
+ }
1024
975
  }
1025
976
  } else {
1026
977
  s.lastBudgetAlertLevel = 0;
@@ -1091,6 +1042,7 @@ export async function autoLoop(
1091
1042
  midTitle: midTitle!,
1092
1043
  state,
1093
1044
  prefs,
1045
+ session: s,
1094
1046
  });
1095
1047
 
1096
1048
  if (dispatchResult.action === "stop") {
@@ -1649,9 +1601,12 @@ export async function autoLoop(
1649
1601
  break;
1650
1602
  }
1651
1603
 
1652
- // Run pre-verification for the sidecar unit
1604
+ // Run pre-verification for the sidecar unit (lightweight path)
1605
+ const sidecarPreOpts: PreVerificationOpts = item.kind === "hook"
1606
+ ? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
1607
+ : { skipSettleDelay: true, skipStateRebuild: true };
1653
1608
  const sidecarPreResult =
1654
- await deps.postUnitPreVerification(postUnitCtx);
1609
+ await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
1655
1610
  if (sidecarPreResult === "dispatched") {
1656
1611
  // Pre-verification caused stop/pause
1657
1612
  debugLog("autoLoop", {
@@ -1740,6 +1695,6 @@ export async function autoLoop(
1740
1695
  }
1741
1696
  }
1742
1697
 
1743
- _activeSession = null;
1698
+ _currentResolve = null;
1744
1699
  debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
1745
1700
  }
@@ -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,24 @@ 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) });
227
+ }
228
+ }
229
+
230
+ // Reactive state cleanup on slice completion
231
+ if (s.currentUnit.type === "complete-slice") {
232
+ try {
233
+ const parts = s.currentUnit.id.split("/");
234
+ const [mid, sid] = parts;
235
+ if (mid && sid) {
236
+ const { clearReactiveState } = await import("./reactive-graph.js");
237
+ clearReactiveState(s.basePath, mid, sid);
238
+ }
239
+ } catch (e) {
240
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
217
241
  }
218
242
  }
219
243
 
@@ -266,8 +290,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
266
290
  if (triggerArtifactVerified) {
267
291
  invalidateAllCaches();
268
292
  }
269
- } catch {
270
- // Non-fatal
293
+ } catch (e) {
294
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
271
295
  }
272
296
  } else {
273
297
  // Hook unit completed — finalize its runtime record
@@ -278,8 +302,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
278
302
  lastProgressKind: "hook-completed",
279
303
  });
280
304
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
281
- } catch {
282
- // Non-fatal
305
+ } catch (e) {
306
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
283
307
  }
284
308
  }
285
309
  }
@@ -415,8 +439,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
415
439
  }
416
440
  }
417
441
  }
418
- } catch {
419
- // Triage check failure is non-fatal
442
+ } catch (e) {
443
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
420
444
  }
421
445
  }
422
446
 
@@ -461,8 +485,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
461
485
  );
462
486
 
463
487
  return "continue";
464
- } catch {
465
- // Non-fatal proceed to normal dispatch
488
+ } catch (e) {
489
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
466
490
  }
467
491
  }
468
492