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
@@ -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,21 @@ import type { CmuxLogLevel } from "../cmux/index.js";
36
36
  */
37
37
  const MAX_LOOP_ITERATIONS = 500;
38
38
 
39
+ /** Data-driven budget threshold notifications (descending). The 100% entry
40
+ * triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
41
+ * a simple notification. */
42
+ const BUDGET_THRESHOLDS: Array<{
43
+ pct: number;
44
+ label: string;
45
+ notifyLevel: "info" | "warning" | "error";
46
+ cmuxLevel: "progress" | "warning" | "error";
47
+ }> = [
48
+ { pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
49
+ { pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
50
+ { pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
51
+ { pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
52
+ ];
53
+
39
54
  // ─── Types ───────────────────────────────────────────────────────────────────
40
55
 
41
56
  /**
@@ -54,17 +69,15 @@ export interface UnitResult {
54
69
  event?: AgentEndEvent;
55
70
  }
56
71
 
57
- // ─── Session-scoped promise state ───────────────────────────────────────────
72
+ // ─── Per-unit one-shot promise state ────────────────────────────────────────
58
73
  //
59
- // pendingResolve and pendingAgentEndQueue live on AutoSession (not module-level)
60
- // so concurrent sessions cannot corrupt each other's promises.
74
+ // A single module-level resolve function scoped to the current unit execution.
75
+ // No queue if an agent_end arrives with no pending resolver, it is dropped
76
+ // (logged as warning). This is simpler and safer than the previous session-
77
+ // scoped pendingResolve + pendingAgentEndQueue pattern.
61
78
 
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;
79
+ let _currentResolve: ((result: UnitResult) => void) | null = null;
80
+ let _sessionSwitchInFlight = false;
68
81
 
69
82
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
70
83
 
@@ -73,60 +86,48 @@ let _activeSession: AutoSession | null = null;
73
86
  * in-flight unit promise. One-shot: the resolver is nulled before calling
74
87
  * to prevent double-resolution from model fallback retries.
75
88
  *
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.
89
+ * If no resolver exists (event arrived between loop iterations or during
90
+ * session switch), the event is dropped with a debug warning.
78
91
  */
79
92
  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
- });
93
+ if (_sessionSwitchInFlight) {
94
+ debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
86
95
  return;
87
96
  }
88
-
89
- if (s.pendingResolve) {
97
+ if (_currentResolve) {
90
98
  debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
91
- const r = s.pendingResolve;
92
- s.pendingResolve = null;
99
+ const r = _currentResolve;
100
+ _currentResolve = null;
93
101
  r({ status: "completed", event });
94
102
  } else {
95
- // Queue the event so the next runUnit picks it up immediately
96
103
  debugLog("resolveAgentEnd", {
97
- status: "queued",
98
- queueLength: s.pendingAgentEndQueue.length + 1,
99
- warning:
100
- "agent_end arrived between loop iterations — queued for next runUnit",
104
+ status: "no-pending-resolve",
105
+ warning: "agent_end with no pending unit",
101
106
  });
102
- s.pendingAgentEndQueue.push(event);
103
107
  }
104
108
  }
105
109
 
106
110
  export function isSessionSwitchInFlight(): boolean {
107
- return _activeSession?.sessionSwitchInFlight ?? false;
111
+ return _sessionSwitchInFlight;
108
112
  }
109
113
 
110
114
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
111
115
 
112
116
  /**
113
- * Reset session promise state. Only exported for test cleanup — production code
114
- * should never call this.
117
+ * Reset module-level promise state. Only exported for test cleanup —
118
+ * production code should never call this.
115
119
  */
116
120
  export function _resetPendingResolve(): void {
117
- if (_activeSession) {
118
- _activeSession.pendingResolve = null;
119
- _activeSession.pendingAgentEndQueue = [];
120
- }
121
- _activeSession = null;
121
+ _currentResolve = null;
122
+ _sessionSwitchInFlight = false;
122
123
  }
123
124
 
124
125
  /**
125
- * Set the active session for resolveAgentEnd. Only exported for test setup —
126
- * production code sets this via autoLoop entry.
126
+ * No-op for backward compatibility with tests that previously set the
127
+ * active session. The module no longer holds a session reference.
127
128
  */
128
- export function _setActiveSession(session: AutoSession | null): void {
129
- _activeSession = session;
129
+ export function _setActiveSession(_session: AutoSession | null): void {
130
+ // No-op — kept for test backward compatibility
130
131
  }
131
132
 
132
133
  // ─── runUnit ─────────────────────────────────────────────────────────────────
@@ -146,45 +147,18 @@ export async function runUnit(
146
147
  unitType: string,
147
148
  unitId: string,
148
149
  prompt: string,
149
- _prefs: GSDPreferences | undefined,
150
150
  ): Promise<UnitResult> {
151
151
  debugLog("runUnit", { phase: "start", unitType, unitId });
152
152
 
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
153
  // ── Session creation with timeout ──
180
154
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
181
155
 
182
156
  let sessionResult: { cancelled: boolean };
183
157
  let sessionTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
184
- s.sessionSwitchInFlight = true;
158
+ _sessionSwitchInFlight = true;
185
159
  try {
186
160
  const sessionPromise = s.cmdCtx!.newSession().finally(() => {
187
- s.sessionSwitchInFlight = false;
161
+ _sessionSwitchInFlight = false;
188
162
  });
189
163
  const timeoutPromise = new Promise<{ cancelled: true }>((resolve) => {
190
164
  sessionTimeoutHandle = setTimeout(
@@ -216,11 +190,12 @@ export async function runUnit(
216
190
  return { status: "cancelled" };
217
191
  }
218
192
 
219
- // ── Create the agent_end promise (session-scoped) ──
193
+ // ── Create the agent_end promise (per-unit one-shot) ──
220
194
  // This happens after newSession completes so session-switch agent_end events
221
195
  // from the previous session cannot resolve the new unit.
196
+ _sessionSwitchInFlight = false;
222
197
  const unitPromise = new Promise<UnitResult>((resolve) => {
223
- s.pendingResolve = resolve;
198
+ _currentResolve = resolve;
224
199
  });
225
200
 
226
201
  // Ensure cwd matches basePath before dispatch (#1389).
@@ -383,6 +358,7 @@ export interface LoopDeps {
383
358
  midTitle: string;
384
359
  state: GSDState;
385
360
  prefs: GSDPreferences | undefined;
361
+ session?: AutoSession;
386
362
  }) => Promise<DispatchAction>;
387
363
  runPreDispatchHooks: (
388
364
  unitType: string,
@@ -500,6 +476,7 @@ export interface LoopDeps {
500
476
  // Post-unit processing
501
477
  postUnitPreVerification: (
502
478
  pctx: PostUnitContext,
479
+ opts?: PreVerificationOpts,
503
480
  ) => Promise<"dispatched" | "continue">;
504
481
  runPostUnitVerification: (
505
482
  vctx: VerificationContext,
@@ -513,6 +490,96 @@ export interface LoopDeps {
513
490
  getSessionFile: (ctx: ExtensionContext) => string;
514
491
  }
515
492
 
493
+ // ─── generateMilestoneReport ──────────────────────────────────────────────────
494
+
495
+ /**
496
+ * Generate and write an HTML milestone report snapshot.
497
+ * Extracted from the milestone-transition block in autoLoop.
498
+ */
499
+ async function generateMilestoneReport(
500
+ s: AutoSession,
501
+ ctx: ExtensionContext,
502
+ milestoneId: string,
503
+ ): Promise<void> {
504
+ const { loadVisualizerData } = await import("./visualizer-data.js");
505
+ const { generateHtmlReport } = await import("./export-html.js");
506
+ const { writeReportSnapshot } = await import("./reports.js");
507
+ const { basename } = await import("node:path");
508
+
509
+ const snapData = await loadVisualizerData(s.basePath);
510
+ const completedMs = snapData.milestones.find(
511
+ (m: { id: string }) => m.id === milestoneId,
512
+ );
513
+ const msTitle = completedMs?.title ?? milestoneId;
514
+ const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
515
+ const projName = basename(s.basePath);
516
+ const doneSlices = snapData.milestones.reduce(
517
+ (acc: number, m: { slices: { done: boolean }[] }) =>
518
+ acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
519
+ 0,
520
+ );
521
+ const totalSlices = snapData.milestones.reduce(
522
+ (acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
523
+ 0,
524
+ );
525
+ const outPath = writeReportSnapshot({
526
+ basePath: s.basePath,
527
+ html: generateHtmlReport(snapData, {
528
+ projectName: projName,
529
+ projectPath: s.basePath,
530
+ gsdVersion,
531
+ milestoneId,
532
+ indexRelPath: "index.html",
533
+ }),
534
+ milestoneId,
535
+ milestoneTitle: msTitle,
536
+ kind: "milestone",
537
+ projectName: projName,
538
+ projectPath: s.basePath,
539
+ gsdVersion,
540
+ totalCost: snapData.totals?.cost ?? 0,
541
+ totalTokens: snapData.totals?.tokens.total ?? 0,
542
+ totalDuration: snapData.totals?.duration ?? 0,
543
+ doneSlices,
544
+ totalSlices,
545
+ doneMilestones: snapData.milestones.filter(
546
+ (m: { status: string }) => m.status === "complete",
547
+ ).length,
548
+ totalMilestones: snapData.milestones.length,
549
+ phase: snapData.phase,
550
+ });
551
+ ctx.ui.notify(
552
+ `Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
553
+ "info",
554
+ );
555
+ }
556
+
557
+ // ─── closeoutAndStop ──────────────────────────────────────────────────────────
558
+
559
+ /**
560
+ * If a unit is in-flight, close it out, then stop auto-mode.
561
+ * Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
562
+ */
563
+ async function closeoutAndStop(
564
+ ctx: ExtensionContext,
565
+ pi: ExtensionAPI,
566
+ s: AutoSession,
567
+ deps: LoopDeps,
568
+ reason: string,
569
+ ): Promise<void> {
570
+ if (s.currentUnit) {
571
+ await deps.closeoutUnit(
572
+ ctx,
573
+ s.basePath,
574
+ s.currentUnit.type,
575
+ s.currentUnit.id,
576
+ s.currentUnit.startedAt,
577
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
578
+ );
579
+ }
580
+ await deps.stopAuto(ctx, pi, reason);
581
+ }
582
+
516
583
  // ─── autoLoop ────────────────────────────────────────────────────────────────
517
584
 
518
585
  /**
@@ -530,7 +597,6 @@ export async function autoLoop(
530
597
  deps: LoopDeps,
531
598
  ): Promise<void> {
532
599
  debugLog("autoLoop", { phase: "enter" });
533
- _activeSession = s;
534
600
  let iteration = 0;
535
601
  let lastDerivedUnit = "";
536
602
  let sameUnitCount = 0;
@@ -668,57 +734,7 @@ export async function autoLoop(
668
734
  }
669
735
  if (vizPrefs?.auto_report !== false) {
670
736
  try {
671
- const { loadVisualizerData } = await import("./visualizer-data.js");
672
- const { generateHtmlReport } = await import("./export-html.js");
673
- const { writeReportSnapshot } = await import("./reports.js");
674
- const { basename } = await import("node:path");
675
- const snapData = await loadVisualizerData(s.basePath);
676
- const completedMs = snapData.milestones.find(
677
- (m: { id: string }) => m.id === s.currentMilestoneId,
678
- );
679
- const msTitle = completedMs?.title ?? s.currentMilestoneId;
680
- const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
681
- const projName = basename(s.basePath);
682
- const doneSlices = snapData.milestones.reduce(
683
- (acc: number, m: { slices: { done: boolean }[] }) =>
684
- acc +
685
- m.slices.filter((sl: { done: boolean }) => sl.done).length,
686
- 0,
687
- );
688
- const totalSlices = snapData.milestones.reduce(
689
- (acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
690
- 0,
691
- );
692
- const outPath = writeReportSnapshot({
693
- basePath: s.basePath,
694
- html: generateHtmlReport(snapData, {
695
- projectName: projName,
696
- projectPath: s.basePath,
697
- gsdVersion,
698
- milestoneId: s.currentMilestoneId,
699
- indexRelPath: "index.html",
700
- }),
701
- milestoneId: s.currentMilestoneId!,
702
- milestoneTitle: msTitle,
703
- kind: "milestone",
704
- projectName: projName,
705
- projectPath: s.basePath,
706
- gsdVersion,
707
- totalCost: snapData.totals?.cost ?? 0,
708
- totalTokens: snapData.totals?.tokens.total ?? 0,
709
- totalDuration: snapData.totals?.duration ?? 0,
710
- doneSlices,
711
- totalSlices,
712
- doneMilestones: snapData.milestones.filter(
713
- (m: { status: string }) => m.status === "complete",
714
- ).length,
715
- totalMilestones: snapData.milestones.length,
716
- phase: snapData.phase,
717
- });
718
- ctx.ui.notify(
719
- `Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`,
720
- "info",
721
- );
737
+ await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
722
738
  } catch (err) {
723
739
  ctx.ui.notify(
724
740
  `Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -787,7 +803,7 @@ export async function autoLoop(
787
803
  (m: { status: string }) =>
788
804
  m.status !== "complete" && m.status !== "parked",
789
805
  );
790
- if (incomplete.length === 0) {
806
+ if (incomplete.length === 0 && state.registry.length > 0) {
791
807
  // All milestones complete — merge milestone branch before stopping
792
808
  if (s.currentMilestoneId) {
793
809
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -804,6 +820,18 @@ export async function autoLoop(
804
820
  "success",
805
821
  );
806
822
  await deps.stopAuto(ctx, pi, "All milestones complete");
823
+ } else if (incomplete.length === 0 && state.registry.length === 0) {
824
+ // Empty registry — no milestones visible, likely a path resolution bug
825
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
826
+ ctx.ui.notify(
827
+ `No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
828
+ "error",
829
+ );
830
+ await deps.stopAuto(
831
+ ctx,
832
+ pi,
833
+ `No milestones found — check basePath resolution`,
834
+ );
807
835
  } else if (state.phase === "blocked") {
808
836
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
809
837
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -844,20 +872,10 @@ export async function autoLoop(
844
872
  }
845
873
 
846
874
  if (!mid || !midTitle) {
847
- if (s.currentUnit) {
848
- await deps.closeoutUnit(
849
- ctx,
850
- s.basePath,
851
- s.currentUnit.type,
852
- s.currentUnit.id,
853
- s.currentUnit.startedAt,
854
- deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
855
- );
856
- }
857
875
  const noMilestoneReason = !mid
858
876
  ? "No active milestone after merge reconciliation"
859
877
  : `Milestone ${mid} has no title after reconciliation`;
860
- await deps.stopAuto(ctx, pi, noMilestoneReason);
878
+ await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
861
879
  debugLog("autoLoop", {
862
880
  phase: "exit",
863
881
  reason: "no-milestone-after-reconciliation",
@@ -867,17 +885,7 @@ export async function autoLoop(
867
885
 
868
886
  // Terminal: complete
869
887
  if (state.phase === "complete") {
870
- if (s.currentUnit) {
871
- await deps.closeoutUnit(
872
- ctx,
873
- s.basePath,
874
- s.currentUnit.type,
875
- s.currentUnit.id,
876
- s.currentUnit.startedAt,
877
- deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
878
- );
879
- }
880
- // Milestone merge on complete
888
+ // Milestone merge on complete (before closeout so branch state is clean)
881
889
  if (s.currentMilestoneId) {
882
890
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
883
891
  }
@@ -892,25 +900,15 @@ export async function autoLoop(
892
900
  `Milestone ${mid} complete.`,
893
901
  "success",
894
902
  );
895
- await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
903
+ await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
896
904
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
897
905
  break;
898
906
  }
899
907
 
900
908
  // Terminal: blocked
901
909
  if (state.phase === "blocked") {
902
- if (s.currentUnit) {
903
- await deps.closeoutUnit(
904
- ctx,
905
- s.basePath,
906
- s.currentUnit.type,
907
- s.currentUnit.id,
908
- s.currentUnit.startedAt,
909
- deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
910
- );
911
- }
912
910
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
913
- await deps.stopAuto(ctx, pi, blockerMsg);
911
+ await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
914
912
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
915
913
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
916
914
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
@@ -941,84 +939,49 @@ export async function autoLoop(
941
939
  budgetPct,
942
940
  );
943
941
 
944
- if (newBudgetAlertLevel === 100 && budgetEnforcementAction !== "none") {
945
- const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
942
+ // Data-driven threshold check loop descending, fire first match
943
+ const threshold = BUDGET_THRESHOLDS.find(
944
+ (t) => newBudgetAlertLevel >= t.pct,
945
+ );
946
+ if (threshold) {
946
947
  s.lastBudgetAlertLevel =
947
948
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
948
- if (budgetEnforcementAction === "halt") {
949
- deps.sendDesktopNotification("GSD", msg, "error", "budget");
950
- await deps.stopAuto(ctx, pi, "Budget ceiling reached");
951
- debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
952
- break;
953
- }
954
- if (budgetEnforcementAction === "pause") {
955
- ctx.ui.notify(
956
- `${msg} Pausing auto-mode — /gsd auto to override and continue.`,
957
- "warning",
958
- );
949
+
950
+ if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
951
+ // 100% special enforcement logic (halt/pause/warn)
952
+ const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
953
+ if (budgetEnforcementAction === "halt") {
954
+ deps.sendDesktopNotification("GSD", msg, "error", "budget");
955
+ await deps.stopAuto(ctx, pi, "Budget ceiling reached");
956
+ debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
957
+ break;
958
+ }
959
+ if (budgetEnforcementAction === "pause") {
960
+ ctx.ui.notify(
961
+ `${msg} Pausing auto-mode — /gsd auto to override and continue.`,
962
+ "warning",
963
+ );
964
+ deps.sendDesktopNotification("GSD", msg, "warning", "budget");
965
+ deps.logCmuxEvent(prefs, msg, "warning");
966
+ await deps.pauseAuto(ctx, pi);
967
+ debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
968
+ break;
969
+ }
970
+ ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
959
971
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
960
972
  deps.logCmuxEvent(prefs, msg, "warning");
961
- await deps.pauseAuto(ctx, pi);
962
- debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
963
- break;
973
+ } else if (threshold.pct < 100) {
974
+ // Sub-100% simple notification
975
+ const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
976
+ ctx.ui.notify(msg, threshold.notifyLevel);
977
+ deps.sendDesktopNotification(
978
+ "GSD",
979
+ msg,
980
+ threshold.notifyLevel,
981
+ "budget",
982
+ );
983
+ deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
964
984
  }
965
- ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
966
- deps.sendDesktopNotification("GSD", msg, "warning", "budget");
967
- 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",
1021
- );
1022
985
  } else if (budgetAlertLevel === 0) {
1023
986
  s.lastBudgetAlertLevel = 0;
1024
987
  }
@@ -1091,20 +1054,11 @@ export async function autoLoop(
1091
1054
  midTitle: midTitle!,
1092
1055
  state,
1093
1056
  prefs,
1057
+ session: s,
1094
1058
  });
1095
1059
 
1096
1060
  if (dispatchResult.action === "stop") {
1097
- if (s.currentUnit) {
1098
- await deps.closeoutUnit(
1099
- ctx,
1100
- s.basePath,
1101
- s.currentUnit.type,
1102
- s.currentUnit.id,
1103
- s.currentUnit.startedAt,
1104
- deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
1105
- );
1106
- }
1107
- await deps.stopAuto(ctx, pi, dispatchResult.reason);
1061
+ await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
1108
1062
  debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
1109
1063
  break;
1110
1064
  }
@@ -1263,12 +1217,12 @@ export async function autoLoop(
1263
1217
  );
1264
1218
 
1265
1219
  if (s.currentUnitRouting) {
1266
- const isRetry =
1220
+ const isRetryForOutcome =
1267
1221
  s.currentUnit.type === unitType && s.currentUnit.id === unitId;
1268
1222
  deps.recordOutcome(
1269
1223
  s.currentUnit.type,
1270
1224
  s.currentUnitRouting.tier as "light" | "standard" | "heavy",
1271
- !isRetry,
1225
+ !isRetryForOutcome,
1272
1226
  );
1273
1227
  }
1274
1228
 
@@ -1463,7 +1417,6 @@ export async function autoLoop(
1463
1417
  unitType,
1464
1418
  unitId,
1465
1419
  finalPrompt,
1466
- prefs,
1467
1420
  );
1468
1421
  debugLog("autoLoop", {
1469
1422
  phase: "runUnit-end",
@@ -1635,7 +1588,6 @@ export async function autoLoop(
1635
1588
  item.unitType,
1636
1589
  item.unitId,
1637
1590
  item.prompt,
1638
- prefs,
1639
1591
  );
1640
1592
  deps.clearUnitTimeout();
1641
1593
 
@@ -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
  }