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
@@ -22,25 +22,24 @@ function missingSliceStop(mid, phase) {
22
22
  }
23
23
  // ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
24
24
  const MAX_REWRITE_ATTEMPTS = 3;
25
- let rewriteAttemptCount = 0;
26
- export function resetRewriteCircuitBreaker() {
27
- rewriteAttemptCount = 0;
28
- }
29
25
  // ─── Rules ────────────────────────────────────────────────────────────────
30
26
  const DISPATCH_RULES = [
31
27
  {
32
28
  name: "rewrite-docs (override gate)",
33
- match: async ({ mid, midTitle, state, basePath }) => {
29
+ match: async ({ mid, midTitle, state, basePath, session }) => {
34
30
  const pendingOverrides = await loadActiveOverrides(basePath);
35
31
  if (pendingOverrides.length === 0)
36
32
  return null;
37
- if (rewriteAttemptCount >= MAX_REWRITE_ATTEMPTS) {
33
+ const count = session?.rewriteAttemptCount ?? 0;
34
+ if (count >= MAX_REWRITE_ATTEMPTS) {
38
35
  const { resolveAllOverrides } = await import("./files.js");
39
36
  await resolveAllOverrides(basePath);
40
- rewriteAttemptCount = 0;
37
+ if (session)
38
+ session.rewriteAttemptCount = 0;
41
39
  return null;
42
40
  }
43
- rewriteAttemptCount++;
41
+ if (session)
42
+ session.rewriteAttemptCount++;
44
43
  const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
45
44
  return {
46
45
  action: "dispatch",
@@ -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,68 @@ 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 (descending). The 100% entry
22
+ * triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
23
+ * a simple notification. */
24
+ const BUDGET_THRESHOLDS = [
25
+ { pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
26
+ { pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
27
+ { pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
28
+ { pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
29
+ ];
30
+ // ─── Per-unit one-shot promise state ────────────────────────────────────────
22
31
  //
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;
32
+ // A single module-level resolve function scoped to the current unit execution.
33
+ // No queue if an agent_end arrives with no pending resolver, it is dropped
34
+ // (logged as warning). This is simpler and safer than the previous session-
35
+ // scoped pendingResolve + pendingAgentEndQueue pattern.
36
+ let _currentResolve = null;
37
+ let _sessionSwitchInFlight = false;
31
38
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
32
39
  /**
33
40
  * Called from the agent_end event handler in index.ts to resolve the
34
41
  * in-flight unit promise. One-shot: the resolver is nulled before calling
35
42
  * to prevent double-resolution from model fallback retries.
36
43
  *
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.
44
+ * If no resolver exists (event arrived between loop iterations or during
45
+ * session switch), the event is dropped with a debug warning.
39
46
  */
40
47
  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
- });
48
+ if (_sessionSwitchInFlight) {
49
+ debugLog("resolveAgentEnd", { status: "ignored-during-switch" });
47
50
  return;
48
51
  }
49
- if (s.pendingResolve) {
52
+ if (_currentResolve) {
50
53
  debugLog("resolveAgentEnd", { status: "resolving", hasEvent: true });
51
- const r = s.pendingResolve;
52
- s.pendingResolve = null;
54
+ const r = _currentResolve;
55
+ _currentResolve = null;
53
56
  r({ status: "completed", event });
54
57
  }
55
58
  else {
56
- // Queue the event so the next runUnit picks it up immediately
57
59
  debugLog("resolveAgentEnd", {
58
- status: "queued",
59
- queueLength: s.pendingAgentEndQueue.length + 1,
60
- warning: "agent_end arrived between loop iterations — queued for next runUnit",
60
+ status: "no-pending-resolve",
61
+ warning: "agent_end with no pending unit",
61
62
  });
62
- s.pendingAgentEndQueue.push(event);
63
63
  }
64
64
  }
65
65
  export function isSessionSwitchInFlight() {
66
- return _activeSession?.sessionSwitchInFlight ?? false;
66
+ return _sessionSwitchInFlight;
67
67
  }
68
68
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
69
69
  /**
70
- * Reset session promise state. Only exported for test cleanup — production code
71
- * should never call this.
70
+ * Reset module-level promise state. Only exported for test cleanup —
71
+ * production code should never call this.
72
72
  */
73
73
  export function _resetPendingResolve() {
74
- if (_activeSession) {
75
- _activeSession.pendingResolve = null;
76
- _activeSession.pendingAgentEndQueue = [];
77
- }
78
- _activeSession = null;
74
+ _currentResolve = null;
75
+ _sessionSwitchInFlight = false;
79
76
  }
80
77
  /**
81
- * Set the active session for resolveAgentEnd. Only exported for test setup —
82
- * production code sets this via autoLoop entry.
78
+ * No-op for backward compatibility with tests that previously set the
79
+ * active session. The module no longer holds a session reference.
83
80
  */
84
- export function _setActiveSession(session) {
85
- _activeSession = session;
81
+ export function _setActiveSession(_session) {
82
+ // No-op — kept for test backward compatibility
86
83
  }
87
84
  // ─── runUnit ─────────────────────────────────────────────────────────────────
88
85
  /**
@@ -93,41 +90,16 @@ export function _setActiveSession(session) {
93
90
  * On session creation failure or timeout, returns { status: 'cancelled' }
94
91
  * without awaiting the promise.
95
92
  */
96
- export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
93
+ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
97
94
  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
95
  // ── Session creation with timeout ──
124
96
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
125
97
  let sessionResult;
126
98
  let sessionTimeoutHandle;
127
- s.sessionSwitchInFlight = true;
99
+ _sessionSwitchInFlight = true;
128
100
  try {
129
101
  const sessionPromise = s.cmdCtx.newSession().finally(() => {
130
- s.sessionSwitchInFlight = false;
102
+ _sessionSwitchInFlight = false;
131
103
  });
132
104
  const timeoutPromise = new Promise((resolve) => {
133
105
  sessionTimeoutHandle = setTimeout(() => resolve({ cancelled: true }), NEW_SESSION_TIMEOUT_MS);
@@ -155,11 +127,12 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
155
127
  if (!s.active) {
156
128
  return { status: "cancelled" };
157
129
  }
158
- // ── Create the agent_end promise (session-scoped) ──
130
+ // ── Create the agent_end promise (per-unit one-shot) ──
159
131
  // This happens after newSession completes so session-switch agent_end events
160
132
  // from the previous session cannot resolve the new unit.
133
+ _sessionSwitchInFlight = false;
161
134
  const unitPromise = new Promise((resolve) => {
162
- s.pendingResolve = resolve;
135
+ _currentResolve = resolve;
163
136
  });
164
137
  // Ensure cwd matches basePath before dispatch (#1389).
165
138
  // async_bash and background jobs can drift cwd away from the worktree.
@@ -184,6 +157,60 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
184
157
  });
185
158
  return result;
186
159
  }
160
+ // ─── generateMilestoneReport ──────────────────────────────────────────────────
161
+ /**
162
+ * Generate and write an HTML milestone report snapshot.
163
+ * Extracted from the milestone-transition block in autoLoop.
164
+ */
165
+ async function generateMilestoneReport(s, ctx, milestoneId) {
166
+ const { loadVisualizerData } = await import("./visualizer-data.js");
167
+ const { generateHtmlReport } = await import("./export-html.js");
168
+ const { writeReportSnapshot } = await import("./reports.js");
169
+ const { basename } = await import("node:path");
170
+ const snapData = await loadVisualizerData(s.basePath);
171
+ const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
172
+ const msTitle = completedMs?.title ?? milestoneId;
173
+ const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
174
+ const projName = basename(s.basePath);
175
+ const doneSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.filter((sl) => sl.done).length, 0);
176
+ const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
177
+ const outPath = writeReportSnapshot({
178
+ basePath: s.basePath,
179
+ html: generateHtmlReport(snapData, {
180
+ projectName: projName,
181
+ projectPath: s.basePath,
182
+ gsdVersion,
183
+ milestoneId,
184
+ indexRelPath: "index.html",
185
+ }),
186
+ milestoneId,
187
+ milestoneTitle: msTitle,
188
+ kind: "milestone",
189
+ projectName: projName,
190
+ projectPath: s.basePath,
191
+ gsdVersion,
192
+ totalCost: snapData.totals?.cost ?? 0,
193
+ totalTokens: snapData.totals?.tokens.total ?? 0,
194
+ totalDuration: snapData.totals?.duration ?? 0,
195
+ doneSlices,
196
+ totalSlices,
197
+ doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
198
+ totalMilestones: snapData.milestones.length,
199
+ phase: snapData.phase,
200
+ });
201
+ ctx.ui.notify(`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`, "info");
202
+ }
203
+ // ─── closeoutAndStop ──────────────────────────────────────────────────────────
204
+ /**
205
+ * If a unit is in-flight, close it out, then stop auto-mode.
206
+ * Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
207
+ */
208
+ async function closeoutAndStop(ctx, pi, s, deps, reason) {
209
+ if (s.currentUnit) {
210
+ await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
211
+ }
212
+ await deps.stopAuto(ctx, pi, reason);
213
+ }
187
214
  // ─── autoLoop ────────────────────────────────────────────────────────────────
188
215
  /**
189
216
  * Main auto-mode execution loop. Iterates: derive → dispatch → guards →
@@ -195,7 +222,6 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
195
222
  */
196
223
  export async function autoLoop(ctx, pi, s, deps) {
197
224
  debugLog("autoLoop", { phase: "enter" });
198
- _activeSession = s;
199
225
  let iteration = 0;
200
226
  let lastDerivedUnit = "";
201
227
  let sameUnitCount = 0;
@@ -292,43 +318,7 @@ export async function autoLoop(ctx, pi, s, deps) {
292
318
  }
293
319
  if (vizPrefs?.auto_report !== false) {
294
320
  try {
295
- const { loadVisualizerData } = await import("./visualizer-data.js");
296
- const { generateHtmlReport } = await import("./export-html.js");
297
- const { writeReportSnapshot } = await import("./reports.js");
298
- const { basename } = await import("node:path");
299
- const snapData = await loadVisualizerData(s.basePath);
300
- const completedMs = snapData.milestones.find((m) => m.id === s.currentMilestoneId);
301
- const msTitle = completedMs?.title ?? s.currentMilestoneId;
302
- const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
303
- const projName = basename(s.basePath);
304
- const doneSlices = snapData.milestones.reduce((acc, m) => acc +
305
- m.slices.filter((sl) => sl.done).length, 0);
306
- const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
307
- const outPath = writeReportSnapshot({
308
- basePath: s.basePath,
309
- html: generateHtmlReport(snapData, {
310
- projectName: projName,
311
- projectPath: s.basePath,
312
- gsdVersion,
313
- milestoneId: s.currentMilestoneId,
314
- indexRelPath: "index.html",
315
- }),
316
- milestoneId: s.currentMilestoneId,
317
- milestoneTitle: msTitle,
318
- kind: "milestone",
319
- projectName: projName,
320
- projectPath: s.basePath,
321
- gsdVersion,
322
- totalCost: snapData.totals?.cost ?? 0,
323
- totalTokens: snapData.totals?.tokens.total ?? 0,
324
- totalDuration: snapData.totals?.duration ?? 0,
325
- doneSlices,
326
- totalSlices,
327
- doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
328
- totalMilestones: snapData.milestones.length,
329
- phase: snapData.phase,
330
- });
331
- ctx.ui.notify(`Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`, "info");
321
+ await generateMilestoneReport(s, ctx, s.currentMilestoneId);
332
322
  }
333
323
  catch (err) {
334
324
  ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
@@ -373,7 +363,7 @@ export async function autoLoop(ctx, pi, s, deps) {
373
363
  await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
374
364
  }
375
365
  const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
376
- if (incomplete.length === 0) {
366
+ if (incomplete.length === 0 && state.registry.length > 0) {
377
367
  // All milestones complete — merge milestone branch before stopping
378
368
  if (s.currentMilestoneId) {
379
369
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -382,6 +372,12 @@ export async function autoLoop(ctx, pi, s, deps) {
382
372
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
383
373
  await deps.stopAuto(ctx, pi, "All milestones complete");
384
374
  }
375
+ else if (incomplete.length === 0 && state.registry.length === 0) {
376
+ // Empty registry — no milestones visible, likely a path resolution bug
377
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
378
+ ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
379
+ await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
380
+ }
385
381
  else if (state.phase === "blocked") {
386
382
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
387
383
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -410,13 +406,10 @@ export async function autoLoop(ctx, pi, s, deps) {
410
406
  midTitle = state.activeMilestone?.title;
411
407
  }
412
408
  if (!mid || !midTitle) {
413
- if (s.currentUnit) {
414
- await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
415
- }
416
409
  const noMilestoneReason = !mid
417
410
  ? "No active milestone after merge reconciliation"
418
411
  : `Milestone ${mid} has no title after reconciliation`;
419
- await deps.stopAuto(ctx, pi, noMilestoneReason);
412
+ await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
420
413
  debugLog("autoLoop", {
421
414
  phase: "exit",
422
415
  reason: "no-milestone-after-reconciliation",
@@ -425,26 +418,20 @@ export async function autoLoop(ctx, pi, s, deps) {
425
418
  }
426
419
  // Terminal: complete
427
420
  if (state.phase === "complete") {
428
- if (s.currentUnit) {
429
- await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
430
- }
431
- // Milestone merge on complete
421
+ // Milestone merge on complete (before closeout so branch state is clean)
432
422
  if (s.currentMilestoneId) {
433
423
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
434
424
  }
435
425
  deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
436
426
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
437
- await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
427
+ await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
438
428
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
439
429
  break;
440
430
  }
441
431
  // Terminal: blocked
442
432
  if (state.phase === "blocked") {
443
- if (s.currentUnit) {
444
- await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
445
- }
446
433
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
447
- await deps.stopAuto(ctx, pi, blockerMsg);
434
+ await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
448
435
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
449
436
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
450
437
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
@@ -465,48 +452,39 @@ export async function autoLoop(ctx, pi, s, deps) {
465
452
  const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
466
453
  const enforcement = prefs?.budget_enforcement ?? "pause";
467
454
  const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
468
- if (newBudgetAlertLevel === 100 && budgetEnforcementAction !== "none") {
469
- const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
455
+ // Data-driven threshold check loop descending, fire first match
456
+ const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel >= t.pct);
457
+ if (threshold) {
470
458
  s.lastBudgetAlertLevel =
471
459
  newBudgetAlertLevel;
472
- if (budgetEnforcementAction === "halt") {
473
- deps.sendDesktopNotification("GSD", msg, "error", "budget");
474
- await deps.stopAuto(ctx, pi, "Budget ceiling reached");
475
- debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
476
- break;
477
- }
478
- if (budgetEnforcementAction === "pause") {
479
- ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
460
+ if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
461
+ // 100% special enforcement logic (halt/pause/warn)
462
+ const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
463
+ if (budgetEnforcementAction === "halt") {
464
+ deps.sendDesktopNotification("GSD", msg, "error", "budget");
465
+ await deps.stopAuto(ctx, pi, "Budget ceiling reached");
466
+ debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
467
+ break;
468
+ }
469
+ if (budgetEnforcementAction === "pause") {
470
+ ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
471
+ deps.sendDesktopNotification("GSD", msg, "warning", "budget");
472
+ deps.logCmuxEvent(prefs, msg, "warning");
473
+ await deps.pauseAuto(ctx, pi);
474
+ debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
475
+ break;
476
+ }
477
+ ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
480
478
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
481
479
  deps.logCmuxEvent(prefs, msg, "warning");
482
- await deps.pauseAuto(ctx, pi);
483
- debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
484
- break;
485
480
  }
486
- ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
487
- deps.sendDesktopNotification("GSD", msg, "warning", "budget");
488
- deps.logCmuxEvent(prefs, msg, "warning");
489
- }
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");
481
+ else if (threshold.pct < 100) {
482
+ // Sub-100% simple notification
483
+ const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
484
+ ctx.ui.notify(msg, threshold.notifyLevel);
485
+ deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
486
+ deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
487
+ }
510
488
  }
511
489
  else if (budgetAlertLevel === 0) {
512
490
  s.lastBudgetAlertLevel = 0;
@@ -557,12 +535,10 @@ export async function autoLoop(ctx, pi, s, deps) {
557
535
  midTitle: midTitle,
558
536
  state,
559
537
  prefs,
538
+ session: s,
560
539
  });
561
540
  if (dispatchResult.action === "stop") {
562
- if (s.currentUnit) {
563
- await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
564
- }
565
- await deps.stopAuto(ctx, pi, dispatchResult.reason);
541
+ await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
566
542
  debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
567
543
  break;
568
544
  }
@@ -666,8 +642,8 @@ export async function autoLoop(ctx, pi, s, deps) {
666
642
  if (s.currentUnit) {
667
643
  await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
668
644
  if (s.currentUnitRouting) {
669
- const isRetry = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
670
- deps.recordOutcome(s.currentUnit.type, s.currentUnitRouting.tier, !isRetry);
645
+ const isRetryForOutcome = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
646
+ deps.recordOutcome(s.currentUnit.type, s.currentUnitRouting.tier, !isRetryForOutcome);
671
647
  }
672
648
  const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
673
649
  const incomingKey = `${unitType}/${unitId}`;
@@ -794,7 +770,7 @@ export async function autoLoop(ctx, pi, s, deps) {
794
770
  unitType,
795
771
  unitId,
796
772
  });
797
- const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt, prefs);
773
+ const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt);
798
774
  debugLog("autoLoop", {
799
775
  phase: "runUnit-end",
800
776
  iteration,
@@ -908,7 +884,7 @@ export async function autoLoop(ctx, pi, s, deps) {
908
884
  const sidecarSessionFile = deps.getSessionFile(ctx);
909
885
  deps.writeLock(deps.lockBase(), item.unitType, item.unitId, s.completedUnits.length, sidecarSessionFile);
910
886
  // Execute via standard runUnit
911
- const sidecarResult = await runUnit(ctx, pi, s, item.unitType, item.unitId, item.prompt, prefs);
887
+ const sidecarResult = await runUnit(ctx, pi, s, item.unitType, item.unitId, item.prompt);
912
888
  deps.clearUnitTimeout();
913
889
  if (sidecarResult.status === "cancelled") {
914
890
  ctx.ui.notify(`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`, "warning");
@@ -916,8 +892,11 @@ export async function autoLoop(ctx, pi, s, deps) {
916
892
  sidecarBroke = true;
917
893
  break;
918
894
  }
919
- // Run pre-verification for the sidecar unit
920
- const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx);
895
+ // Run pre-verification for the sidecar unit (lightweight path)
896
+ const sidecarPreOpts = item.kind === "hook"
897
+ ? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
898
+ : { skipSettleDelay: true, skipStateRebuild: true };
899
+ const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
921
900
  if (sidecarPreResult === "dispatched") {
922
901
  // Pre-verification caused stop/pause
923
902
  debugLog("autoLoop", {
@@ -990,6 +969,6 @@ export async function autoLoop(ctx, pi, s, deps) {
990
969
  }
991
970
  }
992
971
  }
993
- _activeSession = null;
972
+ _currentResolve = null;
994
973
  debugLog("autoLoop", { phase: "exit", totalIterations: iteration });
995
974
  }