gsd-pi 2.37.1 → 2.38.0-dev.add4f78

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 (159) 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 +3 -0
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +61 -31
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +10 -26
  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.js +22 -2
  28. package/dist/resources/extensions/gsd/detection.js +1 -2
  29. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  30. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  31. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  32. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  33. package/dist/resources/extensions/gsd/doctor.js +184 -11
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +43 -2
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/index.js +2 -1
  38. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  39. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  40. package/dist/resources/extensions/gsd/package.json +1 -1
  41. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  42. package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
  43. package/dist/resources/extensions/gsd/preferences.js +4 -3
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  46. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  47. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  48. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  49. package/dist/resources/extensions/gsd/state.js +1 -1
  50. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  51. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  52. package/dist/resources/extensions/gsd/worktree.js +35 -16
  53. package/dist/resources/extensions/remote-questions/status.js +2 -1
  54. package/dist/resources/extensions/remote-questions/store.js +2 -1
  55. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  56. package/dist/resources/extensions/subagent/index.js +12 -3
  57. package/dist/resources/extensions/subagent/isolation.js +2 -1
  58. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  59. package/dist/resources/extensions/universal-config/package.json +1 -1
  60. package/dist/welcome-screen.d.ts +12 -0
  61. package/dist/welcome-screen.js +53 -0
  62. package/package.json +2 -1
  63. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  64. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  65. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  66. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  67. package/packages/pi-ai/dist/models.generated.js +172 -0
  68. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  69. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  70. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  71. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  72. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  73. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  74. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  75. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  76. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  77. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  78. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  80. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  82. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  83. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  84. package/packages/pi-ai/dist/types.d.ts +2 -2
  85. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  86. package/packages/pi-ai/dist/types.js.map +1 -1
  87. package/packages/pi-ai/package.json +1 -0
  88. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  89. package/packages/pi-ai/src/models.generated.ts +172 -0
  90. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  91. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  92. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  93. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  94. package/packages/pi-ai/src/types.ts +2 -0
  95. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  100. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/package.json +1 -1
  102. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  103. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  104. package/pkg/package.json +1 -1
  105. package/src/resources/extensions/cmux/index.ts +57 -1
  106. package/src/resources/extensions/env-utils.ts +31 -0
  107. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  108. package/src/resources/extensions/gsd/auto/session.ts +5 -1
  109. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  110. package/src/resources/extensions/gsd/auto-loop.ts +83 -64
  111. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  112. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  113. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  114. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  115. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  116. package/src/resources/extensions/gsd/auto.ts +14 -29
  117. package/src/resources/extensions/gsd/captures.ts +10 -1
  118. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  119. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  120. package/src/resources/extensions/gsd/commands.ts +24 -2
  121. package/src/resources/extensions/gsd/detection.ts +2 -2
  122. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  123. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  124. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  125. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  126. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  127. package/src/resources/extensions/gsd/doctor.ts +177 -13
  128. package/src/resources/extensions/gsd/export.ts +1 -1
  129. package/src/resources/extensions/gsd/files.ts +47 -2
  130. package/src/resources/extensions/gsd/forensics.ts +1 -1
  131. package/src/resources/extensions/gsd/index.ts +3 -1
  132. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  133. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  134. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  135. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  136. package/src/resources/extensions/gsd/preferences.ts +5 -3
  137. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  138. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  139. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  140. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  141. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  142. package/src/resources/extensions/gsd/state.ts +1 -1
  143. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  144. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  145. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  146. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  147. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  148. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  149. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  150. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  151. package/src/resources/extensions/gsd/types.ts +43 -0
  152. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  153. package/src/resources/extensions/gsd/worktree.ts +35 -15
  154. package/src/resources/extensions/remote-questions/status.ts +3 -1
  155. package/src/resources/extensions/remote-questions/store.ts +3 -1
  156. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  157. package/src/resources/extensions/subagent/index.ts +12 -3
  158. package/src/resources/extensions/subagent/isolation.ts +3 -1
  159. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
@@ -289,10 +289,17 @@ export class CmuxClient {
289
289
  }
290
290
 
291
291
  async createSplit(direction: "right" | "down" | "left" | "up"): Promise<string | null> {
292
+ return this.createSplitFrom(this.config.surfaceId, direction);
293
+ }
294
+
295
+ async createSplitFrom(
296
+ sourceSurfaceId: string | undefined,
297
+ direction: "right" | "down" | "left" | "up",
298
+ ): Promise<string | null> {
292
299
  if (!this.config.splits) return null;
293
300
  const before = new Set(await this.listSurfaceIds());
294
301
  const args = ["new-split", direction];
295
- const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
302
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
296
303
  await this.runAsync(scopedArgs);
297
304
  const after = await this.listSurfaceIds();
298
305
  for (const id of after) {
@@ -301,6 +308,55 @@ export class CmuxClient {
301
308
  return null;
302
309
  }
303
310
 
311
+ /**
312
+ * Create a grid of surfaces for parallel agent execution.
313
+ *
314
+ * Layout strategy (gsd stays in the original surface):
315
+ * 1 agent: [gsd | A]
316
+ * 2 agents: [gsd | A]
317
+ * [ | B]
318
+ * 3 agents: [gsd | A]
319
+ * [ C | B]
320
+ * 4 agents: [gsd | A]
321
+ * [ C | B] (D splits from B downward)
322
+ * [ | D]
323
+ *
324
+ * Returns surface IDs in order, or empty array on failure.
325
+ */
326
+ async createGridLayout(count: number): Promise<string[]> {
327
+ if (!this.config.splits || count <= 0) return [];
328
+ const surfaces: string[] = [];
329
+
330
+ // First split: create right column from the gsd surface
331
+ const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
332
+ if (!rightCol) return [];
333
+ surfaces.push(rightCol);
334
+ if (count === 1) return surfaces;
335
+
336
+ // Second split: split right column down → bottom-right
337
+ const bottomRight = await this.createSplitFrom(rightCol, "down");
338
+ if (!bottomRight) return surfaces;
339
+ surfaces.push(bottomRight);
340
+ if (count === 2) return surfaces;
341
+
342
+ // Third split: split gsd surface down → bottom-left
343
+ const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
344
+ if (!bottomLeft) return surfaces;
345
+ surfaces.push(bottomLeft);
346
+ if (count === 3) return surfaces;
347
+
348
+ // Fourth+: split subsequent surfaces down from the last created
349
+ let lastSurface = bottomRight;
350
+ for (let i = 3; i < count; i++) {
351
+ const next = await this.createSplitFrom(lastSurface, "down");
352
+ if (!next) break;
353
+ surfaces.push(next);
354
+ lastSurface = next;
355
+ }
356
+
357
+ return surfaces;
358
+ }
359
+
304
360
  async sendSurface(surfaceId: string, text: string): Promise<boolean> {
305
361
  const payload = text.endsWith("\n") ? text : `${text}\n`;
306
362
  const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
@@ -0,0 +1,31 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+
8
+ import { readFile } from "node:fs/promises";
9
+
10
+ /**
11
+ * Check which keys already exist in a .env file or process.env.
12
+ * Returns the subset of `keys` that are already set.
13
+ */
14
+ export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
15
+ let fileContent = "";
16
+ try {
17
+ fileContent = await readFile(envFilePath, "utf8");
18
+ } catch {
19
+ // ENOENT or other read error — proceed with empty content
20
+ }
21
+
22
+ const existing: string[] = [];
23
+ for (const key of keys) {
24
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
26
+ if (regex.test(fileContent) || key in process.env) {
27
+ existing.push(key);
28
+ }
29
+ }
30
+ return existing;
31
+ }
@@ -67,30 +67,11 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis
67
67
 
68
68
  // ─── Exported utilities ───────────────────────────────────────────────────────
69
69
 
70
- /**
71
- * Check which keys already exist in the .env file or process.env.
72
- * Returns the subset of `keys` that are already set.
73
- * Handles ENOENT gracefully (still checks process.env).
74
- * Empty-string values count as existing.
75
- */
76
- export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
77
- let fileContent = "";
78
- try {
79
- fileContent = await readFile(envFilePath, "utf8");
80
- } catch {
81
- // ENOENT or other read error — proceed with empty content
82
- }
83
-
84
- const existing: string[] = [];
85
- for (const key of keys) {
86
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
87
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
88
- if (regex.test(fileContent) || key in process.env) {
89
- existing.push(key);
90
- }
91
- }
92
- return existing;
93
- }
70
+ // Re-export from env-utils.ts so existing consumers still work.
71
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
72
+ // into modules that only need env-checking (e.g. files.ts during reports).
73
+ import { checkExistingEnvKeys } from "./env-utils.js";
74
+ export { checkExistingEnvKeys };
94
75
 
95
76
  /**
96
77
  * Detect the write destination based on project files in basePath.
@@ -124,6 +124,9 @@ export class AutoSession {
124
124
  // ── Sidecar queue ─────────────────────────────────────────────────────
125
125
  sidecarQueue: SidecarItem[] = [];
126
126
 
127
+ // ── Dispatch circuit breakers ──────────────────────────────────────
128
+ rewriteAttemptCount = 0;
129
+
127
130
  // ── Metrics ──────────────────────────────────────────────────────────────
128
131
  autoStartTime = 0;
129
132
  lastPromptCharCount: number | undefined;
@@ -154,7 +157,7 @@ export class AutoSession {
154
157
  * events between loop iterations. The next runUnit drains this queue
155
158
  * instead of waiting for a new event.
156
159
  */
157
- pendingAgentEndQueue: Array<{ messages: unknown[] }> = [];
160
+ pendingAgentEndQueue: Array<{ messages: unknown[]; unitId?: string }> = [];
158
161
 
159
162
  // ── Methods ──────────────────────────────────────────────────────────────
160
163
 
@@ -228,6 +231,7 @@ export class AutoSession {
228
231
  this.lastBaselineCharCount = undefined;
229
232
  this.pendingQuickTasks = [];
230
233
  this.sidecarQueue = [];
234
+ this.rewriteAttemptCount = 0;
231
235
 
232
236
  // Signal handler
233
237
  this.sigtermHandler = null;
@@ -38,6 +38,7 @@ import {
38
38
  buildRunUatPrompt,
39
39
  buildReassessRoadmapPrompt,
40
40
  buildRewriteDocsPrompt,
41
+ buildReactiveExecutePrompt,
41
42
  checkNeedsReassessment,
42
43
  checkNeedsRunUat,
43
44
  } from "./auto-prompts.js";
@@ -61,6 +62,7 @@ export interface DispatchContext {
61
62
  midTitle: string;
62
63
  state: GSDState;
63
64
  prefs: GSDPreferences | undefined;
65
+ session?: import("./auto/session.js").AutoSession;
64
66
  }
65
67
 
66
68
  interface DispatchRule {
@@ -81,26 +83,23 @@ function missingSliceStop(mid: string, phase: string): DispatchAction {
81
83
  // ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
82
84
 
83
85
  const MAX_REWRITE_ATTEMPTS = 3;
84
- let rewriteAttemptCount = 0;
85
- export function resetRewriteCircuitBreaker(): void {
86
- rewriteAttemptCount = 0;
87
- }
88
86
 
89
87
  // ─── Rules ────────────────────────────────────────────────────────────────
90
88
 
91
89
  const DISPATCH_RULES: DispatchRule[] = [
92
90
  {
93
91
  name: "rewrite-docs (override gate)",
94
- match: async ({ mid, midTitle, state, basePath }) => {
92
+ match: async ({ mid, midTitle, state, basePath, session }) => {
95
93
  const pendingOverrides = await loadActiveOverrides(basePath);
96
94
  if (pendingOverrides.length === 0) return null;
97
- if (rewriteAttemptCount >= MAX_REWRITE_ATTEMPTS) {
95
+ const count = session?.rewriteAttemptCount ?? 0;
96
+ if (count >= MAX_REWRITE_ATTEMPTS) {
98
97
  const { resolveAllOverrides } = await import("./files.js");
99
98
  await resolveAllOverrides(basePath);
100
- rewriteAttemptCount = 0;
99
+ if (session) session.rewriteAttemptCount = 0;
101
100
  return null;
102
101
  }
103
- rewriteAttemptCount++;
102
+ if (session) session.rewriteAttemptCount++;
104
103
  const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
105
104
  return {
106
105
  action: "dispatch",
@@ -309,6 +308,98 @@ const DISPATCH_RULES: DispatchRule[] = [
309
308
  };
310
309
  },
311
310
  },
311
+ {
312
+ name: "executing → reactive-execute (parallel dispatch)",
313
+ match: async ({ state, mid, midTitle, basePath, prefs }) => {
314
+ if (state.phase !== "executing" || !state.activeTask) return null;
315
+ if (!state.activeSlice) return null; // fall through
316
+
317
+ // Only activate when reactive_execution is explicitly enabled
318
+ const reactiveConfig = prefs?.reactive_execution;
319
+ if (!reactiveConfig?.enabled) return null;
320
+
321
+ const sid = state.activeSlice.id;
322
+ const sTitle = state.activeSlice.title;
323
+ const maxParallel = reactiveConfig.max_parallel ?? 2;
324
+
325
+ // Dry-run mode: max_parallel=1 means graph is derived and logged but
326
+ // execution remains sequential
327
+ if (maxParallel <= 1) return null;
328
+
329
+ try {
330
+ const {
331
+ loadSliceTaskIO,
332
+ deriveTaskGraph,
333
+ isGraphAmbiguous,
334
+ getReadyTasks,
335
+ chooseNonConflictingSubset,
336
+ graphMetrics,
337
+ } = await import("./reactive-graph.js");
338
+
339
+ const taskIO = await loadSliceTaskIO(basePath, mid, sid);
340
+ if (taskIO.length < 2) return null; // single task, no point
341
+
342
+ const graph = deriveTaskGraph(taskIO);
343
+
344
+ // Ambiguous graph → fall through to sequential
345
+ if (isGraphAmbiguous(graph)) return null;
346
+
347
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
348
+ const readyIds = getReadyTasks(graph, completed, new Set());
349
+
350
+ // Only activate reactive dispatch when >1 task is ready
351
+ if (readyIds.length <= 1) return null;
352
+
353
+ const selected = chooseNonConflictingSubset(
354
+ readyIds,
355
+ graph,
356
+ maxParallel,
357
+ new Set(),
358
+ );
359
+ if (selected.length <= 1) return null;
360
+
361
+ // Log graph metrics for observability
362
+ const metrics = graphMetrics(graph);
363
+ process.stderr.write(
364
+ `gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
365
+ `ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`,
366
+ );
367
+
368
+ // Persist dispatched batch so verification and recovery can check
369
+ // exactly which tasks were sent.
370
+ const { saveReactiveState } = await import("./reactive-graph.js");
371
+ saveReactiveState(basePath, mid, sid, {
372
+ sliceId: sid,
373
+ completed: [...completed],
374
+ dispatched: selected,
375
+ graphSnapshot: metrics,
376
+ updatedAt: new Date().toISOString(),
377
+ });
378
+
379
+ // Encode selected task IDs in unitId for artifact verification.
380
+ // Format: M001/S01/reactive+T02,T03
381
+ const batchSuffix = selected.join(",");
382
+
383
+ return {
384
+ action: "dispatch",
385
+ unitType: "reactive-execute",
386
+ unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
387
+ prompt: await buildReactiveExecutePrompt(
388
+ mid,
389
+ midTitle,
390
+ sid,
391
+ sTitle,
392
+ selected,
393
+ basePath,
394
+ ),
395
+ };
396
+ } catch (err) {
397
+ // Non-fatal — fall through to sequential execution
398
+ process.stderr.write(`gsd-reactive: graph derivation failed: ${(err as Error).message}\n`);
399
+ return null;
400
+ }
401
+ },
402
+ },
312
403
  {
313
404
  name: "executing → execute-task (recover missing task plan → plan-slice)",
314
405
  match: async ({ state, mid, midTitle, basePath }) => {
@@ -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
  /**
@@ -96,10 +109,11 @@ export function resolveAgentEnd(event: AgentEndEvent): void {
96
109
  debugLog("resolveAgentEnd", {
97
110
  status: "queued",
98
111
  queueLength: s.pendingAgentEndQueue.length + 1,
112
+ unitId: s.currentUnit?.id,
99
113
  warning:
100
114
  "agent_end arrived between loop iterations — queued for next runUnit",
101
115
  });
102
- s.pendingAgentEndQueue.push(event);
116
+ s.pendingAgentEndQueue.push({ ...event, unitId: s.currentUnit?.id });
103
117
  }
104
118
  }
105
119
 
@@ -166,14 +180,37 @@ export async function runUnit(
166
180
  ];
167
181
  }
168
182
  if (s.pendingAgentEndQueue.length > 0) {
169
- const queued = s.pendingAgentEndQueue.shift()!;
183
+ // Find an event matching this unit; discard stale events from other units
184
+ const matchIdx = s.pendingAgentEndQueue.findIndex(
185
+ (e) => !e.unitId || e.unitId === unitId,
186
+ );
187
+ if (matchIdx >= 0) {
188
+ // Discard any stale events before the match
189
+ if (matchIdx > 0) {
190
+ debugLog("runUnit", {
191
+ phase: "discarded-stale-events",
192
+ count: matchIdx,
193
+ unitType,
194
+ unitId,
195
+ });
196
+ }
197
+ const queued = s.pendingAgentEndQueue.splice(0, matchIdx + 1).pop()!;
198
+ debugLog("runUnit", {
199
+ phase: "drained-queued-event",
200
+ unitType,
201
+ unitId,
202
+ queueRemaining: s.pendingAgentEndQueue.length,
203
+ });
204
+ return { status: "completed", event: queued };
205
+ }
206
+ // No matching event — discard all stale events and proceed to new session
170
207
  debugLog("runUnit", {
171
- phase: "drained-queued-event",
208
+ phase: "discarded-all-stale-events",
209
+ count: s.pendingAgentEndQueue.length,
172
210
  unitType,
173
211
  unitId,
174
- queueRemaining: s.pendingAgentEndQueue.length,
175
212
  });
176
- return { status: "completed", event: queued };
213
+ s.pendingAgentEndQueue = [];
177
214
  }
178
215
 
179
216
  // ── Session creation with timeout ──
@@ -383,6 +420,7 @@ export interface LoopDeps {
383
420
  midTitle: string;
384
421
  state: GSDState;
385
422
  prefs: GSDPreferences | undefined;
423
+ session?: AutoSession;
386
424
  }) => Promise<DispatchAction>;
387
425
  runPreDispatchHooks: (
388
426
  unitType: string,
@@ -500,6 +538,7 @@ export interface LoopDeps {
500
538
  // Post-unit processing
501
539
  postUnitPreVerification: (
502
540
  pctx: PostUnitContext,
541
+ opts?: PreVerificationOpts,
503
542
  ) => Promise<"dispatched" | "continue">;
504
543
  runPostUnitVerification: (
505
544
  vctx: VerificationContext,
@@ -787,7 +826,7 @@ export async function autoLoop(
787
826
  (m: { status: string }) =>
788
827
  m.status !== "complete" && m.status !== "parked",
789
828
  );
790
- if (incomplete.length === 0) {
829
+ if (incomplete.length === 0 && state.registry.length > 0) {
791
830
  // All milestones complete — merge milestone branch before stopping
792
831
  if (s.currentMilestoneId) {
793
832
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -804,6 +843,18 @@ export async function autoLoop(
804
843
  "success",
805
844
  );
806
845
  await deps.stopAuto(ctx, pi, "All milestones complete");
846
+ } else if (incomplete.length === 0 && state.registry.length === 0) {
847
+ // Empty registry — no milestones visible, likely a path resolution bug
848
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
849
+ ctx.ui.notify(
850
+ `No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
851
+ "error",
852
+ );
853
+ await deps.stopAuto(
854
+ ctx,
855
+ pi,
856
+ `No milestones found — check basePath resolution`,
857
+ );
807
858
  } else if (state.phase === "blocked") {
808
859
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
809
860
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -965,62 +1016,26 @@ export async function autoLoop(
965
1016
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
966
1017
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
967
1018
  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",
1019
+ } else {
1020
+ // Data-driven 75/80/90% threshold notifications
1021
+ const threshold = BUDGET_THRESHOLDS.find(
1022
+ (t) => newBudgetAlertLevel === t.pct,
1021
1023
  );
1022
- } else if (budgetAlertLevel === 0) {
1023
- s.lastBudgetAlertLevel = 0;
1024
+ if (threshold) {
1025
+ s.lastBudgetAlertLevel =
1026
+ newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
1027
+ const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
1028
+ ctx.ui.notify(msg, threshold.notifyLevel);
1029
+ deps.sendDesktopNotification(
1030
+ "GSD",
1031
+ msg,
1032
+ threshold.notifyLevel,
1033
+ "budget",
1034
+ );
1035
+ deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
1036
+ } else if (budgetAlertLevel === 0) {
1037
+ s.lastBudgetAlertLevel = 0;
1038
+ }
1024
1039
  }
1025
1040
  } else {
1026
1041
  s.lastBudgetAlertLevel = 0;
@@ -1091,6 +1106,7 @@ export async function autoLoop(
1091
1106
  midTitle: midTitle!,
1092
1107
  state,
1093
1108
  prefs,
1109
+ session: s,
1094
1110
  });
1095
1111
 
1096
1112
  if (dispatchResult.action === "stop") {
@@ -1649,9 +1665,12 @@ export async function autoLoop(
1649
1665
  break;
1650
1666
  }
1651
1667
 
1652
- // Run pre-verification for the sidecar unit
1668
+ // Run pre-verification for the sidecar unit (lightweight path)
1669
+ const sidecarPreOpts: PreVerificationOpts = item.kind === "hook"
1670
+ ? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
1671
+ : { skipSettleDelay: true, skipStateRebuild: true };
1653
1672
  const sidecarPreResult =
1654
- await deps.postUnitPreVerification(postUnitCtx);
1673
+ await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
1655
1674
  if (sidecarPreResult === "dispatched") {
1656
1675
  // Pre-verification caused stop/pause
1657
1676
  debugLog("autoLoop", {