gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -23,24 +23,31 @@ import type {
23
23
  ExtensionCommandContext,
24
24
  ExtensionContext,
25
25
  } from "@gsd/pi-coding-agent";
26
- import {
27
- createBashTool,
28
- createEditTool,
29
- createReadTool,
30
- createWriteTool,
31
- importExtensionModule,
32
- isToolCallEventType,
33
- } from "@gsd/pi-coding-agent";
26
+ import { createBashTool, createWriteTool, createReadTool, createEditTool, isToolCallEventType } from "@gsd/pi-coding-agent";
34
27
  import { Type } from "@sinclair/typebox";
35
28
 
36
29
  import { debugLog, debugTime } from "./debug-logger.js";
37
- import { registerLazyGSDCommand } from "./commands-bootstrap.js";
30
+ import { registerGSDCommand } from "./commands.js";
38
31
  import { loadToolApiKeys } from "./commands-config.js";
39
32
  import { registerExitCommand } from "./exit-command.js";
40
- import { registerLazyWorktreeCommands } from "./worktree-command-bootstrap.js";
33
+ import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
34
+ import { getActiveAutoWorktreeContext } from "./auto-worktree.js";
41
35
  import { saveFile, formatContinue, loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverridesSection } from "./files.js";
42
36
  import { loadPrompt } from "./prompt-loader.js";
37
+ import { deriveState } from "./state.js";
38
+ import { isAutoActive, isAutoPaused, pauseAuto, getAutoDashboardData, getAutoModeStartModel, markToolStart, markToolEnd } from "./auto.js";
39
+ import { isSessionSwitchInFlight, resolveAgentEnd } from "./auto-loop.js";
43
40
  import { saveActivityLog } from "./activity-log.js";
41
+ import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId, findMilestoneIds, nextMilestoneId } from "./guided-flow.js";
42
+ import { GSDDashboardOverlay } from "./dashboard-overlay.js";
43
+ import {
44
+ loadEffectiveGSDPreferences,
45
+ renderPreferencesForSystemPrompt,
46
+ resolveAllSkillReferences,
47
+ resolveModelWithFallbacksForUnit,
48
+ getNextFallbackModel,
49
+ isTransientNetworkError,
50
+ } from "./preferences.js";
44
51
  import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-discovery.js";
45
52
  import {
46
53
  resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
@@ -54,48 +61,10 @@ import { existsSync, readFileSync } from "node:fs";
54
61
  import { homedir } from "node:os";
55
62
  import { shortcutDesc } from "../shared/mod.js";
56
63
  import { Text } from "@gsd/pi-tui";
64
+ import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
57
65
  import { toPosixPath } from "../shared/mod.js";
66
+ import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
58
67
  import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
59
- import { getErrorMessage } from "./error-utils.js";
60
-
61
- function memoizeImport<T>(loader: () => Promise<T>): () => Promise<T> {
62
- let promise: Promise<T> | null = null;
63
- return () => {
64
- if (!promise) {
65
- promise = loader();
66
- }
67
- return promise;
68
- };
69
- }
70
-
71
- const loadAutoModule = memoizeImport(() => importExtensionModule<typeof import("./auto.js")>(import.meta.url, "./auto.js"));
72
- const loadStateModule = memoizeImport(() => importExtensionModule<typeof import("./state.js")>(import.meta.url, "./state.js"));
73
- const loadGuidedFlowModule = memoizeImport(() => importExtensionModule<typeof import("./guided-flow.js")>(import.meta.url, "./guided-flow.js"));
74
- const loadPreferencesModule = memoizeImport(() => importExtensionModule<typeof import("./preferences.js")>(import.meta.url, "./preferences.js"));
75
- const loadDashboardOverlayModule = memoizeImport(() => importExtensionModule<typeof import("./dashboard-overlay.js")>(import.meta.url, "./dashboard-overlay.js"));
76
- const loadWorktreeCommandModule = memoizeImport(() => importExtensionModule<typeof import("./worktree-command.js")>(import.meta.url, "./worktree-command.js"));
77
- const loadAutoWorktreeModule = memoizeImport(() => importExtensionModule<typeof import("./auto-worktree.js")>(import.meta.url, "./auto-worktree.js"));
78
- const loadProviderErrorPauseModule = memoizeImport(() => importExtensionModule<typeof import("./provider-error-pause.js")>(import.meta.url, "./provider-error-pause.js"));
79
- const loadParallelOrchestratorModule = memoizeImport(() => importExtensionModule<typeof import("./parallel-orchestrator.js")>(import.meta.url, "./parallel-orchestrator.js"));
80
-
81
- /**
82
- * Ensure the GSD database is available, auto-initializing if needed.
83
- * Returns true if the DB is ready, false if initialization failed.
84
- */
85
- async function ensureDbAvailable(): Promise<boolean> {
86
- try {
87
- const db = await importExtensionModule<typeof import("./gsd-db.js")>(import.meta.url, "./gsd-db.js");
88
- if (db.isDbAvailable()) return true;
89
-
90
- // Auto-initialize: open (and create if needed) the DB at the standard path
91
- const gsdDir = gsdRoot(process.cwd());
92
- if (!existsSync(gsdDir)) return false; // No GSD project — can't create DB
93
- const dbPath = join(gsdDir, "gsd.db");
94
- return db.openDatabase(dbPath);
95
- } catch {
96
- return false;
97
- }
98
- }
99
68
 
100
69
  // ── Agent Instructions ────────────────────────────────────────────────────
101
70
  // Lightweight "always follow" files injected into every GSD agent session.
@@ -126,9 +95,7 @@ function loadAgentInstructions(): string | null {
126
95
  }
127
96
 
128
97
  // ── Depth verification state ──────────────────────────────────────────────
129
- // Tracks which milestones have passed depth verification.
130
- // Single-milestone flows set '*' (wildcard). Multi-milestone flows set per-ID.
131
- const depthVerifiedMilestones = new Set<string>();
98
+ let depthVerificationDone = false;
132
99
 
133
100
  // ── Queue phase tracking ──────────────────────────────────────────────────
134
101
  // When true, the LLM is in a queue flow writing CONTEXT.md files.
@@ -139,28 +106,11 @@ let activeQueuePhase = false;
139
106
  // Tracks per-model retry attempts for transient network errors.
140
107
  // Cleared when a model switch occurs or retries are exhausted.
141
108
  const networkRetryCounters = new Map<string, number>();
142
-
143
- // ── Transient error escalation ───────────────────────────────────────────
144
- // Tracks consecutive transient auto-resume attempts. Each attempt doubles
145
- // the delay. After MAX_TRANSIENT_AUTO_RESUMES consecutive failures, auto-mode
146
- // pauses indefinitely to avoid infinite rapid-fire retries (#1166).
147
- const MAX_TRANSIENT_AUTO_RESUMES = 5;
109
+ const MAX_TRANSIENT_AUTO_RESUMES = 3;
148
110
  let consecutiveTransientErrors = 0;
149
111
 
150
112
  export function isDepthVerified(): boolean {
151
- return depthVerifiedMilestones.has("*") || depthVerifiedMilestones.size > 0;
152
- }
153
-
154
- /** Check whether a specific milestone has passed depth verification. */
155
- export function isDepthVerifiedFor(milestoneId: string): boolean {
156
- // Wildcard means "all milestones verified" (single-milestone flow)
157
- if (depthVerifiedMilestones.has("*")) return true;
158
- return depthVerifiedMilestones.has(milestoneId);
159
- }
160
-
161
- /** Mark a specific milestone as depth-verified. */
162
- export function markDepthVerified(milestoneId: string): void {
163
- depthVerifiedMilestones.add(milestoneId);
113
+ return depthVerificationDone;
164
114
  }
165
115
 
166
116
  /** Check whether a queue phase is active. */
@@ -191,25 +141,11 @@ export function shouldBlockContextWrite(
191
141
  if (!inDiscussion && !inQueue) return { block: false };
192
142
 
193
143
  if (!MILESTONE_CONTEXT_RE.test(inputPath)) return { block: false };
194
-
195
- // For discussion flows: check global depth verification (backward compat)
196
- if (inDiscussion && depthVerified) return { block: false };
197
-
198
- // For queue flows: extract milestone ID from the path and check per-milestone verification
199
- if (inQueue) {
200
- const pathMatch = inputPath.match(/\/(M\d+(?:-[a-z0-9]{6})?)-CONTEXT\.md$/);
201
- const targetMid = pathMatch?.[1];
202
- if (targetMid && depthVerifiedMilestones.has(targetMid)) return { block: false };
203
- // Wildcard passes all
204
- if (depthVerifiedMilestones.has("*")) return { block: false };
205
- }
144
+ if (depthVerified) return { block: false };
206
145
 
207
146
  return {
208
147
  block: true,
209
- reason: `Blocked: Cannot write milestone CONTEXT.md without depth verification. ` +
210
- `Use ask_user_questions with a question id containing "depth_verification" first. ` +
211
- `For multi-milestone flows, include the milestone ID in the question id (e.g., "depth_verification_M001"). ` +
212
- `This ensures each milestone's context has been critically examined before being written.`,
148
+ reason: `Blocked: Cannot write to milestone CONTEXT.md during discussion phase without depth verification. Call ask_user_questions with question id "depth_verification" first to confirm discussion depth before writing context.`,
213
149
  };
214
150
  }
215
151
 
@@ -224,8 +160,8 @@ const GSD_LOGO_LINES = [
224
160
  ];
225
161
 
226
162
  export default function (pi: ExtensionAPI) {
227
- registerLazyGSDCommand(pi);
228
- registerLazyWorktreeCommands(pi);
163
+ registerGSDCommand(pi);
164
+ registerWorktreeCommand(pi);
229
165
  registerExitCommand(pi);
230
166
 
231
167
  // ── EPIPE guard — prevent crash when stdout/stderr pipe closes unexpectedly ──
@@ -235,22 +171,11 @@ export default function (pi: ExtensionAPI) {
235
171
  // chance to persist state and pause instead of crashing (see issue #739).
236
172
  if (!process.listeners("uncaughtException").some(l => l.name === "_gsdEpipeGuard")) {
237
173
  const _gsdEpipeGuard = (err: Error): void => {
238
- const code = (err as NodeJS.ErrnoException).code;
239
- if (code === "EPIPE") {
174
+ if ((err as NodeJS.ErrnoException).code === "EPIPE") {
240
175
  // Pipe closed — nothing we can write; just exit cleanly
241
176
  process.exit(0);
242
177
  }
243
- // ECOMPROMISED: proper-lockfile's update timer detected mtime drift (system
244
- // sleep, heavy event loop stall, or filesystem precision mismatch on Node.js
245
- // v25+). The onCompromised callback already set _lockCompromised = true, but
246
- // due to a subtle interaction between the synchronous fs adapter and the
247
- // setTimeout boundary, the error can still propagate here as an uncaught
248
- // exception. Exit cleanly so the process.once("exit") handler removes the
249
- // lock directory — allowing the next session to acquire cleanly (#1322).
250
- if (code === "ECOMPROMISED") {
251
- process.exit(1);
252
- }
253
- // Re-throw anything that isn't EPIPE or ECOMPROMISED so real crashes still surface
178
+ // Re-throw anything that isn't EPIPE so real crashes still surface
254
179
  throw err;
255
180
  };
256
181
  process.on("uncaughtException", _gsdEpipeGuard);
@@ -371,8 +296,14 @@ export default function (pi: ExtensionAPI) {
371
296
  when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
372
297
  }),
373
298
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
374
- // Ensure DB is available (auto-initialize if needed)
375
- if (!await ensureDbAvailable()) {
299
+ // Check DB availability
300
+ let dbAvailable = false;
301
+ try {
302
+ const db = await import("./gsd-db.js");
303
+ dbAvailable = db.isDbAvailable();
304
+ } catch { /* dynamic import failed */ }
305
+
306
+ if (!dbAvailable) {
376
307
  return {
377
308
  content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot save decision." }],
378
309
  isError: true,
@@ -381,7 +312,7 @@ export default function (pi: ExtensionAPI) {
381
312
  }
382
313
 
383
314
  try {
384
- const { saveDecisionToDb } = await importExtensionModule<typeof import("./db-writer.js")>(import.meta.url, "./db-writer.js");
315
+ const { saveDecisionToDb } = await import("./db-writer.js");
385
316
  const { id } = await saveDecisionToDb(
386
317
  {
387
318
  scope: params.scope,
@@ -398,7 +329,7 @@ export default function (pi: ExtensionAPI) {
398
329
  details: { operation: "save_decision", id },
399
330
  };
400
331
  } catch (err) {
401
- const msg = getErrorMessage(err);
332
+ const msg = err instanceof Error ? err.message : String(err);
402
333
  process.stderr.write(`gsd-db: gsd_save_decision tool failed: ${msg}\n`);
403
334
  return {
404
335
  content: [{ type: "text" as const, text: `Error saving decision: ${msg}` }],
@@ -432,8 +363,13 @@ export default function (pi: ExtensionAPI) {
432
363
  supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
433
364
  }),
434
365
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
435
- // Ensure DB is available (auto-initialize if needed)
436
- if (!await ensureDbAvailable()) {
366
+ let dbAvailable = false;
367
+ try {
368
+ const db = await import("./gsd-db.js");
369
+ dbAvailable = db.isDbAvailable();
370
+ } catch { /* dynamic import failed */ }
371
+
372
+ if (!dbAvailable) {
437
373
  return {
438
374
  content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot update requirement." }],
439
375
  isError: true,
@@ -443,7 +379,7 @@ export default function (pi: ExtensionAPI) {
443
379
 
444
380
  try {
445
381
  // Verify requirement exists
446
- const db = await importExtensionModule<typeof import("./gsd-db.js")>(import.meta.url, "./gsd-db.js");
382
+ const db = await import("./gsd-db.js");
447
383
  const existing = db.getRequirementById(params.id);
448
384
  if (!existing) {
449
385
  return {
@@ -453,7 +389,7 @@ export default function (pi: ExtensionAPI) {
453
389
  };
454
390
  }
455
391
 
456
- const { updateRequirementInDb } = await importExtensionModule<typeof import("./db-writer.js")>(import.meta.url, "./db-writer.js");
392
+ const { updateRequirementInDb } = await import("./db-writer.js");
457
393
  const updates: Record<string, string | undefined> = {};
458
394
  if (params.status !== undefined) updates.status = params.status;
459
395
  if (params.validation !== undefined) updates.validation = params.validation;
@@ -469,7 +405,7 @@ export default function (pi: ExtensionAPI) {
469
405
  details: { operation: "update_requirement", id: params.id },
470
406
  };
471
407
  } catch (err) {
472
- const msg = getErrorMessage(err);
408
+ const msg = err instanceof Error ? err.message : String(err);
473
409
  process.stderr.write(`gsd-db: gsd_update_requirement tool failed: ${msg}\n`);
474
410
  return {
475
411
  content: [{ type: "text" as const, text: `Error updating requirement: ${msg}` }],
@@ -501,8 +437,13 @@ export default function (pi: ExtensionAPI) {
501
437
  content: Type.String({ description: "The full markdown content of the artifact" }),
502
438
  }),
503
439
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
504
- // Ensure DB is available (auto-initialize if needed)
505
- if (!await ensureDbAvailable()) {
440
+ let dbAvailable = false;
441
+ try {
442
+ const db = await import("./gsd-db.js");
443
+ dbAvailable = db.isDbAvailable();
444
+ } catch { /* dynamic import failed */ }
445
+
446
+ if (!dbAvailable) {
506
447
  return {
507
448
  content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot save artifact." }],
508
449
  isError: true,
@@ -531,7 +472,7 @@ export default function (pi: ExtensionAPI) {
531
472
  relativePath = `milestones/${params.milestone_id}/${params.milestone_id}-${params.artifact_type}.md`;
532
473
  }
533
474
 
534
- const { saveArtifactToDb } = await importExtensionModule<typeof import("./db-writer.js")>(import.meta.url, "./db-writer.js");
475
+ const { saveArtifactToDb } = await import("./db-writer.js");
535
476
  await saveArtifactToDb(
536
477
  {
537
478
  path: relativePath,
@@ -549,7 +490,7 @@ export default function (pi: ExtensionAPI) {
549
490
  details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type },
550
491
  };
551
492
  } catch (err) {
552
- const msg = getErrorMessage(err);
493
+ const msg = err instanceof Error ? err.message : String(err);
553
494
  process.stderr.write(`gsd-db: gsd_save_summary tool failed: ${msg}\n`);
554
495
  return {
555
496
  content: [{ type: "text" as const, text: `Error saving artifact: ${msg}` }],
@@ -586,10 +527,6 @@ export default function (pi: ExtensionAPI) {
586
527
  parameters: Type.Object({}),
587
528
  async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
588
529
  try {
589
- const [{ findMilestoneIds, nextMilestoneId }, { loadEffectiveGSDPreferences }] = await Promise.all([
590
- loadGuidedFlowModule(),
591
- loadPreferencesModule(),
592
- ]);
593
530
  const basePath = process.cwd();
594
531
  const existingIds = findMilestoneIds(basePath);
595
532
  const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
@@ -602,7 +539,7 @@ export default function (pi: ExtensionAPI) {
602
539
  details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled },
603
540
  };
604
541
  } catch (err) {
605
- const msg = getErrorMessage(err);
542
+ const msg = err instanceof Error ? err.message : String(err);
606
543
  return {
607
544
  content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
608
545
  isError: true,
@@ -614,9 +551,8 @@ export default function (pi: ExtensionAPI) {
614
551
 
615
552
  // ── session_start: render branded GSD header + load tool keys + remote status ──
616
553
  pi.on("session_start", async (_event, ctx) => {
617
- // Clear depth verification and queue phase state from any prior session
618
- depthVerifiedMilestones.clear();
619
- activeQueuePhase = false;
554
+ // Clear per-session state that must not leak across sessions (e.g. RPC mode)
555
+ depthVerificationDone = false;
620
556
 
621
557
  // Theme access throws in RPC mode (no TUI) — header is decorative, skip it
622
558
  try {
@@ -635,17 +571,11 @@ export default function (pi: ExtensionAPI) {
635
571
  // Load tool API keys from auth.json into environment
636
572
  loadToolApiKeys();
637
573
 
638
- // Always-on health widget — ambient system health signal below the editor
639
- try {
640
- const { initHealthWidget } = await importExtensionModule<typeof import("./health-widget.js")>(import.meta.url, "./health-widget.js");
641
- initHealthWidget(ctx);
642
- } catch { /* non-fatal — widget is best-effort */ }
643
-
644
574
  // Notify remote questions status if configured
645
575
  try {
646
576
  const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
647
- importExtensionModule<typeof import("../remote-questions/config.js")>(import.meta.url, "../remote-questions/config.js"),
648
- importExtensionModule<typeof import("../remote-questions/status.js")>(import.meta.url, "../remote-questions/status.js"),
577
+ import("../remote-questions/config.js"),
578
+ import("../remote-questions/status.js"),
649
579
  ]);
650
580
  const status = getRemoteConfigStatus();
651
581
  const latest = getLatestPromptSummary();
@@ -663,13 +593,12 @@ export default function (pi: ExtensionAPI) {
663
593
  description: shortcutDesc("Open GSD dashboard", "/gsd status"),
664
594
  handler: async (ctx) => {
665
595
  // Only show if .gsd/ exists
666
- if (!existsSync(gsdRoot(process.cwd()))) {
596
+ if (!existsSync(join(process.cwd(), ".gsd"))) {
667
597
  ctx.ui.notify("No .gsd/ directory found. Run /gsd to start.", "info");
668
598
  return;
669
599
  }
670
600
 
671
- const { GSDDashboardOverlay } = await loadDashboardOverlayModule();
672
- const result = await ctx.ui.custom<void>(
601
+ await ctx.ui.custom<void>(
673
602
  (tui, theme, _kb, done) => {
674
603
  return new GSDDashboardOverlay(tui, theme, () => done());
675
604
  },
@@ -683,23 +612,15 @@ export default function (pi: ExtensionAPI) {
683
612
  },
684
613
  },
685
614
  );
686
-
687
- // Fallback for RPC mode where ctx.ui.custom() returns undefined.
688
- if (result === undefined) {
689
- const { fireStatusViaCommand } = await importExtensionModule<typeof import("./commands.js")>(import.meta.url, "./commands.js");
690
- await fireStatusViaCommand(ctx);
691
- }
692
615
  },
693
616
  });
694
617
 
695
618
  // ── before_agent_start: inject GSD contract into true system prompt ─────
696
619
  pi.on("before_agent_start", async (event, ctx: ExtensionContext) => {
697
- if (!existsSync(gsdRoot(process.cwd()))) return;
620
+ if (!existsSync(join(process.cwd(), ".gsd"))) return;
698
621
 
699
622
  const stopContextTimer = debugTime("context-inject");
700
623
  const systemContent = loadPrompt("system");
701
- const { loadEffectiveGSDPreferences, resolveAllSkillReferences, renderPreferencesForSystemPrompt } =
702
- await loadPreferencesModule();
703
624
  const loadedPreferences = loadEffectiveGSDPreferences();
704
625
  let preferenceBlock = "";
705
626
  if (loadedPreferences) {
@@ -733,7 +654,7 @@ export default function (pi: ExtensionAPI) {
733
654
  // Inject auto-learned project memories
734
655
  let memoryBlock = "";
735
656
  try {
736
- const { getActiveMemoriesRanked, formatMemoriesForPrompt } = await importExtensionModule<typeof import("./memory-store.js")>(import.meta.url, "./memory-store.js");
657
+ const { getActiveMemoriesRanked, formatMemoriesForPrompt } = await import("./memory-store.js");
737
658
  const memories = getActiveMemoriesRanked(30);
738
659
  if (memories.length > 0) {
739
660
  const formatted = formatMemoriesForPrompt(memories, 2000);
@@ -763,10 +684,6 @@ export default function (pi: ExtensionAPI) {
763
684
 
764
685
  // Worktree context — override the static CWD in the system prompt
765
686
  let worktreeBlock = "";
766
- const [{ getActiveWorktreeName, getWorktreeOriginalCwd }, { getActiveAutoWorktreeContext }] = await Promise.all([
767
- loadWorktreeCommandModule(),
768
- loadAutoWorktreeModule(),
769
- ]);
770
687
  const worktreeName = getActiveWorktreeName();
771
688
  const worktreeMainCwd = getWorktreeOriginalCwd();
772
689
  const autoWorktree = getActiveAutoWorktreeContext();
@@ -830,37 +747,9 @@ export default function (pi: ExtensionAPI) {
830
747
 
831
748
  // ── agent_end: auto-mode advancement or auto-start after discuss ───────────
832
749
  pi.on("agent_end", async (event, ctx: ExtensionContext) => {
833
- const [
834
- {
835
- isAutoActive,
836
- pauseAuto,
837
- getAutoDashboardData,
838
- getAutoModeStartModel,
839
- handleAgentEnd,
840
- },
841
- { checkAutoStartAfterDiscuss },
842
- {
843
- isTransientNetworkError,
844
- resolveModelWithFallbacksForUnit,
845
- getNextFallbackModel,
846
- },
847
- { classifyProviderError, pauseAutoForProviderError },
848
- ] = await Promise.all([
849
- loadAutoModule(),
850
- loadGuidedFlowModule(),
851
- loadPreferencesModule(),
852
- loadProviderErrorPauseModule(),
853
- ]);
854
-
855
- // Clean up quick-task branch if one just completed (#1269)
856
- try {
857
- const { cleanupQuickBranch } = await importExtensionModule<typeof import("./quick.js")>(import.meta.url, "./quick.js");
858
- cleanupQuickBranch();
859
- } catch { /* non-fatal */ }
860
-
861
750
  // If discuss phase just finished, start auto-mode
862
751
  if (checkAutoStartAfterDiscuss()) {
863
- depthVerifiedMilestones.clear();
752
+ depthVerificationDone = false;
864
753
  activeQueuePhase = false;
865
754
  return;
866
755
  }
@@ -868,6 +757,13 @@ export default function (pi: ExtensionAPI) {
868
757
  // If auto-mode is already running, advance to next unit
869
758
  if (!isAutoActive()) return;
870
759
 
760
+ // Fresh-session auto-mode intentionally aborts the previous session during
761
+ // cmdCtx.newSession(). Ignore that agent_end so we neither pause nor
762
+ // resolve the new unit with an event from the old session.
763
+ if (isSessionSwitchInFlight()) {
764
+ return;
765
+ }
766
+
871
767
  // If the agent was aborted (user pressed Escape) or hit a provider
872
768
  // error (fetch failure, rate limit, etc.), pause auto-mode instead of
873
769
  // advancing. This preserves the conversation so the user can inspect
@@ -1007,50 +903,46 @@ export default function (pi: ExtensionAPI) {
1007
903
  const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number")
1008
904
  ? lastMsg.retryAfterMs
1009
905
  : undefined;
1010
- let retryAfterMs = explicitRetryAfterMs ?? classification.suggestedDelayMs;
1011
-
1012
- // ── Escalating backoff for repeated transient errors ──────────────
1013
- // Each consecutive transient auto-resume doubles the delay. After
1014
- // MAX_TRANSIENT_AUTO_RESUMES consecutive failures, treat as permanent
1015
- // to avoid infinite rapid-fire retries (#1166).
1016
- let effectiveTransient = classification.isTransient;
1017
906
  if (classification.isTransient) {
1018
- consecutiveTransientErrors++;
1019
- if (consecutiveTransientErrors > MAX_TRANSIENT_AUTO_RESUMES) {
1020
- effectiveTransient = false;
1021
- ctx.ui.notify(
1022
- `${consecutiveTransientErrors} consecutive transient errors. Pausing indefinitely — resume manually with /gsd auto.`,
1023
- "error",
1024
- );
1025
- consecutiveTransientErrors = 0;
1026
- } else {
1027
- // Escalate: base delay × 2^(consecutive-1) → 30s, 60s, 120s, 240s, 480s
1028
- retryAfterMs = retryAfterMs * 2 ** (consecutiveTransientErrors - 1);
1029
- }
907
+ consecutiveTransientErrors += 1;
908
+ } else {
909
+ consecutiveTransientErrors = 0;
910
+ }
911
+ const baseRetryAfterMs = explicitRetryAfterMs ?? classification.suggestedDelayMs;
912
+ const retryAfterMs = classification.isTransient ? baseRetryAfterMs * 2 ** Math.max(0, consecutiveTransientErrors - 1) : baseRetryAfterMs;
913
+ const allowAutoResume = classification.isTransient
914
+ && consecutiveTransientErrors <= MAX_TRANSIENT_AUTO_RESUMES;
915
+
916
+ if (classification.isTransient && !allowAutoResume) {
917
+ ctx.ui.notify(
918
+ `Transient provider errors persisted after ${MAX_TRANSIENT_AUTO_RESUMES} auto-resume attempts. Pausing for manual review.`,
919
+ "warning",
920
+ );
1030
921
  }
1031
922
 
1032
923
  await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi), {
1033
924
  isRateLimit: classification.isRateLimit,
1034
- isTransient: effectiveTransient,
925
+ isTransient: allowAutoResume,
1035
926
  retryAfterMs,
1036
- resume: () => {
1037
- pi.sendMessage(
1038
- { customType: "gsd-auto-timeout-recovery", content: "Continue execution \u2014 provider error recovery delay elapsed.", display: false },
1039
- { triggerTurn: true },
1040
- );
1041
- },
927
+ resume: allowAutoResume
928
+ ? () => {
929
+ pi.sendMessage(
930
+ { customType: "gsd-auto-timeout-recovery", content: "Continue execution \u2014 provider error recovery delay elapsed.", display: false },
931
+ { triggerTurn: true },
932
+ );
933
+ }
934
+ : undefined,
1042
935
  });
1043
936
  return;
1044
937
  }
1045
938
 
1046
939
  try {
940
+ consecutiveTransientErrors = 0;
1047
941
  networkRetryCounters.clear(); // Clear network retry state on successful unit completion
1048
- consecutiveTransientErrors = 0; // Reset escalating backoff on success
1049
- await handleAgentEnd(ctx, pi);
942
+ resolveAgentEnd(event);
1050
943
  } catch (err) {
1051
- // Safety net: if handleAgentEnd throws despite its internal try-catch,
1052
- // ensure auto-mode stops gracefully instead of silently stalling (#381).
1053
- const message = getErrorMessage(err);
944
+ // Safety net: if resolveAgentEnd throws, ensure auto-mode stops gracefully (#381).
945
+ const message = err instanceof Error ? err.message : String(err);
1054
946
  ctx.ui.notify(
1055
947
  `Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`,
1056
948
  "error",
@@ -1065,11 +957,6 @@ export default function (pi: ExtensionAPI) {
1065
957
 
1066
958
  // ── session_before_compact ────────────────────────────────────────────────
1067
959
  pi.on("session_before_compact", async (_event, _ctx: ExtensionContext) => {
1068
- const [{ isAutoActive, isAutoPaused }, { deriveState }] = await Promise.all([
1069
- loadAutoModule(),
1070
- loadStateModule(),
1071
- ]);
1072
-
1073
960
  // Block compaction during auto-mode — each unit is a fresh session
1074
961
  // Also block during paused state — context is valuable for the user
1075
962
  if (isAutoActive() || isAutoPaused()) {
@@ -1116,31 +1003,12 @@ export default function (pi: ExtensionAPI) {
1116
1003
 
1117
1004
  // ── session_shutdown: save activity log on Ctrl+C / SIGTERM ─────────────
1118
1005
  pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
1119
- const [{ isParallelActive, shutdownParallel }, { isAutoActive, isAutoPaused, getAutoDashboardData }] =
1120
- await Promise.all([
1121
- loadParallelOrchestratorModule(),
1122
- loadAutoModule(),
1123
- ]);
1124
-
1125
1006
  if (isParallelActive()) {
1126
1007
  try {
1127
1008
  await shutdownParallel(process.cwd());
1128
1009
  } catch { /* best-effort */ }
1129
1010
  }
1130
1011
 
1131
- // Auto-commit dirty work in CLI-spawned worktrees so nothing is lost.
1132
- // The CLI sets GSD_CLI_WORKTREE when launched with -w.
1133
- const cliWorktree = process.env.GSD_CLI_WORKTREE;
1134
- if (cliWorktree) {
1135
- try {
1136
- const { autoCommitCurrentBranch } = await importExtensionModule<typeof import("./worktree.js")>(import.meta.url, "./worktree.js");
1137
- const msg = autoCommitCurrentBranch(process.cwd(), "session-end", cliWorktree);
1138
- if (msg) {
1139
- ctx.ui.notify(`Auto-committed worktree ${cliWorktree} before exit.`, "info");
1140
- }
1141
- } catch { /* best-effort */ }
1142
- }
1143
-
1144
1012
  if (!isAutoActive() && !isAutoPaused()) return;
1145
1013
 
1146
1014
  // Save the current session — the lock file stays on disk
@@ -1151,14 +1019,9 @@ export default function (pi: ExtensionAPI) {
1151
1019
  }
1152
1020
  });
1153
1021
 
1154
- // ── tool_call: block CONTEXT.md writes without depth verification ──
1155
- // Active during both discussion flows (pendingAutoStart set) and
1156
- // queue flows (activeQueuePhase set). For multi-milestone queue flows,
1157
- // each milestone must pass its own depth verification before its
1158
- // CONTEXT.md can be written.
1022
+ // ── tool_call: block CONTEXT.md writes during discussion without depth verification ──
1159
1023
  pi.on("tool_call", async (event) => {
1160
1024
  if (!isToolCallEventType("write", event)) return;
1161
- const { getDiscussionMilestoneId } = await loadGuidedFlowModule();
1162
1025
  const result = shouldBlockContextWrite(
1163
1026
  event.toolName,
1164
1027
  event.input.path,
@@ -1170,43 +1033,24 @@ export default function (pi: ExtensionAPI) {
1170
1033
  });
1171
1034
 
1172
1035
  // ── tool_result: persist discussion exchanges & detect depth gate ──────
1173
- // Handles both discussion flows and queue flows. For queue flows,
1174
- // depth verification question IDs may include milestone IDs
1175
- // (e.g., "depth_verification_M001") for per-milestone gating.
1176
1036
  pi.on("tool_result", async (event) => {
1177
1037
  if (event.toolName !== "ask_user_questions") return;
1178
1038
 
1179
- const { getDiscussionMilestoneId } = await loadGuidedFlowModule();
1180
1039
  const milestoneId = getDiscussionMilestoneId();
1181
- // Queue flows don't set pendingAutoStart, so milestoneId may be null.
1182
- // Depth gate detection still applies — it sets per-milestone flags.
1183
- const inQueue = activeQueuePhase;
1040
+ if (!milestoneId) return;
1184
1041
 
1185
1042
  const details = event.details as any;
1186
1043
  if (details?.cancelled || !details?.response) return;
1187
1044
 
1188
1045
  // ── Depth gate detection ──────────────────────────────────────────
1189
- // Supports two patterns:
1190
- // 1. "depth_verification" — wildcard, marks all milestones verified
1191
- // 2. "depth_verification_M001" — per-milestone verification
1192
1046
  const questions: any[] = (event.input as any)?.questions ?? [];
1193
1047
  for (const q of questions) {
1194
1048
  if (typeof q.id === "string" && q.id.includes("depth_verification")) {
1195
- // Extract milestone ID from question ID if present
1196
- const midMatch = q.id.match(/depth_verification[_-](M\d+(?:-[a-z0-9]{6})?)/i);
1197
- if (midMatch) {
1198
- depthVerifiedMilestones.add(midMatch[1]);
1199
- } else {
1200
- // Wildcard — all milestones verified (backward compat for single-milestone)
1201
- depthVerifiedMilestones.add("*");
1202
- }
1049
+ depthVerificationDone = true;
1203
1050
  break;
1204
1051
  }
1205
1052
  }
1206
1053
 
1207
- // Discussion persistence only applies when in a discussion flow with a known milestone
1208
- if (!milestoneId) return;
1209
-
1210
1054
  // ── Persist exchange to DISCUSSION.md ──────────────────────────────
1211
1055
  const basePath = process.cwd();
1212
1056
  const milestoneDir = resolveMilestonePath(basePath, milestoneId);
@@ -1252,13 +1096,11 @@ export default function (pi: ExtensionAPI) {
1252
1096
 
1253
1097
  // ── tool_execution_start/end: track in-flight tools for idle detection ──
1254
1098
  pi.on("tool_execution_start", async (event) => {
1255
- const { isAutoActive, markToolStart } = await loadAutoModule();
1256
1099
  if (!isAutoActive()) return;
1257
1100
  markToolStart(event.toolCallId);
1258
1101
  });
1259
1102
 
1260
1103
  pi.on("tool_execution_end", async (event) => {
1261
- const { markToolEnd } = await loadAutoModule();
1262
1104
  markToolEnd(event.toolCallId);
1263
1105
  });
1264
1106
  }
@@ -1273,7 +1115,6 @@ async function buildGuidedExecuteContextInjection(prompt: string, basePath: stri
1273
1115
  const resumeMatch = prompt.match(/Resume interrupted work\.[\s\S]*?slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i);
1274
1116
  if (resumeMatch) {
1275
1117
  const [, sliceId, milestoneId] = resumeMatch;
1276
- const { deriveState } = await loadStateModule();
1277
1118
  const state = await deriveState(basePath);
1278
1119
  if (
1279
1120
  state.activeMilestone?.id === milestoneId &&