@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.9f86580

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 (219) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  4. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  5. package/dist/resources/extensions/browser-tools/index.js +29 -2
  6. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +45 -3
  8. package/dist/resources/extensions/gsd/auto/session.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  13. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  14. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  15. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  16. package/dist/resources/extensions/gsd/auto.js +26 -4
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  21. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  23. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  24. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  25. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  26. package/dist/resources/extensions/gsd/guided-flow.js +93 -108
  27. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  28. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  29. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  30. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
  33. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  34. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  35. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  36. package/dist/resources/extensions/gsd/tool-contract.js +6 -1
  37. package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
  38. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  39. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
  40. package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
  42. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  43. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  73. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/package.json +1 -1
  81. package/packages/daemon/package.json +4 -4
  82. package/packages/gsd-agent-core/package.json +5 -5
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  86. package/packages/gsd-agent-modes/package.json +7 -7
  87. package/packages/mcp-server/package.json +3 -3
  88. package/packages/native/package.json +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -3
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  92. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  93. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  94. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  95. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  97. package/packages/pi-agent-core/dist/types.d.ts +3 -1
  98. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  99. package/packages/pi-agent-core/dist/types.js.map +1 -1
  100. package/packages/pi-agent-core/package.json +1 -1
  101. package/packages/pi-ai/dist/models.generated.d.ts +157 -18
  102. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  103. package/packages/pi-ai/dist/models.generated.js +159 -36
  104. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  105. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  107. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  108. package/packages/pi-ai/package.json +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  110. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  113. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  116. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  118. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  120. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  123. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  124. package/packages/pi-coding-agent/package.json +7 -7
  125. package/packages/pi-tui/package.json +1 -1
  126. package/packages/rpc-client/package.json +2 -2
  127. package/pkg/package.json +1 -1
  128. package/scripts/install/handoff.js +16 -3
  129. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  130. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  131. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  132. package/src/resources/extensions/browser-tools/index.ts +36 -5
  133. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  134. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  135. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  136. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  137. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  138. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  139. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
  140. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  141. package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  143. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  144. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  145. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  146. package/src/resources/extensions/gsd/auto.ts +28 -4
  147. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
  148. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
  149. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  150. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  151. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  152. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  153. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  154. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  155. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  156. package/src/resources/extensions/gsd/guided-flow.ts +128 -135
  157. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  158. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  159. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  160. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  161. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  162. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
  164. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  165. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  166. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  167. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  168. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  169. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  170. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  171. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  172. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  173. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  174. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  175. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  176. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  177. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  178. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  179. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  181. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  182. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  183. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  184. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  185. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  186. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  187. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +7 -1
  188. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  189. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  190. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  191. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  193. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  194. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  195. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  196. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
  197. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  198. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  199. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  200. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +56 -0
  201. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  202. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
  205. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  206. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tool-contract.ts +7 -1
  208. package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
  209. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  210. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
  211. package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
  212. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
  213. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  214. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  215. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  217. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  218. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  219. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
@@ -26,6 +26,7 @@ import { MILESTONE_ID_RE } from "./milestone-ids.js";
26
26
  import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
27
27
  import { classifyProject } from "./detection.js";
28
28
  import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchForceReset, nativeBranchExists, nativeDiffNumstat, nativeUpdateRef, nativeIsAncestor, nativeMergeAbort, nativeWorktreeList, nativeLsFiles, } from "./native-git-bridge.js";
29
+ import { CLOSEOUT_CONSISTENCY_BLOCKED_REASON, checkCloseoutConsistencyGate, formatCloseoutConsistencyBlock, } from "./closeout-consistency-gate.js";
29
30
  import { gsdHome } from "./gsd-home.js";
30
31
  import { createWorkspace } from "./workspace.js";
31
32
  import { _finalizeProjectionForMergeImpl, _projectRootToWorktreeImpl, _projectWorktreeToRootImpl, } from "./worktree-state-projection.js";
@@ -1513,17 +1514,29 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1513
1514
  // symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
1514
1515
  // database (#2823).
1515
1516
  if (isDbAvailable()) {
1517
+ const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1518
+ const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1519
+ const mainDbPath = contract.projectDb;
1516
1520
  try {
1517
- const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1518
- const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1519
- const mainDbPath = contract.projectDb;
1521
+ const activeDbPath = getDbPath();
1522
+ if (activeDbPath && _shouldReconcileWorktreeDb(activeDbPath, mainDbPath)) {
1523
+ closeDatabase();
1524
+ if (!openDatabase(mainDbPath)) {
1525
+ throw new Error(`cannot open project DB at ${mainDbPath}`);
1526
+ }
1527
+ }
1520
1528
  if (_shouldReconcileWorktreeDb(worktreeDbPath, mainDbPath)) {
1521
1529
  reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1522
1530
  }
1523
1531
  }
1524
1532
  catch (err) {
1525
- /* non-fatal */
1526
- logError("worktree", `DB reconciliation failed: ${err instanceof Error ? err.message : String(err)}`);
1533
+ const message = `DB reconciliation failed before milestone ${milestoneId} merge: ${err instanceof Error ? err.message : String(err)}`;
1534
+ logError("worktree", message);
1535
+ throw new GSDError(GSD_GIT_ERROR, `${message}. Recovery reason: ${CLOSEOUT_CONSISTENCY_BLOCKED_REASON}.`);
1536
+ }
1537
+ const closeoutGate = checkCloseoutConsistencyGate(milestoneId);
1538
+ if (!closeoutGate.ok) {
1539
+ throw new GSDError(GSD_GIT_ERROR, formatCloseoutConsistencyBlock(closeoutGate));
1527
1540
  }
1528
1541
  }
1529
1542
  // 2. Get completed slices for commit message
@@ -30,7 +30,7 @@ import { playNotificationBell, sendDesktopNotification } from "./notifications.j
30
30
  import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, } from "./auto-budget.js";
31
31
  import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, isToolInvocationError, isQueuedUserMessageSkip, isDeterministicPolicyError, } from "./auto-tool-tracking.js";
32
32
  import { closeoutUnit } from "./auto-unit-closeout.js";
33
- import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-model-selection.js";
33
+ import { selectAndApplyModel, resolveModelId, clearToolBaseline, getToolBaselineSnapshot } from "./auto-model-selection.js";
34
34
  import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
35
35
  import { resetHookState, runPreDispatchHooks, restoreHookState, clearPersistedHookState, } from "./post-unit-hooks.js";
36
36
  import { runGSDDoctor, rebuildState } from "./doctor.js";
@@ -287,8 +287,24 @@ export function _synthesizePausedSessionRecoveryForTest(basePath, unitType, unit
287
287
  function handlePausedSessionResumeRecovery(basePath, state, notify) {
288
288
  if (!state.pausedSessionFile)
289
289
  return { skippedReplay: false };
290
- const pausedRecoveryUnitType = state.currentUnit?.type ?? state.pausedUnitType ?? "unknown";
291
- const pausedRecoveryUnitId = state.currentUnit?.id ?? state.pausedUnitId ?? "unknown";
290
+ const pausedRecoveryUnitType = state.currentUnit?.type ?? state.pausedUnitType ?? null;
291
+ const pausedRecoveryUnitId = state.currentUnit?.id ?? state.pausedUnitId ?? null;
292
+ // When the paused-session metadata never captured the unit identity (the
293
+ // pause happened between units, or the worker died before currentUnit was
294
+ // set), we have nothing to verify against and nothing correct to target. A
295
+ // replay synthesized with an "unknown" unit re-injects an unbounded,
296
+ // mis-identified tool-call blob into the fresh resume context — exactly the
297
+ // thrash that turns one stuck unit into several. Disk state has already been
298
+ // rebuilt (rebuildState + doctor) before this runs, so skip the replay and
299
+ // let the normal dispatcher recompute the next unit from disk.
300
+ if (!pausedRecoveryUnitType || !pausedRecoveryUnitId) {
301
+ state.pausedSessionFile = null;
302
+ state.pausedUnitType = null;
303
+ state.pausedUnitId = null;
304
+ state.pendingCrashRecovery = null;
305
+ notify("Paused session had no recorded unit identity. Skipping tool-call replay and resuming from disk state.");
306
+ return { skippedReplay: true };
307
+ }
292
308
  const completedPausedUnit = verifyExpectedArtifact(pausedRecoveryUnitType, pausedRecoveryUnitId, basePath);
293
309
  if (completedPausedUnit) {
294
310
  state.pausedSessionFile = null;
@@ -1637,7 +1653,10 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath, session) {
1637
1653
  const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
1638
1654
  ? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
1639
1655
  : undefined;
1640
- const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1656
+ // Use baseline snapshot same reason as phases.ts:runDispatch: the live
1657
+ // active set may be narrowed by the prior unit before selectAndApplyModel
1658
+ // restores it, causing false transport-preflight failures (#477 follow-up).
1659
+ const activeTools = getToolBaselineSnapshot(pi);
1641
1660
  // Mirrors runDispatch: deep-planning keeps approval gates in plain chat
1642
1661
  // because structured questions can be cancelled outside the chat turn on
1643
1662
  // some transports.
@@ -1678,6 +1697,9 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath, session) {
1678
1697
  sessionContextWindow,
1679
1698
  sessionProvider,
1680
1699
  modelRegistry,
1700
+ activeTools,
1701
+ sessionAuthMode: authMode,
1702
+ sessionBaseUrl: ctx.model?.baseUrl,
1681
1703
  });
1682
1704
  if (action.action === "stop") {
1683
1705
  if (session)
@@ -65,6 +65,9 @@ function readDetails(result) {
65
65
  }
66
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- result shape varies by tool
67
67
  function formatToolErrorText(result, details) {
68
+ if (typeof details?.displayReason === "string" && details.displayReason) {
69
+ return details.displayReason;
70
+ }
68
71
  const message = details?.error
69
72
  ?? result?.content?.find((entry) => entry.type === "text")?.text
70
73
  ?? "unknown";
@@ -405,6 +408,9 @@ export function registerDbTools(pi) {
405
408
  kind: StringEnum(["gsd_uat_exec", "gsd_exec", "screenshot", "log", "url", "browser"], { description: "Evidence kind" }),
406
409
  ref: Type.String({ description: "Evidence ID, approved .gsd path, or URL" }),
407
410
  note: Type.Optional(Type.String({ description: "Short evidence note" })),
411
+ unitType: Type.Optional(Type.String({ description: "Unit that produced the evidence" })),
412
+ tool: Type.Optional(Type.String({ description: "Tool that produced the evidence" })),
413
+ executionId: Type.Optional(Type.String({ description: "Stable execution or artifact id" })),
408
414
  });
409
415
  const uatCheck = Type.Object({
410
416
  id: Type.String({ description: "Stable check ID from the UAT spec" }),
@@ -416,17 +422,17 @@ export function registerDbTools(pi) {
416
422
  nonAutomatable: Type.Optional(Type.Boolean({ description: "True when the check is explicitly non-automatable" })),
417
423
  });
418
424
  const toolPresentationBlock = Type.Object({
419
- surface: StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" }),
425
+ surface: Type.Optional(StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" })),
420
426
  model: Type.Optional(Type.Object({
421
427
  provider: Type.Optional(Type.String()),
422
428
  api: Type.Optional(Type.String()),
423
429
  id: Type.Optional(Type.String()),
424
430
  })),
425
- presentedTools: Type.Array(Type.String(), { description: "Tool names actually presented to the model" }),
426
- blockedTools: Type.Array(Type.Object({
431
+ presentedTools: Type.Optional(Type.Array(Type.String(), { description: "Tool names actually presented to the model" })),
432
+ blockedTools: Type.Optional(Type.Array(Type.Object({
427
433
  name: Type.String(),
428
434
  reason: Type.String(),
429
- }), { description: "Tool names blocked from the model with reasons" }),
435
+ }), { description: "Tool names blocked from the model with reasons" })),
430
436
  aliases: Type.Optional(Type.Array(Type.Object({
431
437
  requested: Type.String(),
432
438
  canonical: Type.String(),
@@ -448,12 +454,12 @@ export function registerDbTools(pi) {
448
454
  "Do not use raw gsd_summary_save as a substitute for UAT results.",
449
455
  ],
450
456
  parameters: Type.Object({
451
- milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
452
- sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
453
- uatType: StringEnum(["artifact-driven", "browser-executable", "runtime-executable", "live-runtime", "mixed", "human-experience"], { description: "Declared UAT mode" }),
454
- verdict: StringEnum(["PASS", "FAIL", "PARTIAL"], { description: "Overall UAT verdict" }),
455
- checks: Type.Array(uatCheck, { description: "Structured check results" }),
456
- presentation: toolPresentationBlock,
457
+ milestoneId: Type.Optional(Type.String({ description: "Milestone ID (e.g. M001)" })),
458
+ sliceId: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
459
+ uatType: Type.Optional(Type.String({ description: "Declared UAT mode" })),
460
+ verdict: Type.Optional(Type.String({ description: "Overall UAT verdict: PASS, FAIL, or PARTIAL" })),
461
+ checks: Type.Optional(Type.Array(uatCheck, { description: "Structured check results" })),
462
+ presentation: Type.Optional(toolPresentationBlock),
457
463
  notes: Type.Optional(Type.String({ description: "Overall verdict rationale" })),
458
464
  attempt: Type.Optional(Type.String({ description: "Attempt number or auto" })),
459
465
  previousAttemptId: Type.Optional(Type.String({ description: "Prior attempt ID, when retrying" })),
@@ -30,7 +30,7 @@ import { getGuidedUnitContext } from "../guided-unit-context.js";
30
30
  import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
31
31
  import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
32
32
  import { filterToolsForProvider } from "../model-router.js";
33
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
33
+ import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
34
34
  let approvalQuestionAbortInFlight = false;
35
35
  async function loadWelcomeScreenModule() {
36
36
  const candidates = [];
@@ -204,8 +204,18 @@ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registered
204
204
  return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
205
205
  }
206
206
  export function buildRunUatGsdToolSet(activeToolNames, registeredToolNames = activeToolNames) {
207
- const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES]);
208
- return [...new Set(scoped)];
207
+ const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [
208
+ ...RUN_UAT_WORKFLOW_TOOL_NAMES,
209
+ ...RUN_UAT_READ_ONLY_TOOL_NAMES,
210
+ "subagent",
211
+ ...RUN_UAT_BROWSER_TOOL_NAMES,
212
+ ]);
213
+ const resolved = [...new Set(scoped)];
214
+ const unresolved = RUN_UAT_WORKFLOW_TOOL_NAMES.filter((tool) => !resolved.some((name) => name === tool || (name.startsWith("mcp__") && name.endsWith(`__${tool}`))));
215
+ if (unresolved.length > 0) {
216
+ safetyLogWarning("bootstrap", `buildRunUatGsdToolSet: required run-uat workflow tool(s) not found in active/registered surface: ${unresolved.join(", ")}. Session may lack gsd-workflow MCP connection.`);
217
+ }
218
+ return resolved;
209
219
  }
210
220
  export function buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames = activeToolNames) {
211
221
  const autoBaseTools = new Set(MINIMAL_AUTO_BASE_TOOL_NAMES);
@@ -382,6 +392,11 @@ function isContextDraftSummarySave(toolName, input) {
382
392
  return false;
383
393
  return input.artifact_type === "CONTEXT-DRAFT";
384
394
  }
395
+ function withDepthGateDisplayReason(result, displayReason = "Depth confirmation is waiting for your answer.") {
396
+ if (!result.block)
397
+ return result;
398
+ return { ...result, displayReason };
399
+ }
385
400
  function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
386
401
  if (deferredApprovalGate?.basePath !== basePath)
387
402
  return { block: false };
@@ -389,14 +404,14 @@ function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
389
404
  return { block: false };
390
405
  if (isContextDraftSummarySave(toolName, input))
391
406
  return { block: false };
392
- return {
407
+ return withDepthGateDisplayReason({
393
408
  block: true,
394
409
  reason: [
395
410
  `HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
396
411
  `Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
397
412
  `Wait for the user's answer before calling additional tools.`,
398
413
  ].join(" "),
399
- };
414
+ });
400
415
  }
401
416
  export function resolveNotificationStoreBasePath(basePath) {
402
417
  return resolveWorktreeProjectRoot(basePath);
@@ -453,6 +468,18 @@ export function registerHooks(pi, ecosystemHandlers) {
453
468
  if (isAutoActive() || preserveCloseoutSurface) {
454
469
  ctx.ui.setWidget("gsd-health", undefined);
455
470
  }
471
+ // Cold start after /quit relaunches with cwd at the project root. When
472
+ // auto-mode is neither active nor paused (its own resume path re-enters the
473
+ // worktree with a lease check — auto.ts:3032), proactively chdir back into
474
+ // the active milestone's worktree so subsequent work isn't stranded at the
475
+ // root. Best-effort and a no-op when already inside a worktree.
476
+ if (!isAutoActive() && !isAutoPaused() && !preserveCloseoutSurface) {
477
+ try {
478
+ const { reenterActiveWorktreeIfNeeded } = await import("../worktree-reentry.js");
479
+ await reenterActiveWorktreeIfNeeded(basePath);
480
+ }
481
+ catch { /* non-fatal */ }
482
+ }
456
483
  });
457
484
  pi.on("session_switch", async (_event, ctx) => {
458
485
  const basePath = contextBasePath(ctx);
@@ -754,7 +781,7 @@ export function registerHooks(pi, ecosystemHandlers) {
754
781
  if (ctx) {
755
782
  await maybePauseAutoForApprovalGate(ctx, pi, true, "Depth confirmation is waiting for your answer — pausing auto-mode.");
756
783
  }
757
- return bashGuard;
784
+ return withDepthGateDisplayReason(bashGuard);
758
785
  }
759
786
  }
760
787
  else {
@@ -763,7 +790,7 @@ export function registerHooks(pi, ecosystemHandlers) {
763
790
  if (ctx) {
764
791
  await maybePauseAutoForApprovalGate(ctx, pi, true, "Depth confirmation is waiting for your answer — pausing auto-mode.");
765
792
  }
766
- return gateGuard;
793
+ return withDepthGateDisplayReason(gateGuard);
767
794
  }
768
795
  }
769
796
  }
@@ -847,8 +874,9 @@ export function registerHooks(pi, ecosystemHandlers) {
847
874
  if (!isToolCallEventType("write", event))
848
875
  return;
849
876
  const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive(discussionBasePath), discussionBasePath);
850
- if (result.block)
851
- return result;
877
+ if (result.block) {
878
+ return withDepthGateDisplayReason(result, "Depth check required before writing milestone context.");
879
+ }
852
880
  });
853
881
  // ── Safety harness: evidence collection + destructive command blocking ──
854
882
  pi.on("tool_call", async (event, ctx) => {
@@ -1073,16 +1101,19 @@ export function registerHooks(pi, ecosystemHandlers) {
1073
1101
  if (isAutoActive()) {
1074
1102
  try {
1075
1103
  const { loadEffectiveGSDPreferences } = await import("../preferences.js");
1104
+ const { createObservationMask, createResponsesInputObservationMask, truncateContextResultMessages, truncateResponsesInputResultItems, } = await import("../context-masker.js");
1076
1105
  const prefs = loadEffectiveGSDPreferences();
1077
1106
  const cmConfig = prefs?.preferences.context_management;
1078
1107
  // Observation masking: replace old tool results with placeholders
1079
1108
  if (cmConfig?.observation_masking !== false) {
1080
1109
  const keepTurns = cmConfig?.observation_mask_turns ?? 8;
1081
- const { createObservationMask } = await import("../context-masker.js");
1082
- const mask = createObservationMask(keepTurns);
1083
1110
  const messages = payload.messages;
1084
1111
  if (Array.isArray(messages)) {
1085
- payload.messages = mask(messages);
1112
+ payload.messages = createObservationMask(keepTurns)(messages);
1113
+ }
1114
+ const input = payload.input;
1115
+ if (Array.isArray(input)) {
1116
+ payload.input = createResponsesInputObservationMask(keepTurns)(input);
1086
1117
  }
1087
1118
  }
1088
1119
  // Tool result truncation: cap individual tool result content length.
@@ -1091,23 +1122,11 @@ export function registerHooks(pi, ecosystemHandlers) {
1091
1122
  const maxChars = cmConfig?.tool_result_max_chars ?? 800;
1092
1123
  const msgs = payload.messages;
1093
1124
  if (Array.isArray(msgs)) {
1094
- payload.messages = msgs.map((msg) => {
1095
- // Match toolResult messages (role: "toolResult", content is array of content blocks)
1096
- if (msg?.role === "toolResult" && Array.isArray(msg.content)) {
1097
- const blocks = msg.content;
1098
- const totalLen = blocks.reduce((sum, b) => sum + (typeof b.text === "string" ? b.text.length : 0), 0);
1099
- if (totalLen > maxChars) {
1100
- const truncated = blocks.map(b => {
1101
- if (typeof b.text === "string" && b.text.length > maxChars) {
1102
- return { ...b, text: b.text.slice(0, maxChars) + "\n…[truncated]" };
1103
- }
1104
- return b;
1105
- });
1106
- return { ...msg, content: truncated };
1107
- }
1108
- }
1109
- return msg;
1110
- });
1125
+ payload.messages = truncateContextResultMessages(msgs, maxChars);
1126
+ }
1127
+ const input = payload.input;
1128
+ if (Array.isArray(input)) {
1129
+ payload.input = truncateResponsesInputResultItems(input, maxChars);
1111
1130
  }
1112
1131
  }
1113
1132
  catch { /* non-fatal */ }
@@ -56,7 +56,7 @@ export const BUNDLED_SKILL_TRIGGERS = [
56
56
  { trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
57
57
  { trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
58
58
  { trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
59
- { trigger: "gsd-browser UAT default browser MCP/CLI for real UI verification, screenshots, assertions, console/network diagnostics", skill: "gsd-browser" },
59
+ { trigger: "gsd-browser opt-in and External MCP UAT screenshots, assertions, console/network diagnostics", skill: "gsd-browser" },
60
60
  { trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
61
61
  { trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
62
62
  { trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
@@ -2,7 +2,7 @@
2
2
  import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { minimatch } from "minimatch";
5
- import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
5
+ import { GSD_PHASE_SCOPE_DISPLAY_REASON, shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
6
6
  import { getIsolationMode } from "../preferences.js";
7
7
  import { compileSubagentPermissionContract } from "../unit-context-manifest.js";
8
8
  import { logWarning } from "../workflow-logger.js";
@@ -643,6 +643,13 @@ function blockReason(unitType, mode, what) {
643
643
  `the work belongs in execute-task, not in a planning unit.`,
644
644
  ].join(" ");
645
645
  }
646
+ function planningBlock(unitType, mode, what) {
647
+ return {
648
+ block: true,
649
+ reason: blockReason(unitType, mode, what),
650
+ displayReason: GSD_PHASE_SCOPE_DISPLAY_REASON,
651
+ };
652
+ }
646
653
  /**
647
654
  * Planning-unit tool-policy enforcement. Returns { block } per the policy
648
655
  * resolved from the active unit's manifest:
@@ -690,10 +697,10 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
690
697
  if (tool.startsWith("gsd_"))
691
698
  return { block: false };
692
699
  if (PLANNING_WRITE_TOOLS.has(tool) || tool === "bash" || PLANNING_SUBAGENT_TOOLS.has(tool)) {
693
- return { block: true, reason: blockReason(unitType, policy.mode, `${tool} is not permitted (read-only)`) };
700
+ return planningBlock(unitType, policy.mode, `${tool} is not permitted (read-only)`);
694
701
  }
695
702
  // Unknown tool in read-only mode — block by default.
696
- return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
703
+ return planningBlock(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`);
697
704
  }
698
705
  // planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
699
706
  if (PLANNING_SAFE_TOOLS.has(tool))
@@ -711,10 +718,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
711
718
  // instead of silently bypassing the gate.
712
719
  if (agentClasses === undefined) {
713
720
  warnMissingControlledDispatchAgentClasses(unitType, policy.mode, tool);
714
- return {
715
- block: true,
716
- reason: blockReason(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`),
717
- };
721
+ return planningBlock(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`);
718
722
  }
719
723
  // agentClasses was explicitly provided but resolved to an empty list (for
720
724
  // example, a bare tool call with no agent field). Pass through; no agents
@@ -724,41 +728,29 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
724
728
  }
725
729
  const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
726
730
  if (globallyDisallowed) {
727
- return {
728
- block: true,
729
- reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`),
730
- };
731
+ return planningBlock(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`);
731
732
  }
732
733
  const disallowedByPolicy = requested.find(a => !allowed.has(a));
733
734
  if (disallowedByPolicy) {
734
- return {
735
- block: true,
736
- reason: blockReason(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`),
737
- };
735
+ return planningBlock(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`);
738
736
  }
739
737
  return { block: false };
740
738
  }
741
- return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
739
+ return planningBlock(unitType, policy.mode, "subagent dispatch is not permitted in planning units");
742
740
  }
743
741
  if (tool === "bash") {
744
742
  if (policy.mode === "verification") {
745
743
  if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
746
744
  return { block: false };
747
- return {
748
- block: true,
749
- reason: blockReason(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
750
- };
745
+ return planningBlock(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`);
751
746
  }
752
747
  if (BASH_READ_ONLY_RE.test(pathOrCommand))
753
748
  return { block: false };
754
- return {
755
- block: true,
756
- reason: blockReason(unitType, policy.mode, `bash is restricted to read-only commands (cat/grep/git log/etc); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
757
- };
749
+ return planningBlock(unitType, policy.mode, `bash is restricted to read-only commands (cat/grep/git log/etc); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`);
758
750
  }
759
751
  if (PLANNING_WRITE_TOOLS.has(tool)) {
760
752
  if (!pathOrCommand) {
761
- return { block: true, reason: blockReason(unitType, policy.mode, `${tool} called with empty path`) };
753
+ return planningBlock(unitType, policy.mode, `${tool} called with empty path`);
762
754
  }
763
755
  const absPath = isAbsolute(pathOrCommand) ? pathOrCommand : resolve(basePath, pathOrCommand);
764
756
  // Always allow .gsd/ writes — that's where planning artifacts live.
@@ -768,10 +760,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
768
760
  if (policy.mode === "docs" && matchesAllowedGlob(absPath, basePath, policy.allowedPathGlobs)) {
769
761
  return { block: false };
770
762
  }
771
- return {
772
- block: true,
773
- reason: blockReason(unitType, policy.mode, `cannot ${tool} "${pathOrCommand}" — writes are restricted to .gsd/${policy.mode === "docs" ? " and " + policy.allowedPathGlobs.join(", ") : ""}`),
774
- };
763
+ return planningBlock(unitType, policy.mode, `cannot ${tool} "${pathOrCommand}" — writes are restricted to .gsd/${policy.mode === "docs" ? " and " + policy.allowedPathGlobs.join(", ") : ""}`);
775
764
  }
776
765
  // Unknown tool name — pass through. Other layers (queue, pending-gate,
777
766
  // CONTEXT.md write) catch known mutating shapes; defaulting to allow here
@@ -0,0 +1,61 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Shared DB-backed guard for milestone closeout finalization.
3
+ import { getDbPath, getLatestAssessmentByScope, getMilestone, getMilestoneSlices, getPendingGates, getSliceTasks, isDbAvailable, refreshOpenDatabaseFromDisk, } from "./gsd-db.js";
4
+ import { isClosedStatus } from "./status-guards.js";
5
+ export const CLOSEOUT_CONSISTENCY_BLOCKED_REASON = "closeout-consistency-blocked";
6
+ function blocked(reason, message) {
7
+ return {
8
+ ok: false,
9
+ reason,
10
+ recoveryReason: CLOSEOUT_CONSISTENCY_BLOCKED_REASON,
11
+ message,
12
+ };
13
+ }
14
+ function isFileBackedDbPath(path) {
15
+ return Boolean(path && path !== ":memory:");
16
+ }
17
+ export function checkCloseoutConsistencyGate(milestoneId, options = {}) {
18
+ if (!isDbAvailable()) {
19
+ return blocked("db-unavailable", `Closeout consistency blocked for ${milestoneId}: canonical DB is unavailable.`);
20
+ }
21
+ if (options.refreshFromDisk && isFileBackedDbPath(getDbPath()) && !refreshOpenDatabaseFromDisk()) {
22
+ return blocked("db-refresh-failed", `Closeout consistency blocked for ${milestoneId}: canonical DB refresh failed.`);
23
+ }
24
+ const milestone = getMilestone(milestoneId);
25
+ if (!milestone) {
26
+ return blocked("milestone-missing", `Closeout consistency blocked for ${milestoneId}: milestone is missing from canonical DB.`);
27
+ }
28
+ if (!isClosedStatus(milestone.status)) {
29
+ return blocked("milestone-open", `Closeout consistency blocked for ${milestoneId}: canonical DB milestone status is "${milestone.status}".`);
30
+ }
31
+ if (milestone.status !== "skipped") {
32
+ const validation = getLatestAssessmentByScope(milestoneId, "milestone-validation");
33
+ if (validation?.status !== "pass") {
34
+ return blocked("validation-not-pass", `Closeout consistency blocked for ${milestoneId}: latest milestone validation is "${validation?.status ?? "absent"}".`);
35
+ }
36
+ }
37
+ const slices = getMilestoneSlices(milestoneId);
38
+ if (slices.length === 0 && milestone.status !== "skipped") {
39
+ return blocked("slice-missing", `Closeout consistency blocked for ${milestoneId}: no slices exist in canonical DB.`);
40
+ }
41
+ for (const slice of slices) {
42
+ if (!isClosedStatus(slice.status)) {
43
+ return blocked("slice-open", `Closeout consistency blocked for ${milestoneId}: slice ${slice.id} status is "${slice.status}".`);
44
+ }
45
+ for (const task of getSliceTasks(milestoneId, slice.id)) {
46
+ if (!isClosedStatus(task.status)) {
47
+ return blocked("task-open", `Closeout consistency blocked for ${milestoneId}: task ${slice.id}/${task.id} status is "${task.status}".`);
48
+ }
49
+ }
50
+ const pendingGate = getPendingGates(milestoneId, slice.id)[0];
51
+ if (pendingGate) {
52
+ return blocked("quality-gate-pending", `Closeout consistency blocked for ${milestoneId}: quality gate ${pendingGate.gate_id} is still pending for ${slice.id}.`);
53
+ }
54
+ }
55
+ return { ok: true };
56
+ }
57
+ export function formatCloseoutConsistencyBlock(result) {
58
+ if (result.ok)
59
+ return "";
60
+ return `${result.message} Recovery reason: ${result.recoveryReason}. Resolve the canonical DB state and run /gsd auto to retry.`;
61
+ }
@@ -198,6 +198,16 @@ export async function handleAutoCommand(trimmed, ctx, pi) {
198
198
  if (!(await guardRemoteSession(ctx, pi)))
199
199
  return true;
200
200
  const basePath = projectRoot();
201
+ // Cold start after /quit lands at the project root, not the worktree. If the
202
+ // active milestone has a live worktree, chdir back into it now so the agent
203
+ // doesn't have to search for it. Best-effort; resolves to a no-op otherwise.
204
+ try {
205
+ const { reenterActiveWorktreeIfNeeded } = await import("../../worktree-reentry.js");
206
+ await reenterActiveWorktreeIfNeeded(basePath, {
207
+ notify: (message) => ctx.ui.notify(message, "info"),
208
+ });
209
+ }
210
+ catch { /* non-fatal */ }
201
211
  const { hasGsdBootstrapArtifacts } = await import("../../detection.js");
202
212
  const { gsdRoot } = await import("../../paths.js");
203
213
  if (!hasGsdBootstrapArtifacts(gsdRoot(basePath))) {
@@ -34,7 +34,7 @@ export function formatMcpInitResult(status, configPath, targetPath) {
34
34
  `Config: ${configPath}`,
35
35
  "",
36
36
  "MCP-capable clients can now load the GSD workflow and gsd-browser MCP servers from this folder.",
37
- "Pi Providers use the managed gsd-browser engine directly; this project config is for External MCP Clients.",
37
+ "Pi Providers use built-in browser tools directly; this project config is for External MCP Clients.",
38
38
  "Restart or reconnect any client that already has this project open.",
39
39
  ].join("\n");
40
40
  }
@@ -122,6 +122,7 @@ function collectConfigSections() {
122
122
  supRows.push({ label: "Model", value: sup.model });
123
123
  supRows.push({ label: "Soft timeout", value: `${sup.soft_timeout_minutes}m` });
124
124
  supRows.push({ label: "Idle timeout", value: `${sup.idle_timeout_minutes}m` });
125
+ supRows.push({ label: "Stalled tool timeout", value: `${sup.stalled_tool_timeout_minutes}m` });
125
126
  supRows.push({ label: "Hard timeout", value: `${sup.hard_timeout_minutes}m` });
126
127
  sections.push({ title: "Auto Supervisor", rows: supRows });
127
128
  }