gsd-pi 2.80.0-dev.0aaa4fc17 → 2.80.0-dev.1486c1899

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 (166) 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 +22 -15
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  7. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  8. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  9. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  10. package/dist/resources/extensions/gsd/auto.js +52 -4
  11. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  12. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  14. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  15. package/dist/resources/extensions/gsd/guided-flow.js +19 -4
  16. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  18. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  19. package/dist/resources/extensions/gsd/quick.js +34 -2
  20. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  21. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  22. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  23. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  24. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  25. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/package.json +3 -3
  60. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  62. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  63. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  64. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  65. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  66. package/packages/native/tsconfig.tsbuildinfo +1 -1
  67. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  68. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  71. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  73. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  75. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  77. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  79. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  80. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  81. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  83. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  85. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  87. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  89. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  92. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  94. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  101. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  107. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  108. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  109. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  110. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  111. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  112. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  113. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  114. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  115. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  116. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  117. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  118. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  119. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  120. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  121. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  122. package/packages/pi-tui/dist/tui.js +18 -8
  123. package/packages/pi-tui/dist/tui.js.map +1 -1
  124. package/packages/pi-tui/src/tui.ts +20 -8
  125. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  126. package/src/resources/extensions/gsd/auto/phases.ts +28 -20
  127. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  128. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  129. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  130. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  131. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  132. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  133. package/src/resources/extensions/gsd/auto.ts +60 -3
  134. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  135. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  136. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  137. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  138. package/src/resources/extensions/gsd/guided-flow.ts +32 -4
  139. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  140. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  141. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  142. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  143. package/src/resources/extensions/gsd/quick.ts +37 -2
  144. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  145. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  146. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
  147. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  148. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  149. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  150. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  151. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  152. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  153. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  154. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  155. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  156. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  157. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  158. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  159. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  160. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  161. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  162. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  163. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  164. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  165. /package/dist/web/standalone/.next/static/{YRK7-Es93MwpIn3VL35K0 → O-2rnxSJki_W-2fkK3zIj}/_buildManifest.js +0 -0
  166. /package/dist/web/standalone/.next/static/{YRK7-Es93MwpIn3VL35K0 → O-2rnxSJki_W-2fkK3zIj}/_ssgManifest.js +0 -0
@@ -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";
@@ -293,11 +296,15 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
293
296
  * the DB is unavailable (e.g. fresh project before init) we skip registration
294
297
  * silently rather than blocking session start.
295
298
  */
296
- function registerAutoWorkerForSession(session: AutoSession): void {
299
+ function registerAutoWorkerForSession(
300
+ session: AutoSession,
301
+ projectRootOverride?: string,
302
+ ): void {
297
303
  if (session.workerId) return; // already registered (e.g. resume re-runs)
298
304
  try {
299
305
  const projectRootRealpath = normalizeRealPath(
300
- session.scope?.workspace.projectRoot
306
+ projectRootOverride
307
+ ?? session.scope?.workspace.projectRoot
301
308
  ?? (session.originalBasePath || session.basePath),
302
309
  );
303
310
  session.workerId = registerAutoWorker({ projectRootRealpath });
@@ -501,9 +508,54 @@ export {
501
508
  getBudgetEnforcementAction,
502
509
  } from "./auto-budget.js";
503
510
 
511
+ function closeOutSignalInterruptedUnit(currentBasePath: string): void {
512
+ const currentUnit = s.currentUnit;
513
+ if (!currentUnit) return;
514
+
515
+ const reason = "Auto-mode process received a termination signal";
516
+ const errorContext: ErrorContext = {
517
+ message: reason,
518
+ category: "aborted",
519
+ isTransient: false,
520
+ };
521
+ const basePath = s.basePath || currentBasePath;
522
+
523
+ try {
524
+ emitOpenUnitEndForUnit(basePath, currentUnit.type, currentUnit.id, "cancelled", errorContext);
525
+ } catch (err) {
526
+ logWarning("engine", `signal unit-end cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
527
+ }
528
+
529
+ try {
530
+ writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
531
+ phase: "crashed",
532
+ lastProgressAt: Date.now(),
533
+ lastProgressKind: "signal",
534
+ });
535
+ } catch (err) {
536
+ logWarning("engine", `signal runtime cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
537
+ }
538
+
539
+ try {
540
+ if (s.workerId) markLatestActiveForWorkerCanceled(s.workerId, "signal-exit");
541
+ } catch (err) {
542
+ logWarning("engine", `signal dispatch cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
543
+ }
544
+
545
+ try {
546
+ resolveAgentEndCancelled(errorContext);
547
+ } catch (err) {
548
+ logWarning("engine", `signal resolve cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
549
+ }
550
+ }
551
+
504
552
  /** Wrapper: register SIGTERM handler and store reference. */
505
553
  function registerSigtermHandler(currentBasePath: string): void {
506
- s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
554
+ s.sigtermHandler = _registerSigtermHandler(
555
+ currentBasePath,
556
+ s.sigtermHandler,
557
+ () => closeOutSignalInterruptedUnit(currentBasePath),
558
+ );
507
559
  }
508
560
 
509
561
  /** Wrapper: deregister SIGTERM handler and clear reference. */
@@ -2057,6 +2109,11 @@ export async function startAuto(
2057
2109
  buildResolver,
2058
2110
  };
2059
2111
 
2112
+ // Register the worker before bootstrap enters a milestone worktree.
2113
+ // This ensures enterMilestone can claim a lease and seed dispatch claims
2114
+ // for crash-recovery fidelity (#5405).
2115
+ registerAutoWorkerForSession(s, base);
2116
+
2060
2117
  const ready = await bootstrapAutoSession(
2061
2118
  s,
2062
2119
  ctx,
@@ -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
  });
@@ -65,6 +65,26 @@ async function applyDisabledModelProviderPolicy(ctx: ExtensionContext): Promise<
65
65
  }
66
66
  }
67
67
 
68
+ /**
69
+ * Bridge `context_management.compaction_threshold_percent` from GSD preferences
70
+ * into the agent's runtime compaction settings (#5475). The preference is
71
+ * validated to (0.5, 0.95) at load time, but defense-in-depth normalization
72
+ * here protects against a stale or hand-edited prefs file. Calling with
73
+ * `undefined` clears any prior override so a removed preference does not leak.
74
+ */
75
+ async function applyCompactionThresholdOverride(ctx: ExtensionContext): Promise<void> {
76
+ try {
77
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
78
+ const prefs = loadEffectiveGSDPreferences();
79
+ const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
80
+ const value =
81
+ typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
82
+ ctx.setCompactionThresholdOverride(value);
83
+ } catch {
84
+ // Non-fatal: leave any existing override in place.
85
+ }
86
+ }
87
+
68
88
  export function resolveNotificationStoreBasePath(cwd: string = process.cwd()): string {
69
89
  return resolveWorktreeProjectRoot(cwd);
70
90
  }
@@ -123,6 +143,7 @@ export function registerHooks(
123
143
  await resetAskUserQuestionsTurnCache();
124
144
  await syncServiceTierStatus(ctx);
125
145
  await applyDisabledModelProviderPolicy(ctx);
146
+ await applyCompactionThresholdOverride(ctx);
126
147
  // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
127
148
  const { isInAutoWorktree } = await import("../auto-worktree.js");
128
149
  if (!isInAutoWorktree(process.cwd())) {
@@ -172,6 +193,7 @@ export function registerHooks(
172
193
  clearDiscussionFlowState(process.cwd());
173
194
  await syncServiceTierStatus(ctx);
174
195
  await applyDisabledModelProviderPolicy(ctx);
196
+ await applyCompactionThresholdOverride(ctx);
175
197
  // Skip MCP auto-prep when running inside an auto-worktree. The worktree
176
198
  // already has .mcp.json from createAutoWorktree, and re-running the writer
177
199
  // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
@@ -359,6 +359,47 @@ export function markCanceled(dispatchId: number, reason: string): void {
359
359
  ).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
360
360
  }
361
361
 
362
+ /**
363
+ * Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
364
+ * a worker when the process is exiting before the normal loop can settle it.
365
+ */
366
+ export function markLatestActiveForWorkerCanceled(workerId: string, reason: string): boolean {
367
+ if (!isDbAvailable()) return false;
368
+ const now = new Date().toISOString();
369
+ const db = _getAdapter()!;
370
+ const result = transaction(() => {
371
+ return db.prepare(
372
+ `UPDATE unit_dispatches
373
+ SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
374
+ WHERE id = (
375
+ SELECT id FROM unit_dispatches
376
+ WHERE worker_id = :worker_id
377
+ AND status IN ('pending','claimed','running')
378
+ ORDER BY id DESC
379
+ LIMIT 1
380
+ )`,
381
+ ).run({
382
+ ":ended_at": now,
383
+ ":reason": reason,
384
+ ":worker_id": workerId,
385
+ });
386
+ });
387
+ const changes =
388
+ typeof (result as { changes?: unknown }).changes === "number"
389
+ ? (result as { changes: number }).changes
390
+ : 0;
391
+ if (changes <= 0) return false;
392
+ insertAuditEvent({
393
+ eventId: randomUUID(),
394
+ traceId: workerId,
395
+ category: "orchestration",
396
+ type: "dispatch-canceled",
397
+ ts: now,
398
+ payload: { workerId, reason },
399
+ });
400
+ return true;
401
+ }
402
+
362
403
  /**
363
404
  * Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
364
405
  * callers to compute attempt_n and by detect-stuck.ts (B3) to consult
@@ -87,6 +87,34 @@ import { logWarning } from "./workflow-logger.js";
87
87
  import { deleteRuntimeKv } from "./db/runtime-kv.js";
88
88
  import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
89
89
 
90
+ type AutoStartOptions = Parameters<typeof startAutoDetached>[4];
91
+ type AutoStartLauncher = typeof startAutoDetached;
92
+
93
+ function scheduleAutoStartAfterIdle(
94
+ ctx: ExtensionCommandContext,
95
+ pi: ExtensionAPI,
96
+ basePath: string,
97
+ verboseMode: boolean,
98
+ options?: AutoStartOptions,
99
+ launch: AutoStartLauncher = startAutoDetached,
100
+ ): void {
101
+ const waitForIdle =
102
+ typeof (ctx as { waitForIdle?: unknown }).waitForIdle === "function"
103
+ ? ctx.waitForIdle.bind(ctx)
104
+ : async () => {};
105
+ void waitForIdle()
106
+ .then(() => {
107
+ setTimeout(() => launch(ctx, pi, basePath, verboseMode, options), 0);
108
+ })
109
+ .catch((err) => {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ ctx.ui.notify(`Auto-start failed while waiting for the prior turn to settle: ${message}`, "error");
112
+ logWarning("guided", `auto-start idle wait failed: ${message}`);
113
+ });
114
+ }
115
+
116
+ export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
117
+
90
118
  // ─── Scope-based validator wrappers ──────────────────────────────────────────
91
119
  // These thin wrappers accept a MilestoneScope so callers that already hold a
92
120
  // pinned scope never have to re-derive (basePath, milestoneId) separately.
@@ -446,7 +474,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
446
474
 
447
475
  if (!hasPendingDeepStage(prefs, entry.basePath)) {
448
476
  pendingDeepProjectSetupMap.delete(entry.basePath);
449
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
477
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
450
478
  return true;
451
479
  }
452
480
 
@@ -479,7 +507,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
479
507
  entry.ctx.ui.notify(result.reason, result.level);
480
508
  } else if (hasPendingDeepStage(prefs, entry.basePath)) {
481
509
  pendingDeepProjectSetupMap.delete(entry.basePath);
482
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
510
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
483
511
  return true;
484
512
  }
485
513
  return false;
@@ -487,7 +515,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
487
515
 
488
516
  if (!USER_DRIVEN_DEEP_SETUP_UNITS.has(result.unitType)) {
489
517
  pendingDeepProjectSetupMap.delete(entry.basePath);
490
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
518
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
491
519
  return true;
492
520
  }
493
521
 
@@ -693,7 +721,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
693
721
 
694
722
  pendingAutoStartMap.delete(basePath);
695
723
  ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
696
- startAutoDetached(ctx, pi, basePath, false, { step });
724
+ scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step });
697
725
  return true;
698
726
  }
699
727
 
@@ -32,7 +32,7 @@ export interface ContextManagementConfig {
32
32
  /**
33
33
  * Opt-in tool-output sandboxing for sub-sessions. When enabled, the gsd_exec
34
34
  * MCP tool runs scripts in an isolated subprocess and returns only a short
35
- * digest to the calling agent's context window; full stdout/stderr persist
35
+ * digest to the calling agent's context window; capped stdout/stderr persist
36
36
  * in the project memory store and can be retrieved by id later.
37
37
  *
38
38
  * Inspired by mksglu/context-mode (Elastic License 2.0). This is an
@@ -16,7 +16,7 @@ Dispatch ALL slices simultaneously using the `subagent` tool in **parallel mode*
16
16
 
17
17
  1. Call `subagent` with `tasks: [...]` containing one entry per slice below
18
18
  2. Wait for ALL subagents to complete
19
- 3. Verify each slice's RESEARCH file was written (check the `.gsd/{{mid}}/` directory)
19
+ 3. Verify each slice's RESEARCH file was written (check `.gsd/milestones/{{mid}}/slices/<slice-id>/`)
20
20
  4. If a subagent failed to write its RESEARCH file, retry it **once** individually
21
21
  5. If it fails a second time, write a partial RESEARCH file for that slice with a `## BLOCKER` section explaining the failure — do NOT retry again
22
22
  6. Report which slices completed research and which (if any) needed a blocker note
@@ -17,11 +17,7 @@ You are executing a GSD quick task — a lightweight, focused unit of work outsi
17
17
  5. Verify your work:
18
18
  - Run tests if applicable.
19
19
  - Verify both happy path and failure modes for non-trivial changes.
20
- 6. Commit your changes atomically:
21
- - Use conventional commit messages (feat:, fix:, refactor:, etc.)
22
- - Stage only relevant files — never commit secrets or runtime files.
23
- - Commit logical units separately if the task involves distinct changes.
24
- - Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.
20
+ 6. {{commitInstruction}}
25
21
  7. Write a brief summary to `{{summaryPath}}`:
26
22
  - Quick tasks operate outside the milestone/slice/task DB structure, so `gsd_summary_save` (which requires a `milestone_id`) cannot be used here. Write the file directly.
27
23
 
@@ -27,13 +27,13 @@ Roadmap, slice summaries, assessments, requirements, decisions, and project cont
27
27
  Call `subagent` with `tasks: [...]` containing ALL THREE reviewers simultaneously:
28
28
 
29
29
  **Reviewer A - Requirements Coverage**
30
- Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Read `.gsd/{{milestoneId}}/REQUIREMENTS.md` or equivalent. For each requirement, check slice SUMMARY files in `.gsd/{{milestoneId}}/` and mark COVERED, PARTIAL, or MISSING. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
30
+ Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Read `.gsd/REQUIREMENTS.md` or use the inlined requirements context. For each requirement, check slice SUMMARY files under `.gsd/milestones/{{milestoneId}}/slices/` and mark COVERED, PARTIAL, or MISSING. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
31
31
 
32
32
  **Reviewer B - Cross-Slice Integration**
33
33
  Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Read `{{roadmapPath}}` and find the boundary map (produces/consumes contracts). For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
34
34
 
35
35
  **Reviewer C - Assessment & Acceptance Criteria**
36
- Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Read `.gsd/{{milestoneId}}/CONTEXT.md` for criteria. Check slice ASSESSMENT files. Verify each criterion maps to a passing assessment or clear SUMMARY evidence. Then review inlined milestone verification classes. For each non-empty planned class, output table: Class | Planned Check | Evidence | Verdict. Use the exact class names `Contract`, `Integration`, `Operational`, and `UAT` whenever those classes are present. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered, NEEDS-ATTENTION if gaps exist."
36
+ Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Read `.gsd/milestones/{{milestoneId}}/{{milestoneId}}-CONTEXT.md` for criteria. Check slice SUMMARY/UAT files under `.gsd/milestones/{{milestoneId}}/slices/`. Verify each criterion maps to passing evidence. Then review inlined milestone verification classes. For each non-empty planned class, output table: Class | Planned Check | Evidence | Verdict. Use the exact class names `Contract`, `Integration`, `Operational`, and `UAT` whenever those classes are present. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered, NEEDS-ATTENTION if gaps exist."
37
37
 
38
38
  ### Step 2 - Synthesize Findings
39
39
 
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
13
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
14
- import { join } from "node:path";
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
14
+ import { isAbsolute, join, relative } from "node:path";
15
15
  import { loadPrompt } from "./prompt-loader.js";
16
16
  import { gsdRoot } from "./paths.js";
17
17
  import { GitServiceImpl, runGit } from "./git-service.js";
@@ -77,6 +77,40 @@ function ensureQuickDir(basePath: string, taskNum: number, slug: string): string
77
77
  return taskDir;
78
78
  }
79
79
 
80
+ function isPathInside(parent: string, child: string): boolean {
81
+ const rel = relative(parent, child);
82
+ return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
83
+ }
84
+
85
+ function isExternalGsdRoot(basePath: string, root: string): boolean {
86
+ try {
87
+ return !isPathInside(realpathSync(basePath), realpathSync(root));
88
+ } catch {
89
+ return !isPathInside(basePath, root);
90
+ }
91
+ }
92
+
93
+ export function buildQuickCommitInstruction(basePath: string, root: string): string {
94
+ const externalState = isExternalGsdRoot(basePath, root);
95
+ if (externalState) {
96
+ return [
97
+ "Commit repo changes atomically, but do not stage or commit `.gsd/quick/...`:",
98
+ " - `.gsd/` resolves outside this git repository, so Git cannot stage quick-task summary files from the project repo.",
99
+ " - Write the quick summary file directly at the requested path; that file is persisted by GSD external state.",
100
+ " - Stage and commit only implementation/test/docs files that live inside the repository.",
101
+ " - If the task only writes quick-task research/summary files and no repository files changed, do not run `git commit`; report that there was nothing in the project repo to commit.",
102
+ ].join("\n");
103
+ }
104
+
105
+ return [
106
+ "Commit your changes atomically:",
107
+ " - Use conventional commit messages (feat:, fix:, refactor:, etc.)",
108
+ " - Stage only relevant files — never commit secrets or runtime files.",
109
+ " - Commit logical units separately if the task involves distinct changes.",
110
+ " - Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.",
111
+ ].join("\n");
112
+ }
113
+
80
114
  function quickReturnStatePath(basePath: string): string {
81
115
  return join(gsdRoot(basePath), "runtime", "quick-return.json");
82
116
  }
@@ -246,6 +280,7 @@ export async function handleQuick(
246
280
  taskDir: taskDirRel,
247
281
  branch: actualBranch,
248
282
  summaryPath,
283
+ commitInstruction: buildQuickCommitInstruction(basePath, root),
249
284
  date,
250
285
  taskNum: String(taskNum),
251
286
  slug,
@@ -10,6 +10,8 @@ import {
10
10
  _resetPendingResolve,
11
11
  _hasPendingResolveForTest,
12
12
  _setActiveSession,
13
+ _setSessionSwitchInFlight,
14
+ _consumePendingSwitchCancellation,
13
15
  isSessionSwitchInFlight,
14
16
  } from "../auto/resolve.js";
15
17
  import { runUnit } from "../auto/run-unit.js";
@@ -206,6 +208,28 @@ test("runUnit returns cancelled when session creation fails", async () => {
206
208
  assert.equal(pi.calls.length, 0);
207
209
  });
208
210
 
211
+ test("runUnit clears queued switch cancellation when session creation fails", async () => {
212
+ _resetPendingResolve();
213
+
214
+ const ctx = makeMockCtx();
215
+ const pi = makeMockPi();
216
+ const s = makeMockSession({
217
+ newSessionThrows: "connection refused",
218
+ onNewSessionStart: () => {
219
+ resolveAgentEndCancelled({
220
+ message: "Claude Code process aborted by user",
221
+ category: "aborted",
222
+ isTransient: false,
223
+ });
224
+ },
225
+ });
226
+
227
+ const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
228
+
229
+ assert.equal(result.status, "cancelled");
230
+ assert.equal(_consumePendingSwitchCancellation(), null);
231
+ });
232
+
209
233
  test("runUnit returns cancelled when session creation times out", async () => {
210
234
  _resetPendingResolve();
211
235
 
@@ -221,6 +245,34 @@ test("runUnit returns cancelled when session creation times out", async () => {
221
245
  assert.equal(pi.calls.length, 0);
222
246
  });
223
247
 
248
+ test("runUnit consumes a cancellation queued during session switch before dispatch", async () => {
249
+ _resetPendingResolve();
250
+
251
+ const ctx = makeMockCtx();
252
+ const pi = makeMockPi();
253
+ let cancellationQueued = false;
254
+ const s = makeMockSession({
255
+ newSessionDelayMs: 10,
256
+ onNewSessionStart: () => {
257
+ setTimeout(() => {
258
+ cancellationQueued = !resolveAgentEndCancelled({
259
+ message: "Claude Code process aborted by user",
260
+ category: "aborted",
261
+ isTransient: false,
262
+ });
263
+ }, 0);
264
+ },
265
+ });
266
+
267
+ const result = await runUnit(ctx, pi, s, "plan-slice", "M009/S01", "prompt");
268
+
269
+ assert.equal(cancellationQueued, true);
270
+ assert.equal(result.status, "cancelled");
271
+ assert.equal(result.errorContext?.category, "aborted");
272
+ assert.equal(result.errorContext?.message, "Claude Code process aborted by user");
273
+ assert.equal(pi.calls.length, 0, "queued switch cancellation must prevent prompt dispatch");
274
+ });
275
+
224
276
  test("runUnit keeps the session-switch guard across a late newSession settlement", async () => {
225
277
  _resetPendingResolve();
226
278
  mock.timers.enable();
@@ -2089,6 +2141,25 @@ test("resolveAgentEndCancelled without args produces no errorContext field", asy
2089
2141
  assert.equal(resolved.errorContext, undefined, "errorContext must not be present when no args passed");
2090
2142
  });
2091
2143
 
2144
+ test("resolveAgentEndCancelled queues cancellation that arrives during session switch", () => {
2145
+ _resetPendingResolve();
2146
+
2147
+ _setSessionSwitchInFlight(true);
2148
+ const resolved = resolveAgentEndCancelled({
2149
+ message: "Claude Code process aborted by user",
2150
+ category: "aborted",
2151
+ isTransient: false,
2152
+ });
2153
+
2154
+ assert.equal(resolved, false);
2155
+ const pending = _consumePendingSwitchCancellation();
2156
+ assert.ok(pending?.errorContext, "queued cancellation should preserve errorContext");
2157
+ assert.equal(pending.errorContext.category, "aborted");
2158
+ assert.equal(pending.errorContext.message, "Claude Code process aborted by user");
2159
+ assert.equal(_consumePendingSwitchCancellation(), null);
2160
+ _resetPendingResolve();
2161
+ });
2162
+
2092
2163
  // ─── #1571: artifact verification retry ──────────────────────────────────────
2093
2164
 
2094
2165
  test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
8
+ import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
9
9
  import { resolveMilestoneFile } from "../paths.ts";
10
10
  import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, getMilestoneCommitAttributionShas } from "../gsd-db.ts";
11
11
  import { clearParseCache } from "../files.ts";
@@ -175,6 +175,19 @@ test("resolveExpectedArtifactPath returns correct path for all slice-level types
175
175
  }
176
176
  });
177
177
 
178
+ test("refreshRecoveryDbForArtifact treats missing execute-task DB rows as fatal mismatches", () => {
179
+ makeTmpProject();
180
+
181
+ const result = refreshRecoveryDbForArtifact("execute-task", "M001/S01/T01");
182
+
183
+ assert.deepEqual(result, {
184
+ ok: false,
185
+ fatal: true,
186
+ reason: "execute-task-artifact-db-missing",
187
+ message: "Stuck recovery found execute-task M001/S01/T01 artifacts, but no matching DB task row exists after refresh.",
188
+ });
189
+ });
190
+
178
191
  // ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
179
192
 
180
193
  test("diagnoseExpectedArtifact returns description for known types", () => {
@@ -121,3 +121,14 @@ test('executeResume: reports friendly empty state when no snapshot exists', () =
121
121
  cleanup(base);
122
122
  }
123
123
  });
124
+
125
+ test('executeResume: returns disabled error when context_mode.enabled=false', () => {
126
+ const base = freshBase();
127
+ try {
128
+ const result = executeResume({}, { baseDir: base, preferences: { context_mode: { enabled: false } } });
129
+ assert.equal(result.isError, true);
130
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
131
+ } finally {
132
+ cleanup(base);
133
+ }
134
+ });