gsd-pi 2.80.0-dev.e146beb20 → 2.80.0-dev.e6c48c3af

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 (218) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +59 -21
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +17 -2
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  8. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  9. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  10. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  11. package/dist/resources/extensions/gsd/auto.js +84 -5
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  13. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +75 -4
  15. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  16. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  18. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  19. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  20. package/dist/resources/extensions/gsd/git-service.js +36 -4
  21. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  22. package/dist/resources/extensions/gsd/guided-flow.js +33 -4
  23. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  24. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  25. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  26. package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
  27. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  29. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  31. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  32. package/dist/resources/extensions/gsd/quick.js +34 -2
  33. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  34. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  35. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  36. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  37. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  38. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  39. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  40. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  41. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/package.json +3 -3
  76. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  77. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  78. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  79. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  80. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  81. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  82. package/packages/native/tsconfig.tsbuildinfo +1 -1
  83. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  84. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +15 -0
  86. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  88. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -3
  90. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  92. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  94. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  96. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  98. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  100. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  104. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  106. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +12 -0
  108. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  111. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  120. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  122. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  126. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  127. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +18 -0
  128. package/packages/pi-coding-agent/src/core/agent-session.ts +14 -3
  129. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  130. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  131. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  132. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  133. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -0
  134. package/packages/pi-coding-agent/src/core/extensions/types.ts +12 -0
  135. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  137. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  139. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  140. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  141. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  142. package/packages/pi-tui/dist/tui.js +18 -8
  143. package/packages/pi-tui/dist/tui.js.map +1 -1
  144. package/packages/pi-tui/src/tui.ts +20 -8
  145. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  146. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  147. package/src/resources/extensions/gsd/auto/phases.ts +85 -35
  148. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  149. package/src/resources/extensions/gsd/auto/run-unit.ts +22 -2
  150. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -1
  151. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  152. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  153. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  154. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  155. package/src/resources/extensions/gsd/auto.ts +96 -4
  156. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  157. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  158. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +88 -4
  159. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  160. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  161. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  162. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  163. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  164. package/src/resources/extensions/gsd/git-service.ts +46 -8
  165. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  166. package/src/resources/extensions/gsd/guided-flow.ts +49 -4
  167. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  168. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  169. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  170. package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
  171. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  172. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  173. package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  174. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  175. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  176. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  177. package/src/resources/extensions/gsd/quick.ts +37 -2
  178. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +215 -1
  179. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  181. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +166 -4
  182. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  183. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  184. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  185. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  186. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  187. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  188. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  189. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  190. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
  191. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  192. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  193. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  194. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
  195. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  196. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  197. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  198. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  199. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  200. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  201. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  202. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  203. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  204. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  205. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  206. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  207. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  208. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  209. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  210. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  211. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  212. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  213. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  214. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  215. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  216. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  217. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_ssgManifest.js +0 -0
@@ -306,7 +306,7 @@ export async function dispatchDirectPhase(
306
306
  return;
307
307
  }
308
308
 
309
- const result = await ctx.newSession();
309
+ const result = await ctx.newSession({ cwd: dispatchBase });
310
310
  if (result.cancelled) {
311
311
  ctx.ui.notify("Session creation cancelled.", "warning");
312
312
  return;
@@ -35,6 +35,7 @@ import {
35
35
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
36
36
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
37
37
  import { composeContextModeInstructions, composeInlinedContext, type ArtifactResolver, type ContextModeRenderMode } from "./unit-context-composer.js";
38
+ import { readCompactionSnapshot } from "./compaction-snapshot.js";
38
39
  import { logWarning } from "./workflow-logger.js";
39
40
  import { inlineGraphSubgraph } from "./graph-context.js";
40
41
  import { buildExtractionStepsBlock } from "./commands-extract-learnings.js";
@@ -205,13 +206,28 @@ function renderContextModeForPrompt(
205
206
  });
206
207
  }
207
208
 
209
+ function renderContextModeBlockForPrompt(
210
+ unitType: string,
211
+ base: string,
212
+ renderMode: ContextModeRenderMode = "standalone",
213
+ ): string {
214
+ const contextMode = renderContextModeForPrompt(unitType, base, renderMode);
215
+ if (!contextMode) return "";
216
+ if (renderMode === "nested") return contextMode;
217
+
218
+ const snapshot = readCompactionSnapshot(base);
219
+ if (!snapshot?.trim()) return contextMode;
220
+
221
+ return `${contextMode}\n\n## Context Snapshot\nSource: \`.gsd/last-snapshot.md\`\n\n${snapshot.trimEnd()}`;
222
+ }
223
+
208
224
  function prependContextModeToBlock(
209
225
  unitType: string,
210
226
  base: string,
211
227
  block: string,
212
228
  renderMode: ContextModeRenderMode = "standalone",
213
229
  ): string {
214
- const contextMode = renderContextModeForPrompt(unitType, base, renderMode);
230
+ const contextMode = renderContextModeBlockForPrompt(unitType, base, renderMode);
215
231
  if (!contextMode) return block;
216
232
  if (!block.trim()) return contextMode;
217
233
  return `${contextMode}\n\n${block}`;
@@ -67,6 +67,60 @@ export {
67
67
 
68
68
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
69
69
 
70
+ export type ArtifactRecoveryDbRefreshResult =
71
+ | { ok: true }
72
+ | { ok: false; fatal: boolean; message: string; reason: string };
73
+
74
+ export function refreshRecoveryDbForArtifact(
75
+ unitType: string,
76
+ unitId: string,
77
+ ): ArtifactRecoveryDbRefreshResult {
78
+ if (unitType !== "plan-slice" && unitType !== "execute-task") return { ok: true };
79
+ if (!isDbAvailable()) return { ok: true };
80
+
81
+ if (!refreshOpenDatabaseFromDisk()) {
82
+ return {
83
+ ok: false,
84
+ fatal: unitType === "execute-task",
85
+ reason: `${unitType}-db-refresh-failed`,
86
+ message: `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed.`,
87
+ };
88
+ }
89
+
90
+ if (unitType !== "execute-task") return { ok: true };
91
+
92
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
93
+ if (!mid || !sid || !tid) {
94
+ return {
95
+ ok: false,
96
+ fatal: true,
97
+ reason: "execute-task-invalid-unit-id",
98
+ message: `Stuck recovery found execute-task ${unitId} artifacts, but the unit id could not be parsed for DB verification.`,
99
+ };
100
+ }
101
+
102
+ const task = getTask(mid, sid, tid);
103
+ if (!task) {
104
+ return {
105
+ ok: false,
106
+ fatal: true,
107
+ reason: "execute-task-artifact-db-missing",
108
+ message: `Stuck recovery found execute-task ${unitId} artifacts, but no matching DB task row exists after refresh.`,
109
+ };
110
+ }
111
+
112
+ if (!isClosedStatus(task.status)) {
113
+ return {
114
+ ok: false,
115
+ fatal: true,
116
+ reason: "execute-task-artifact-db-mismatch",
117
+ message: `Stuck recovery found execute-task ${unitId} artifacts, but the DB task status is still '${task.status}' after refresh.`,
118
+ };
119
+ }
120
+
121
+ return { ok: true };
122
+ }
123
+
70
124
  function hasCapturedWorkflowPrefs(base: string): boolean {
71
125
  const prefsPath = resolveExpectedArtifactPath("workflow-preferences", "WORKFLOW-PREFS", base);
72
126
  if (!prefsPath || !existsSync(prefsPath)) return false;
@@ -32,6 +32,7 @@ let _currentSigtermHandler: (() => void) | null = null;
32
32
  export function registerSigtermHandler(
33
33
  currentBasePath: string,
34
34
  previousHandler: (() => void) | null,
35
+ onSignalCleanup?: () => void,
35
36
  ): () => void {
36
37
  // Remove the explicitly-passed previous handler
37
38
  if (previousHandler) {
@@ -43,6 +44,12 @@ export function registerSigtermHandler(
43
44
  for (const sig of CLEANUP_SIGNALS) process.off(sig, _currentSigtermHandler);
44
45
  }
45
46
  const handler = () => {
47
+ try {
48
+ onSignalCleanup?.();
49
+ } catch {
50
+ void 0;
51
+ // Signal cleanup is best-effort; lock cleanup and process exit still run.
52
+ }
46
53
  clearLock(currentBasePath);
47
54
  releaseSessionLock(currentBasePath);
48
55
  process.exit(0);
@@ -71,14 +71,14 @@ export async function recoverTimedOutUnit(
71
71
  recovery: status,
72
72
  });
73
73
 
74
- const durableComplete = status.summaryExists && status.taskChecked && status.nextActionAdvanced;
74
+ const durableComplete = status.dbComplete || (status.summaryExists && status.taskChecked && status.nextActionAdvanced);
75
75
  if (durableComplete) {
76
76
  writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
77
77
  phase: "finalized",
78
78
  recovery: status,
79
79
  });
80
80
  ctx.ui.notify(
81
- `${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode. (attempt ${attemptNumber})`,
81
+ `${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed. Continuing auto-mode. (attempt ${attemptNumber})`,
82
82
  "info",
83
83
  );
84
84
  unitRecoveryCount.delete(recoveryKey);
@@ -59,6 +59,7 @@ import {
59
59
  isLockProcessAlive,
60
60
  formatCrashInfo,
61
61
  emitCrashRecoveredUnitEnd,
62
+ emitOpenUnitEndForUnit,
62
63
  } from "./crash-recovery.js";
63
64
  import {
64
65
  acquireSessionLock,
@@ -200,6 +201,8 @@ import {
200
201
  detectWorkingTreeActivity,
201
202
  } from "./auto-supervisor.js";
202
203
  import { isDbAvailable, getMilestone } from "./gsd-db.js";
204
+ import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
205
+ import { writeUnitRuntimeRecord } from "./unit-runtime.js";
203
206
  import { countPendingCaptures } from "./captures.js";
204
207
  import { CMUX_CHANNELS, type CmuxLogLevel } from "../shared/cmux-events.js";
205
208
  import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
@@ -242,6 +245,20 @@ import {
242
245
  type WorktreeResolverDeps,
243
246
  } from "./worktree-resolver.js";
244
247
  import { reorderForCaching } from "./prompt-ordering.js";
248
+ import { initTokenCounter } from "./token-counter.js";
249
+
250
+ // Warm the tiktoken encoder at extension startup so context-budget computations
251
+ // can use accurate token counts via countTokensSync without paying the load
252
+ // cost mid-prompt-build. Fire-and-forget — failure falls back to the
253
+ // provider-aware char-ratio estimator already used by getCharsPerToken().
254
+ // Catch rejections explicitly: an unhandled rejection at module-import time
255
+ // can destabilize startup before the engine logger is configured.
256
+ void initTokenCounter().catch((err) => {
257
+ logWarning(
258
+ "engine",
259
+ `token counter warm-up failed: ${err instanceof Error ? err.message : String(err)}`,
260
+ );
261
+ });
245
262
 
246
263
  // ─── Session State ─────────────────────────────────────────────────────────
247
264
 
@@ -293,11 +310,15 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
293
310
  * the DB is unavailable (e.g. fresh project before init) we skip registration
294
311
  * silently rather than blocking session start.
295
312
  */
296
- function registerAutoWorkerForSession(session: AutoSession): void {
313
+ function registerAutoWorkerForSession(
314
+ session: AutoSession,
315
+ projectRootOverride?: string,
316
+ ): void {
297
317
  if (session.workerId) return; // already registered (e.g. resume re-runs)
298
318
  try {
299
319
  const projectRootRealpath = normalizeRealPath(
300
- session.scope?.workspace.projectRoot
320
+ projectRootOverride
321
+ ?? session.scope?.workspace.projectRoot
301
322
  ?? (session.originalBasePath || session.basePath),
302
323
  );
303
324
  session.workerId = registerAutoWorker({ projectRootRealpath });
@@ -501,9 +522,54 @@ export {
501
522
  getBudgetEnforcementAction,
502
523
  } from "./auto-budget.js";
503
524
 
525
+ function closeOutSignalInterruptedUnit(currentBasePath: string): void {
526
+ const currentUnit = s.currentUnit;
527
+ if (!currentUnit) return;
528
+
529
+ const reason = "Auto-mode process received a termination signal";
530
+ const errorContext: ErrorContext = {
531
+ message: reason,
532
+ category: "aborted",
533
+ isTransient: false,
534
+ };
535
+ const basePath = s.basePath || currentBasePath;
536
+
537
+ try {
538
+ emitOpenUnitEndForUnit(basePath, currentUnit.type, currentUnit.id, "cancelled", errorContext);
539
+ } catch (err) {
540
+ logWarning("engine", `signal unit-end cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
541
+ }
542
+
543
+ try {
544
+ writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
545
+ phase: "crashed",
546
+ lastProgressAt: Date.now(),
547
+ lastProgressKind: "signal",
548
+ });
549
+ } catch (err) {
550
+ logWarning("engine", `signal runtime cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
551
+ }
552
+
553
+ try {
554
+ if (s.workerId) markLatestActiveForWorkerCanceled(s.workerId, "signal-exit");
555
+ } catch (err) {
556
+ logWarning("engine", `signal dispatch cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
557
+ }
558
+
559
+ try {
560
+ resolveAgentEndCancelled(errorContext);
561
+ } catch (err) {
562
+ logWarning("engine", `signal resolve cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
563
+ }
564
+ }
565
+
504
566
  /** Wrapper: register SIGTERM handler and store reference. */
505
567
  function registerSigtermHandler(currentBasePath: string): void {
506
- s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
568
+ s.sigtermHandler = _registerSigtermHandler(
569
+ currentBasePath,
570
+ s.sigtermHandler,
571
+ () => closeOutSignalInterruptedUnit(currentBasePath),
572
+ );
507
573
  }
508
574
 
509
575
  /** Wrapper: deregister SIGTERM handler and clear reference. */
@@ -968,6 +1034,8 @@ export async function stopAuto(
968
1034
  if (s.workerId) {
969
1035
  markWorkerStopping(s.workerId);
970
1036
  }
1037
+ s.workerId = null;
1038
+ s.milestoneLeaseToken = null;
971
1039
  } catch (e) {
972
1040
  debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
973
1041
  }
@@ -1105,6 +1173,21 @@ export async function stopAuto(
1105
1173
  debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
1106
1174
  }
1107
1175
 
1176
+ // Re-root the active command session/tool runtime after worktree teardown.
1177
+ // mergeAndExit restores process.cwd(), but AgentSession has already captured
1178
+ // its own cwd for tools and system prompt; refresh it before returning to the
1179
+ // user so follow-up commands do not target a removed milestone worktree.
1180
+ if (s.originalBasePath && ctx && s.cmdCtx) {
1181
+ try {
1182
+ const result = await s.cmdCtx.newSession({ cwd: s.basePath });
1183
+ if (result.cancelled) {
1184
+ logWarning("engine", "post-stop session re-root was cancelled", { file: "auto.ts", basePath: s.basePath });
1185
+ }
1186
+ } catch (err) {
1187
+ logWarning("engine", `post-stop session re-root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts", basePath: s.basePath });
1188
+ }
1189
+ }
1190
+
1108
1191
  // ── Step 8: Ledger notification ──
1109
1192
  try {
1110
1193
  const ledger = getLedger();
@@ -1971,6 +2054,10 @@ export async function startAuto(
1971
2054
  : new URL("../../../resource-loader.js", import.meta.url).href;
1972
2055
  const { initResources } = await import(resourceLoaderPath);
1973
2056
  initResources(agentDir);
2057
+ // initResources() uses synchronous fs APIs, so the prompt-template cache
2058
+ // can be primed immediately — no need for the legacy 1s setTimeout deferral.
2059
+ const { primeCache } = await import("./prompt-loader.js");
2060
+ primeCache();
1974
2061
  // Open the project DB before rebuild/derive so resume uses DB-backed
1975
2062
  // state instead of falling back to stale markdown parsing (#2940).
1976
2063
  await openProjectDbIfPresent(s.basePath);
@@ -2057,6 +2144,11 @@ export async function startAuto(
2057
2144
  buildResolver,
2058
2145
  };
2059
2146
 
2147
+ // Register the worker before bootstrap enters a milestone worktree.
2148
+ // This ensures enterMilestone can claim a lease and seed dispatch claims
2149
+ // for crash-recovery fidelity (#5405).
2150
+ registerAutoWorkerForSession(s, base);
2151
+
2060
2152
  const ready = await bootstrapAutoSession(
2061
2153
  s,
2062
2154
  ctx,
@@ -2241,7 +2333,7 @@ export async function dispatchHookUnit(
2241
2333
  return false;
2242
2334
  }
2243
2335
 
2244
- const result = await s.cmdCtx!.newSession();
2336
+ const result = await s.cmdCtx!.newSession({ cwd: s.basePath });
2245
2337
  if (result.cancelled) {
2246
2338
  await stopAuto(ctx, pi);
2247
2339
  return false;
@@ -153,9 +153,29 @@ export async function handleAgentEnd(
153
153
  if (maybeHandleEmptyIntentTurn(event, isAutoActive())) return;
154
154
 
155
155
  if (!isAutoActive()) return;
156
- if (isSessionSwitchInFlight()) return;
157
156
 
158
157
  const lastMsg = event.messages[event.messages.length - 1];
158
+ if (isSessionSwitchInFlight()) {
159
+ if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
160
+ const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
161
+ if (isUserInitiatedAbortMessage(rawErrorMsg)) {
162
+ resolveAgentEndCancelled({
163
+ message: rawErrorMsg,
164
+ category: "aborted",
165
+ isTransient: false,
166
+ });
167
+ }
168
+ } else if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
169
+ const content = "content" in lastMsg ? lastMsg.content : undefined;
170
+ const hasEmptyContent = Array.isArray(content) && content.length === 0;
171
+ const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
172
+ if (!hasEmptyContent || hasErrorMessage) {
173
+ resolveAgentEndCancelled(_buildAbortedPauseContext(lastMsg as { errorMessage?: unknown }));
174
+ }
175
+ }
176
+ return;
177
+ }
178
+
159
179
  if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
160
180
  // Empty content with aborted stopReason is a non-fatal agent stop (the LLM
161
181
  // chose to end without producing output). Only pause on genuine fatal aborts
@@ -6,23 +6,36 @@
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { ExtensionAPI } from "@gsd/pi-coding-agent";
8
8
 
9
+ async function loadContextModePreferences(baseDir: string) {
10
+ const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
11
+ import("../preferences.js"),
12
+ import("../workflow-logger.js"),
13
+ ]);
14
+ try {
15
+ return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
16
+ } catch (err) {
17
+ logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
18
+ return null;
19
+ }
20
+ }
21
+
9
22
  export function registerExecTools(pi: ExtensionAPI): void {
10
23
  pi.registerTool({
11
24
  name: "gsd_exec",
12
25
  label: "Exec (Sandboxed)",
13
26
  description:
14
- "Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
27
+ "Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
15
28
  ".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
16
29
  "this instead of reading many files or emitting large tool outputs — e.g. have the script " +
17
30
  "count/grep/summarize and log the finding. Enabled by default; opt out via " +
18
31
  "preferences.context_mode.enabled=false.",
19
32
  promptSnippet:
20
- "Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
33
+ "Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
21
34
  promptGuidelines: [
22
35
  "Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
23
36
  "Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
24
37
  "The digest is the last ~300 chars of stdout — size your log output accordingly.",
25
- "Need the full output? Read the stdout_path returned in details (file on local disk).",
38
+ "Need persisted output? Read the stdout_path returned in details (file on local disk).",
26
39
  ],
27
40
  parameters: Type.Object({
28
41
  runtime: Type.Union(
@@ -40,20 +53,11 @@ export function registerExecTools(pi: ExtensionAPI): void {
40
53
  ),
41
54
  }),
42
55
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
43
- const [{ executeGsdExec }, { loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
44
- import("../tools/exec-tool.js"),
45
- import("../preferences.js"),
46
- import("../workflow-logger.js"),
47
- ]);
48
- let prefs: ReturnType<typeof loadEffectiveGSDPreferences> | null = null;
49
- try {
50
- prefs = loadEffectiveGSDPreferences();
51
- } catch (err) {
52
- logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
53
- }
56
+ const { executeGsdExec } = await import("../tools/exec-tool.js");
57
+ const baseDir = process.cwd();
54
58
  return executeGsdExec(params as Parameters<typeof executeGsdExec>[0], {
55
- baseDir: process.cwd(),
56
- preferences: prefs?.preferences ?? null,
59
+ baseDir,
60
+ preferences: await loadContextModePreferences(baseDir),
57
61
  });
58
62
  },
59
63
  });
@@ -67,7 +71,7 @@ export function registerExecTools(pi: ExtensionAPI): void {
67
71
  promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
68
72
  promptGuidelines: [
69
73
  "Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
70
- "The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
74
+ "The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
71
75
  ],
72
76
  parameters: Type.Object({
73
77
  query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
@@ -81,8 +85,10 @@ export function registerExecTools(pi: ExtensionAPI): void {
81
85
  }),
82
86
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
83
87
  const { executeExecSearch } = await import("../tools/exec-search-tool.js");
88
+ const baseDir = process.cwd();
84
89
  return executeExecSearch(params as Parameters<typeof executeExecSearch>[0], {
85
- baseDir: process.cwd(),
90
+ baseDir,
91
+ preferences: await loadContextModePreferences(baseDir),
86
92
  });
87
93
  },
88
94
  });
@@ -102,8 +108,10 @@ export function registerExecTools(pi: ExtensionAPI): void {
102
108
  parameters: Type.Object({}),
103
109
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
104
110
  const { executeResume } = await import("../tools/resume-tool.js");
111
+ const baseDir = process.cwd();
105
112
  return executeResume(params as Parameters<typeof executeResume>[0], {
106
- baseDir: process.cwd(),
113
+ baseDir,
114
+ preferences: await loadContextModePreferences(baseDir),
107
115
  });
108
116
  },
109
117
  });
@@ -31,6 +31,13 @@ import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserAp
31
31
  let isFirstSession = true;
32
32
  let approvalQuestionAbortInFlight = false;
33
33
 
34
+ interface DeferredApprovalGate {
35
+ gateId: string;
36
+ basePath: string;
37
+ }
38
+
39
+ let deferredApprovalGate: DeferredApprovalGate | null = null;
40
+
34
41
  async function deriveGsdState(basePath: string) {
35
42
  const { deriveState } = await import("../state.js");
36
43
  return deriveState(basePath);
@@ -65,6 +72,66 @@ async function applyDisabledModelProviderPolicy(ctx: ExtensionContext): Promise<
65
72
  }
66
73
  }
67
74
 
75
+ /**
76
+ * Bridge `context_management.compaction_threshold_percent` from GSD preferences
77
+ * into the agent's runtime compaction settings (#5475). The preference is
78
+ * validated to (0.5, 0.95) at load time, but defense-in-depth normalization
79
+ * here protects against a stale or hand-edited prefs file. Calling with
80
+ * `undefined` clears any prior override so a removed preference does not leak.
81
+ */
82
+ async function applyCompactionThresholdOverride(ctx: ExtensionContext): Promise<void> {
83
+ try {
84
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
85
+ const prefs = loadEffectiveGSDPreferences();
86
+ const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
87
+ const value =
88
+ typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
89
+ ctx.setCompactionThresholdOverride(value);
90
+ } catch {
91
+ // Non-fatal: leave any existing override in place.
92
+ }
93
+ }
94
+
95
+ function clearDeferredApprovalGate(basePath?: string): void {
96
+ if (!basePath || deferredApprovalGate?.basePath === basePath) {
97
+ deferredApprovalGate = null;
98
+ }
99
+ }
100
+
101
+ function deferApprovalGate(gateId: string, basePath: string): void {
102
+ deferredApprovalGate = { gateId, basePath };
103
+ }
104
+
105
+ function activateDeferredApprovalGate(basePath: string): void {
106
+ if (deferredApprovalGate?.basePath !== basePath) return;
107
+ setPendingGate(deferredApprovalGate.gateId, basePath);
108
+ deferredApprovalGate = null;
109
+ }
110
+
111
+ function isContextDraftSummarySave(toolName: string, input: unknown): boolean {
112
+ if (toolName !== "gsd_summary_save" && toolName !== "summary_save") return false;
113
+ if (!input || typeof input !== "object") return false;
114
+ return (input as { artifact_type?: unknown }).artifact_type === "CONTEXT-DRAFT";
115
+ }
116
+
117
+ function shouldBlockDeferredApprovalTool(
118
+ toolName: string,
119
+ input: unknown,
120
+ basePath: string,
121
+ ): { block: boolean; reason?: string } {
122
+ if (deferredApprovalGate?.basePath !== basePath) return { block: false };
123
+ if (toolName === "ask_user_questions") return { block: false };
124
+ if (isContextDraftSummarySave(toolName, input)) return { block: false };
125
+ return {
126
+ block: true,
127
+ reason: [
128
+ `HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
129
+ `Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
130
+ `Wait for the user's answer before calling additional tools.`,
131
+ ].join(" "),
132
+ };
133
+ }
134
+
68
135
  export function resolveNotificationStoreBasePath(cwd: string = process.cwd()): string {
69
136
  return resolveWorktreeProjectRoot(cwd);
70
137
  }
@@ -120,9 +187,11 @@ export function registerHooks(
120
187
  resetWriteGateState(process.cwd());
121
188
  resetToolCallLoopGuard();
122
189
  approvalQuestionAbortInFlight = false;
190
+ clearDeferredApprovalGate();
123
191
  await resetAskUserQuestionsTurnCache();
124
192
  await syncServiceTierStatus(ctx);
125
193
  await applyDisabledModelProviderPolicy(ctx);
194
+ await applyCompactionThresholdOverride(ctx);
126
195
  // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
127
196
  const { isInAutoWorktree } = await import("../auto-worktree.js");
128
197
  if (!isInAutoWorktree(process.cwd())) {
@@ -168,10 +237,12 @@ export function registerHooks(
168
237
  initSessionNotifications(ctx);
169
238
  resetWriteGateState(process.cwd());
170
239
  resetToolCallLoopGuard();
240
+ clearDeferredApprovalGate();
171
241
  await resetAskUserQuestionsTurnCache();
172
242
  clearDiscussionFlowState(process.cwd());
173
243
  await syncServiceTierStatus(ctx);
174
244
  await applyDisabledModelProviderPolicy(ctx);
245
+ await applyCompactionThresholdOverride(ctx);
175
246
  // Skip MCP auto-prep when running inside an auto-worktree. The worktree
176
247
  // already has .mcp.json from createAutoWorktree, and re-running the writer
177
248
  // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
@@ -203,6 +274,7 @@ export function registerHooks(
203
274
  if (milestoneId) markDepthVerified(milestoneId, beforeAgentBasePath);
204
275
  clearPendingGate(beforeAgentBasePath);
205
276
  }
277
+ clearDeferredApprovalGate(beforeAgentBasePath);
206
278
 
207
279
  // GSD's own context injection (existing behavior — unchanged).
208
280
  const { buildBeforeAgentStartResult } = await import("./system-context.js");
@@ -253,7 +325,11 @@ export function registerHooks(
253
325
  resetToolCallLoopGuard();
254
326
  await resetAskUserQuestionsTurnCache();
255
327
  const { handleAgentEnd } = await import("./agent-end-recovery.js");
256
- await handleAgentEnd(pi, event, ctx);
328
+ try {
329
+ await handleAgentEnd(pi, event, ctx);
330
+ } finally {
331
+ activateDeferredApprovalGate(process.cwd());
332
+ }
257
333
  });
258
334
 
259
335
  // Squash-merge quick-task branch back to the original branch after the
@@ -355,15 +431,16 @@ export function registerHooks(
355
431
  if (!shouldPauseForUserApprovalQuestion(unitType, [event.message])) return;
356
432
 
357
433
  const gateId = approvalGateIdForUnit(unitType, unitId);
358
- if (gateId) setPendingGate(gateId, process.cwd());
434
+ if (gateId) deferApprovalGate(gateId, process.cwd());
359
435
 
360
436
  approvalQuestionAbortInFlight = true;
361
437
  ctx.ui.notify(
362
438
  `${unitType}${unitId ? ` ${unitId}` : ""} is waiting for your approval - pausing before more tool calls run.`,
363
439
  "info",
364
440
  );
365
- // The pending gate set above blocks subsequent non-read-only tool calls
366
- // via the tool_call hook below, so we do not abort the in-flight stream.
441
+ // The durable pending gate is activated at agent_end so same-turn
442
+ // CONTEXT-DRAFT persistence can finish after the text boundary streams.
443
+ // The tool_call hook below still blocks non-draft tools in this turn.
367
444
  // Aborting mid-stream eats the model's question text on external CLI
368
445
  // providers (Claude Code SDK) because lastTextContent isn't populated
369
446
  // from in-flight builder state — the user only ever sees "Claude Code
@@ -395,6 +472,13 @@ export function registerHooks(
395
472
  return { block: true, reason: loopCheck.reason };
396
473
  }
397
474
 
475
+ const deferredGateGuard = shouldBlockDeferredApprovalTool(
476
+ toolName,
477
+ event.input,
478
+ discussionBasePath,
479
+ );
480
+ if (deferredGateGuard.block) return deferredGateGuard;
481
+
398
482
  // ── Discussion gate enforcement: track pending gate questions ─────────
399
483
  // Only gate-shaped ask_user_questions calls should block execution.
400
484
  // The gate stays pending until the user selects the approval option.