@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd

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/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
  3. package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
  4. package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
  5. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  8. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  9. package/dist/resources/extensions/gsd/auto.js +10 -3
  10. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
  12. package/dist/resources/extensions/gsd/gsd-db.js +52 -0
  13. package/dist/resources/extensions/gsd/migrate-external.js +13 -4
  14. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
  15. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  16. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  17. package/dist/resources/extensions/gsd/unit-registry.js +27 -2
  18. package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
  19. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  22. package/dist/web/standalone/.next/build-manifest.json +2 -2
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.html +1 -1
  41. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  48. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  50. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  51. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  52. package/integrations/hermes/CONTEXT.md +47 -0
  53. package/integrations/hermes/README.md +21 -0
  54. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  55. package/integrations/hermes/docs/setup.md +4 -0
  56. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  57. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  58. package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
  59. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  60. package/integrations/hermes/plugin.yaml +4 -0
  61. package/integrations/hermes/tests/test_config.py +6 -0
  62. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  63. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  64. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  65. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  66. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  67. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  68. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  69. package/package.json +1 -1
  70. package/packages/cloud-mcp-gateway/package.json +2 -2
  71. package/packages/contracts/package.json +1 -1
  72. package/packages/daemon/dist/session-manager.js +1 -1
  73. package/packages/daemon/dist/session-manager.js.map +1 -1
  74. package/packages/daemon/package.json +4 -4
  75. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  76. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  77. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  78. package/packages/gsd-agent-core/package.json +5 -5
  79. package/packages/gsd-agent-modes/package.json +7 -7
  80. package/packages/mcp-server/dist/server.d.ts +18 -5
  81. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/server.js +46 -5
  83. package/packages/mcp-server/dist/server.js.map +1 -1
  84. package/packages/mcp-server/dist/session-manager.js +1 -1
  85. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  86. package/packages/mcp-server/package.json +4 -4
  87. package/packages/native/package.json +1 -1
  88. package/packages/pi-agent-core/package.json +1 -1
  89. package/packages/pi-ai/package.json +1 -1
  90. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  91. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  95. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  96. package/packages/pi-coding-agent/package.json +7 -7
  97. package/packages/pi-tui/package.json +2 -2
  98. package/packages/rpc-client/package.json +2 -2
  99. package/pkg/package.json +1 -1
  100. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
  101. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
  102. package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
  103. package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
  104. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  105. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  106. package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
  107. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  108. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  109. package/src/resources/extensions/gsd/auto.ts +9 -3
  110. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  111. package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
  112. package/src/resources/extensions/gsd/gsd-db.ts +72 -0
  113. package/src/resources/extensions/gsd/migrate-external.ts +14 -4
  114. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
  115. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  116. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  117. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  118. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  119. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  120. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
  121. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  123. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
  124. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
  125. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  126. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  127. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  128. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  129. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
  130. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  131. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  132. package/src/resources/extensions/gsd/unit-registry.ts +34 -2
  133. package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
  134. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
  135. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- b92be6014924117b
1
+ f30abebf080db301
@@ -21,7 +21,7 @@ import { resolveWorkflowQuestionToolSurface } from "../gsd/question-transport.js
21
21
  import { buildProjectGsdMcpServers, ensureProjectWorkflowMcpConfig } from "../gsd/mcp-project-config.js";
22
22
  import { loadProjectGSDPreferences } from "../gsd/preferences.js";
23
23
  import { markToolStart, markToolEnd } from "../gsd/auto.js";
24
- import { markInteractiveElicitationStart, markInteractiveElicitationEnd, } from "../gsd/auto-tool-tracking.js";
24
+ import { markInteractiveElicitationStart, markInteractiveElicitationEnd, isInteractiveElicitationInFlight, } from "../gsd/auto-tool-tracking.js";
25
25
  import { discoverBrowserMcpServerName, discoverMcpServers, discoverMcpServerNames, discoverUserMcpServerNames, discoverWorkflowMcpServerName, computeMcpDisallowedTools, } from "../gsd/mcp-filter.js";
26
26
  import { RUN_UAT_CLAUDE_NATIVE_TOOL_NAMES, RUN_UAT_FORBIDDEN_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES, resolveToolPresentationPlan } from "../gsd/tool-presentation-plan.js";
27
27
  import { getUnitToolSurfaceContract } from "../gsd/unit-tool-contracts.js";
@@ -51,6 +51,27 @@ export function resolveClaudeCodeCwd(options) {
51
51
  const OTHER_OPTION_LABEL = "None of the above";
52
52
  /** Regex pattern that identifies field names and descriptions that should be treated as sensitive/secure inputs. */
53
53
  const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
54
+ /** Mirrors @opengsd/mcp-server's ask_user_questions host elicitation timeout. */
55
+ const WORKFLOW_ELICIT_TIMEOUT_MS = 10 * 60 * 1000;
56
+ /** Close the local form just before the server drops the MCP elicitation request. */
57
+ const WORKFLOW_ELICIT_TIMEOUT_SKEW_MS = 1_000;
58
+ export const CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS = Math.max(1, WORKFLOW_ELICIT_TIMEOUT_MS - WORKFLOW_ELICIT_TIMEOUT_SKEW_MS);
59
+ const ELICITATION_EXPIRED_NOTICE = "Question expired before you answered - the form was closed and no answers were sent.";
60
+ const activeAskUserQuestionElicitationTimeoutAborters = new Set();
61
+ function isAskUserQuestionsToolName(toolName) {
62
+ const name = String(toolName ?? "");
63
+ return name === "ask_user_questions" || name.endsWith("__ask_user_questions");
64
+ }
65
+ function isAskUserQuestionsTimedOutResult(toolCall, result) {
66
+ return isAskUserQuestionsToolName(toolCall.name) && result.details?.timed_out === true;
67
+ }
68
+ function abortActiveAskUserQuestionElicitationsForTimeout() {
69
+ if (!isInteractiveElicitationInFlight())
70
+ return;
71
+ for (const abort of [...activeAskUserQuestionElicitationTimeoutAborters]) {
72
+ abort();
73
+ }
74
+ }
54
75
  // ---------------------------------------------------------------------------
55
76
  // Stream factory
56
77
  // ---------------------------------------------------------------------------
@@ -1238,17 +1259,40 @@ export function createClaudeCodeElicitationHandler(ui) {
1238
1259
  // it does not tear down the very elicitation that IS the human boundary
1239
1260
  // (#cc-elicitation-self-cancel). It clears in the finally on every exit.
1240
1261
  const elicId = "cc-elicit-" + (request.id ?? `${Date.now()}-${nextElicitationSeq()}`);
1262
+ const formAbortController = new AbortController();
1263
+ let expiryNoticeShown = false;
1264
+ const abortExpiredForm = () => {
1265
+ if (formAbortController.signal.aborted)
1266
+ return;
1267
+ formAbortController.abort();
1268
+ if (!expiryNoticeShown) {
1269
+ expiryNoticeShown = true;
1270
+ ui.notify(ELICITATION_EXPIRED_NOTICE, "warning");
1271
+ }
1272
+ };
1273
+ const forwardSdkAbort = () => {
1274
+ if (!formAbortController.signal.aborted)
1275
+ formAbortController.abort();
1276
+ };
1277
+ if (signal.aborted) {
1278
+ forwardSdkAbort();
1279
+ }
1280
+ else {
1281
+ signal.addEventListener("abort", forwardSdkAbort, { once: true });
1282
+ }
1283
+ const expiryTimer = setTimeout(abortExpiredForm, CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS);
1241
1284
  markInteractiveElicitationStart();
1242
1285
  markToolStart(elicId, "ask_user_questions");
1286
+ activeAskUserQuestionElicitationTimeoutAborters.add(abortExpiredForm);
1243
1287
  try {
1244
- const interviewResult = await showInterviewRound(questions, { signal, overlay: true }, { ui }).catch(() => undefined);
1288
+ const interviewResult = await showInterviewRound(questions, { signal: formAbortController.signal, overlay: true }, { ui }).catch(() => undefined);
1245
1289
  if (interviewResult === undefined) {
1246
1290
  // `await` so the dialog human-wait stays inside try/finally and the
1247
1291
  // in-flight guard is held until the dialog resolves. Without it,
1248
1292
  // `finally` runs the moment the promise is created and the fallback
1249
1293
  // wait runs with zero in-flight tools — reintroducing the
1250
1294
  // self-cancel on this path (Bugbot #1c00624d).
1251
- return await promptElicitationWithDialogs(request, questions, ui, signal);
1295
+ return await promptElicitationWithDialogs(request, questions, ui, formAbortController.signal);
1252
1296
  }
1253
1297
  if (Object.keys(interviewResult.answers).length === 0) {
1254
1298
  // A system/host teardown (compaction, session_switch, true
@@ -1265,6 +1309,9 @@ export function createClaudeCodeElicitationHandler(ui) {
1265
1309
  };
1266
1310
  }
1267
1311
  finally {
1312
+ clearTimeout(expiryTimer);
1313
+ signal.removeEventListener("abort", forwardSdkAbort);
1314
+ activeAskUserQuestionElicitationTimeoutAborters.delete(abortExpiredForm);
1268
1315
  markToolEnd(elicId);
1269
1316
  markInteractiveElicitationEnd();
1270
1317
  }
@@ -2052,6 +2099,9 @@ async function pumpSdkMessages(model, context, options, stream) {
2052
2099
  // Push synthetic completion events with result attached so the
2053
2100
  // chat-controller can update pending ToolExecutionComponents.
2054
2101
  if (block.type === "toolCall") {
2102
+ if (isAskUserQuestionsTimedOutResult(block, extResult)) {
2103
+ abortActiveAskUserQuestionElicitationsForTimeout();
2104
+ }
2055
2105
  if (suppressDuplicateUnavailable) {
2056
2106
  delete block.externalResult;
2057
2107
  stream.push({
@@ -2082,9 +2132,13 @@ async function pumpSdkMessages(model, context, options, stream) {
2082
2132
  emittedExternalToolResultIds.add(block.id);
2083
2133
  }
2084
2134
  else if (block.type === "serverToolUse") {
2135
+ const toolCall = serverToolUseToToolCallLike(block);
2136
+ if (isAskUserQuestionsTimedOutResult(toolCall, extResult)) {
2137
+ abortActiveAskUserQuestionElicitationsForTimeout();
2138
+ }
2085
2139
  try {
2086
2140
  await onExternalToolResult?.({
2087
- toolCall: serverToolUseToToolCallLike(block),
2141
+ toolCall,
2088
2142
  result: extResult,
2089
2143
  });
2090
2144
  }
@@ -314,6 +314,19 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
314
314
  try {
315
315
  let taskIds = null;
316
316
  let dbPrimary = false;
317
+ const planContent = readFileSync(absPath, "utf-8");
318
+ let parsedTaskIds = null;
319
+ const getParsedTaskIds = () => {
320
+ if (parsedTaskIds)
321
+ return parsedTaskIds;
322
+ parsedTaskIds = parseLegacyPlan(planContent).tasks.map((t) => t.id);
323
+ return parsedTaskIds;
324
+ };
325
+ const tasksBlockMatch = planContent.match(/<tasks>([\s\S]*?)<\/tasks>/i);
326
+ const tasksBlock = tasksBlockMatch?.[1] ?? "";
327
+ const hasEmbeddedTaskEntries = tasksBlock.length > 0 &&
328
+ (/^\s*- \[[xX ]\] \*\*T\d+/m.test(tasksBlock) ||
329
+ /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(tasksBlock));
317
330
  if (isDbAvailable()) {
318
331
  const refreshed = refreshWorkflowDatabaseFromDisk();
319
332
  if (refreshed) {
@@ -325,18 +338,17 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
325
338
  }
326
339
  }
327
340
  if (!taskIds) {
328
- const planContent = readFileSync(absPath, "utf-8");
329
341
  const hasCheckboxTask = /^\s*- \[[xX ]\] \*\*T\d+/m.test(planContent);
330
342
  const hasHeadingTask = /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
331
343
  if (!hasCheckboxTask && !hasHeadingTask) {
332
344
  logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
333
345
  return false;
334
346
  }
335
- const plan = parseLegacyPlan(planContent);
336
- if (plan.tasks.length > 0)
337
- taskIds = plan.tasks.map((t) => t.id);
347
+ const parsedIds = getParsedTaskIds();
348
+ if (parsedIds.length > 0)
349
+ taskIds = parsedIds;
338
350
  }
339
- if (taskIds && taskIds.length > 0) {
351
+ if (taskIds && taskIds.length > 0 && !hasEmbeddedTaskEntries) {
340
352
  const tasksDir = join(dirname(absPath), "tasks");
341
353
  if (existsSync(tasksDir)) {
342
354
  for (const tid of taskIds) {
@@ -37,62 +37,51 @@ export async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
37
37
  return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
38
38
  }
39
39
  /**
40
- * Run a milestone merge surrounded by preflight stash + always-on postflight
41
- * pop. The previous code popped the stash only after a successful merge, which
42
- * leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
43
- * leaving the user's pre-merge working tree silently stashed away after a
44
- * merge-conflict or other merge error. This helper restores the stash on
45
- * every exit path, then surfaces the merge or stash failure (in priority
46
- * order) as the loop's stop reason.
40
+ * Run a milestone merge through Worktree Lifecycle's guarded merge option,
41
+ * which surrounds the inner merge with preflight stash + always-on postflight
42
+ * pop. The previous closeout code popped the stash only after a successful
43
+ * merge, which leaked `gsd-preflight-stash:M00x:*` entries whenever
44
+ * `mergeAndExit` threw leaving the user's pre-merge working tree silently
45
+ * stashed away after a merge-conflict or other merge error. Lifecycle now
46
+ * restores the stash on every attempted merge path, then this adapter surfaces
47
+ * the merge or stash failure (in priority order) as the loop's stop reason.
47
48
  *
48
49
  * Returns a `break` action when auto-mode must stop, or `null` when the merge
49
50
  * succeeded and the stash (if any) was restored cleanly.
50
51
  */
51
52
  export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, options = {}) {
52
53
  const { ctx, pi, s, deps } = ic;
53
- const preflight = deps.preflightCleanRoot(s.originalBasePath || s.basePath, milestoneId, ctx.ui.notify.bind(ctx.ui));
54
- if (preflight.blocked) {
55
- const reason = preflight.blockedReason === "unmerged-conflicts"
54
+ const projectRoot = s.originalBasePath || s.basePath;
55
+ const mergeResult = deps.lifecycle.exitMilestone(milestoneId, {
56
+ merge: true,
57
+ guardedMerge: {
58
+ projectRoot,
59
+ preflightCleanRoot: deps.preflightCleanRoot,
60
+ postflightPopStash: deps.postflightPopStash,
61
+ },
62
+ }, ctx.ui);
63
+ if (mergeResult.ok) {
64
+ await markMilestoneMergedAndRebuild(s);
65
+ return null;
66
+ }
67
+ if (mergeResult.reason === "postflight-stash-restore-failed") {
68
+ await markMilestoneMergedAndRebuild(s);
69
+ }
70
+ if (mergeResult.reason === "preflight-dirty-overlap" || mergeResult.reason === "preflight-unmerged-conflicts") {
71
+ const reason = mergeResult.reason === "preflight-unmerged-conflicts"
56
72
  ? `Pre-merge unresolved Git conflicts block milestone ${milestoneId}`
57
73
  : `Pre-merge dirty working tree overlaps milestone ${milestoneId}`;
58
74
  await deps.stopAuto(ctx, pi, reason, {
59
75
  preserveCompletedMilestoneBranch: true,
60
76
  preserveCloseoutTranscript: options.preserveCloseoutTranscript,
61
77
  });
62
- return {
63
- action: "break",
64
- reason: preflight.blockedReason === "unmerged-conflicts"
65
- ? "preflight-unmerged-conflicts"
66
- : "preflight-dirty-overlap",
67
- };
68
- }
69
- let mergeError = null;
70
- const exitResult = deps.lifecycle.exitMilestone(milestoneId, { merge: true }, ctx.ui);
71
- if (exitResult.ok) {
72
- s.milestoneMergedInPhases = true;
73
- try {
74
- const projectRoot = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
75
- const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
76
- await rebuildMarkdownProjectionsFromDb(projectRoot);
77
- }
78
- catch (err) {
79
- logWarning("engine", `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`);
80
- }
81
- }
82
- else {
83
- mergeError = exitResult.cause ?? new Error(`exit ${exitResult.reason}`);
84
- }
85
- // Always attempt to restore the stashed working tree, even on merge error.
86
- // postflightPopStash itself does not throw; failures surface via the
87
- // PostflightResult.needsManualRecovery flag.
88
- let stashResult = null;
89
- if (preflight.stashPushed) {
90
- stashResult = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
78
+ return { action: "break", reason: mergeResult.reason };
91
79
  }
92
80
  // Merge failure takes priority over stash recovery — the merge is the
93
81
  // authoritative gate. If the stash also needed manual recovery, the user
94
82
  // already saw the postflightPopStash notify above.
95
- if (mergeError) {
83
+ if (mergeResult.reason === "merge-conflict") {
84
+ const mergeError = mergeResult.cause;
96
85
  if (mergeError instanceof MergeConflictError) {
97
86
  // A merge conflict is a recoverable human checkpoint, not an
98
87
  // infrastructure failure — the user resolves the conflict and runs
@@ -109,6 +98,9 @@ export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, option
109
98
  });
110
99
  return { action: "break", reason: "merge-conflict" };
111
100
  }
101
+ }
102
+ if (mergeResult.reason === "merge-failed" || mergeResult.reason === "merge-conflict") {
103
+ const mergeError = mergeResult.cause;
112
104
  logError("engine", "Milestone merge failed with non-conflict error", {
113
105
  milestone: milestoneId,
114
106
  error: String(mergeError),
@@ -125,11 +117,22 @@ export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, option
125
117
  });
126
118
  return { action: "break", reason: "merge-failed" };
127
119
  }
128
- if (stashResult) {
129
- return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
120
+ if (mergeResult.postflight) {
121
+ return stopOnPostflightRecoveryNeeded(ic, mergeResult.postflight, milestoneId);
130
122
  }
131
123
  return null;
132
124
  }
125
+ async function markMilestoneMergedAndRebuild(s) {
126
+ s.milestoneMergedInPhases = true;
127
+ try {
128
+ const rebuildBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
129
+ const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
130
+ await rebuildMarkdownProjectionsFromDb(rebuildBasePath);
131
+ }
132
+ catch (err) {
133
+ logWarning("engine", `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`);
134
+ }
135
+ }
133
136
  export async function _runMilestoneMergeOnceWithStashRestore(ic, milestoneId, options = {}) {
134
137
  if (ic.s.milestoneMergedInPhases) {
135
138
  debugLog("autoLoop", {
@@ -280,6 +280,15 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
280
280
  files: leak.files.map((file) => ({ path: file.path, status: file.status })),
281
281
  });
282
282
  ctx.ui.notify(message, "error");
283
+ try {
284
+ deps.checkpointWorkflowDatabase?.();
285
+ }
286
+ catch (err) {
287
+ debugLog("autoLoop", {
288
+ phase: "root-write-leak-checkpoint-failed",
289
+ error: err instanceof Error ? err.message : String(err),
290
+ });
291
+ }
283
292
  await deps.stopAuto(ctx, pi, "Root-write leak during isolated auto-mode", {
284
293
  preserveCompletedMilestoneBranch: true,
285
294
  });
@@ -560,7 +560,7 @@ export class AutoOrchestrator {
560
560
  return new GitServiceImpl(basePath, gitConfig);
561
561
  },
562
562
  worktreeProjection: new WorktreeStateProjection(),
563
- mergeMilestoneToMain: createMilestoneMergeTransaction(mergeMilestoneToMain),
563
+ mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
564
564
  });
565
565
  }
566
566
  rebuildScope(rawPath, milestoneId) {
@@ -6,7 +6,7 @@ import { debugLog } from "../debug-logger.js";
6
6
  import { logWarning } from "../workflow-logger.js";
7
7
  import { resolveAutoSupervisorConfig } from "../preferences.js";
8
8
  import { readUnitRuntimeRecord } from "../unit-runtime.js";
9
- import { consumeAutoWakeup } from "./schedule-wakeup.js";
9
+ import { clearAutoWakeup, consumeAutoWakeup } from "./schedule-wakeup.js";
10
10
  import { applyUnitSkillVisibility } from "../skill-scope.js";
11
11
  const UNIT_FAILSAFE_BUFFER_MS = 30_000;
12
12
  const UNIT_FAILSAFE_RECHECK_MS = 30_000;
@@ -192,7 +192,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
192
192
  const UNIT_HARD_TIMEOUT_MS = Math.max(UNIT_FAILSAFE_BUFFER_MS, ((supervisor.hard_timeout_minutes ?? 30) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS);
193
193
  const freshProgressMs = Math.max(UNIT_FAILSAFE_BUFFER_MS, ((supervisor.idle_timeout_minutes ?? 10) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS);
194
194
  let unitTimeoutHandle;
195
- let result;
195
+ let result = { status: "cancelled" };
196
196
  try {
197
197
  const awaitAgentEndWithTimeout = (agentEndPromise) => {
198
198
  const timeoutResult = new Promise((resolve) => {
@@ -297,6 +297,9 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
297
297
  finally {
298
298
  if (unitTimeoutHandle)
299
299
  clearTimeout(unitTimeoutHandle);
300
+ if (result.status !== "completed") {
301
+ clearAutoWakeup(s.basePath, unitType, unitId);
302
+ }
300
303
  ctx.ui.setWorkingMessage?.(undefined);
301
304
  setVisibleSkills?.(undefined);
302
305
  }
@@ -13,6 +13,9 @@ export function consumeAutoWakeup(basePath, unitType, unitId) {
13
13
  pendingWakeups.delete(key);
14
14
  return wakeup;
15
15
  }
16
+ export function clearAutoWakeup(basePath, unitType, unitId) {
17
+ pendingWakeups.delete(wakeupKey(basePath, unitType, unitId));
18
+ }
16
19
  export function _resetAutoWakeupsForTest() {
17
20
  pendingWakeups.clear();
18
21
  }
@@ -68,7 +68,7 @@ import { MILESTONE_ID_RE } from "./milestone-ids.js";
68
68
  import { updateProgressWidget as _updateProgressWidget, setCompletionProgressWidget, setAutoOutcomeWidget, setAutoActiveStatus, updateSliceProgressCache, clearSliceProgressCache, unitVerb, } from "./auto-dashboard.js";
69
69
  import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
70
70
  import { isDbAvailable, getMilestone, getMilestoneSlices, } from "./gsd-db.js";
71
- import { closeWorkflowDatabase } from "./db-workspace.js";
71
+ import { checkpointWorkflowDatabase, closeWorkflowDatabase } from "./db-workspace.js";
72
72
  import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
73
73
  import { writeUnitRuntimeRecord } from "./unit-runtime.js";
74
74
  import { countPendingCaptures } from "./captures.js";
@@ -1102,6 +1102,12 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
1102
1102
  const installTerminalCloseoutOutcome = completionStopRequested && preserveCloseoutTranscript;
1103
1103
  const preserveCompletionSurface = completionStopRequested || preserveCloseoutTranscript;
1104
1104
  s.completionStopInProgress = preserveCompletionSurface;
1105
+ try {
1106
+ checkpointWorkflowDatabase();
1107
+ }
1108
+ catch (e) {
1109
+ debugLog("stop-checkpoint-failed", { error: e instanceof Error ? e.message : String(e) });
1110
+ }
1105
1111
  playNotificationBell("stop", loadedPreferences?.notifications);
1106
1112
  // #4764 — telemetry: record the exit reason, isolation mode, whether an auto
1107
1113
  // worktree was active, and whether the current milestone was merged before
@@ -1732,7 +1738,7 @@ export function buildWorktreeLifecycleDeps() {
1732
1738
  // C4 (#5627) — GitServiceImpl constructor → gitServiceFactory
1733
1739
  //
1734
1740
  // Final WorktreeLifecycleDeps shape: 3 fields (gitServiceFactory,
1735
- // worktreeProjection, mergeMilestoneToMain transaction runner). Down from 18 at slice-7
1741
+ // worktreeProjection, mergeMilestone transaction runner). Down from 18 at slice-7
1736
1742
  // closure.
1737
1743
  return {
1738
1744
  gitServiceFactory: (basePath) => {
@@ -1740,7 +1746,7 @@ export function buildWorktreeLifecycleDeps() {
1740
1746
  return new GitServiceImpl(basePath, gitConfig);
1741
1747
  },
1742
1748
  worktreeProjection: new WorktreeStateProjection(),
1743
- mergeMilestoneToMain: createMilestoneMergeTransaction(mergeMilestoneToMain),
1749
+ mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
1744
1750
  };
1745
1751
  }
1746
1752
  function buildLifecycle() {
@@ -1783,6 +1789,7 @@ function buildLoopDeps(pi, ctx) {
1783
1789
  stopAuto,
1784
1790
  pauseAuto,
1785
1791
  clearUnitTimeout,
1792
+ checkpointWorkflowDatabase,
1786
1793
  updateProgressWidget,
1787
1794
  ...cmux,
1788
1795
  handleLostSessionLock: (ctx, lockStatus) => {
@@ -3,19 +3,63 @@
3
3
  import { Type } from "@sinclair/typebox";
4
4
  import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
5
5
  import { scheduleAutoWakeup } from "../auto/schedule-wakeup.js";
6
+ import { logWarning } from "../workflow-logger.js";
6
7
  import { resolveCtxCwd } from "./dynamic-tools.js";
7
8
  const MAX_WAKEUP_DELAY_SECONDS = 24 * 60 * 60;
9
+ const INTERACTIVE_WAKEUP_CUSTOM_TYPE = "gsd-schedule-wakeup";
10
+ // One pending interactive wakeup per session, keyed by base path — the same
11
+ // scoping auto-mode uses for its wakeup map (see `wakeupKey`). Re-arming cancels
12
+ // only that session's prior timer, so repeated polling never stacks overlapping
13
+ // wakeups, and concurrent projects in one host process don't cancel each other.
14
+ const pendingInteractiveWakeups = new Map();
15
+ function scheduleInteractiveWakeup(pi, key, delaySeconds, prompt, reason) {
16
+ const existing = pendingInteractiveWakeups.get(key);
17
+ if (existing)
18
+ clearTimeout(existing);
19
+ const handle = setTimeout(() => {
20
+ if (pendingInteractiveWakeups.get(key) === handle) {
21
+ pendingInteractiveWakeups.delete(key);
22
+ }
23
+ try {
24
+ void Promise.resolve(pi.sendMessage({
25
+ customType: INTERACTIVE_WAKEUP_CUSTOM_TYPE,
26
+ content: prompt,
27
+ display: true,
28
+ details: { delaySeconds, reason },
29
+ }, { triggerTurn: true })).catch((error) => {
30
+ logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
31
+ });
32
+ }
33
+ catch (error) {
34
+ logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
35
+ }
36
+ }, delaySeconds * 1000);
37
+ pendingInteractiveWakeups.set(key, handle);
38
+ if (typeof handle === "object" &&
39
+ handle !== null &&
40
+ "unref" in handle &&
41
+ typeof handle.unref === "function") {
42
+ handle.unref();
43
+ }
44
+ }
45
+ export function _resetInteractiveWakeupsForTest() {
46
+ for (const handle of pendingInteractiveWakeups.values()) {
47
+ clearTimeout(handle);
48
+ }
49
+ pendingInteractiveWakeups.clear();
50
+ }
8
51
  export function registerScheduleWakeupTool(pi) {
9
52
  pi.registerTool({
10
53
  name: "ScheduleWakeup",
11
54
  label: "Schedule Wakeup",
12
- description: "In GSD auto-mode, pause the current unit and continue the same session after a delay. " +
13
- "Use this for long external processes that need polling without ending the unit as no-artifact.",
14
- promptSnippet: "Schedule a same-session auto-mode wakeup after a delay.",
55
+ description: "Schedule a delayed continuation turn. In GSD auto-mode, continue the current unit in the same session; " +
56
+ "outside auto-mode, start a new triggered turn with the supplied wakeup prompt.",
57
+ promptSnippet: "Schedule a wakeup prompt after a delay.",
15
58
  promptGuidelines: [
16
59
  "Use ScheduleWakeup at the end of an execute-task turn when waiting for a long external process.",
17
60
  "Include a prompt that says exactly what external state to check next and what artifact to write when done.",
18
61
  "Re-arm ScheduleWakeup on each polling turn if the external process is still running.",
62
+ "Outside auto-mode, use ScheduleWakeup when the user asks you to check back or poll later.",
19
63
  ],
20
64
  parameters: Type.Object({
21
65
  delaySeconds: Type.Number({
@@ -34,22 +78,27 @@ export function registerScheduleWakeupTool(pi) {
34
78
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
35
79
  const dash = getAutoRuntimeSnapshot();
36
80
  const currentUnit = dash.currentUnit;
37
- const basePath = dash.basePath || resolveCtxCwd(ctx);
81
+ const delaySeconds = Math.max(1, Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)));
82
+ const reason = params.reason ?? "";
38
83
  if (!dash.active || !currentUnit) {
84
+ const interactiveKey = dash.basePath || resolveCtxCwd(ctx);
85
+ scheduleInteractiveWakeup(pi, interactiveKey, delaySeconds, params.prompt, reason);
39
86
  return {
40
- content: [{ type: "text", text: "ScheduleWakeup is only available during an active GSD auto-mode unit." }],
41
- details: { operation: "schedule_wakeup", error: "auto_mode_inactive" },
42
- isError: true,
87
+ content: [{
88
+ type: "text",
89
+ text: `Wakeup scheduled for ${delaySeconds}s. GSD will start a new turn with the wakeup prompt.`,
90
+ }],
91
+ details: { operation: "schedule_wakeup", delaySeconds, mode: "interactive" },
43
92
  };
44
93
  }
45
- const delaySeconds = Math.max(1, Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)));
94
+ const basePath = dash.basePath || resolveCtxCwd(ctx);
46
95
  scheduleAutoWakeup({
47
96
  basePath,
48
97
  unitType: currentUnit.type,
49
98
  unitId: currentUnit.id,
50
99
  delayMs: delaySeconds * 1000,
51
100
  prompt: params.prompt,
52
- reason: params.reason ?? "",
101
+ reason,
53
102
  createdAt: Date.now(),
54
103
  });
55
104
  return {