@opengsd/gsd-pi 1.0.2-dev.235ebf3 → 1.0.2-dev.29398d2

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 (184) hide show
  1. package/README.md +63 -12
  2. package/dist/onboarding.js +22 -3
  3. package/dist/resource-loader.d.ts +7 -0
  4. package/dist/resource-loader.js +42 -9
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/context7/index.js +12 -2
  7. package/dist/resources/extensions/google-cli/index.js +30 -0
  8. package/dist/resources/extensions/google-cli/models.js +55 -0
  9. package/dist/resources/extensions/google-cli/package.json +11 -0
  10. package/dist/resources/extensions/google-cli/readiness.js +12 -0
  11. package/dist/resources/extensions/google-cli/stream-adapter.js +191 -0
  12. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  13. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  14. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  15. package/dist/resources/extensions/gsd/auto-start.js +232 -49
  16. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
  19. package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
  20. package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
  21. package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
  22. package/dist/resources/extensions/gsd/doctor-providers.js +54 -24
  23. package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
  24. package/dist/resources/extensions/gsd/key-manager.js +45 -13
  25. package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
  26. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
  27. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
  28. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  29. package/dist/resources/extensions/search-the-web/native-search.js +57 -8
  30. package/dist/resources/shared/package-manager-detection.js +36 -0
  31. package/dist/update-check.d.ts +6 -2
  32. package/dist/update-check.js +7 -3
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/index.html +1 -1
  58. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  65. package/dist/web/standalone/.next/server/chunks/1834.js +2 -2
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  71. package/dist/web/standalone/package.json +0 -1
  72. package/dist/worktree-cli.d.ts +0 -2
  73. package/dist/worktree-cli.js +21 -9
  74. package/package.json +5 -2
  75. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  76. package/packages/cloud-mcp-gateway/package.json +4 -3
  77. package/packages/contracts/package.json +1 -1
  78. package/packages/daemon/package.json +4 -4
  79. package/packages/gsd-agent-core/package.json +5 -5
  80. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +1 -1
  81. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  82. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +2 -2
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts +6 -1
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  86. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js +9 -6
  87. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  89. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  90. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  91. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  92. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  94. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  95. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  96. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  97. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  98. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  100. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts +3 -0
  102. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  103. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +144 -2
  104. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.d.ts.map +1 -1
  106. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js +2 -14
  107. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js.map +1 -1
  108. package/packages/gsd-agent-modes/package.json +7 -7
  109. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  110. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  112. package/packages/mcp-server/package.json +5 -4
  113. package/packages/native/package.json +1 -1
  114. package/packages/pi-agent-core/dist/agent-loop.js +13 -13
  115. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  116. package/packages/pi-agent-core/package.json +1 -1
  117. package/packages/pi-ai/bin/pi-ai.js +14 -0
  118. package/packages/pi-ai/dist/models.generated.d.ts +40 -17
  119. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  120. package/packages/pi-ai/dist/models.generated.js +49 -30
  121. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  122. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/providers/anthropic.js +50 -0
  124. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  125. package/packages/pi-ai/dist/types.d.ts +2 -0
  126. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  127. package/packages/pi-ai/dist/types.js.map +1 -1
  128. package/packages/pi-ai/package.json +3 -2
  129. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  130. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  132. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  133. package/packages/pi-coding-agent/package.json +8 -8
  134. package/packages/pi-tui/package.json +1 -1
  135. package/packages/rpc-client/package.json +2 -2
  136. package/pkg/package.json +1 -1
  137. package/scripts/install/deps.js +10 -0
  138. package/scripts/install/detect-existing.js +17 -3
  139. package/scripts/install/npm-global.js +103 -33
  140. package/scripts/install.js +1 -0
  141. package/src/resources/extensions/context7/index.ts +15 -2
  142. package/src/resources/extensions/google-cli/index.ts +34 -0
  143. package/src/resources/extensions/google-cli/models.ts +57 -0
  144. package/src/resources/extensions/google-cli/package.json +11 -0
  145. package/src/resources/extensions/google-cli/readiness.ts +15 -0
  146. package/src/resources/extensions/google-cli/stream-adapter.ts +245 -0
  147. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  148. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  149. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  150. package/src/resources/extensions/gsd/auto-start.ts +307 -56
  151. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  152. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
  153. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
  154. package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
  155. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
  156. package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
  157. package/src/resources/extensions/gsd/doctor-providers.ts +55 -27
  158. package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
  159. package/src/resources/extensions/gsd/key-manager.ts +57 -14
  160. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
  161. package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
  162. package/src/resources/extensions/gsd/tests/commands-context.test.ts +5 -3
  163. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
  164. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  165. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
  166. package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
  167. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
  168. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
  169. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
  170. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
  171. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
  172. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
  173. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
  174. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  175. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
  176. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
  177. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
  178. package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
  179. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  180. package/src/resources/extensions/search-the-web/native-search.ts +60 -8
  181. package/src/resources/shared/package-manager-detection.ts +39 -0
  182. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  183. /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → bukT6Ux1YchPm2XqjaexX}/_buildManifest.js +0 -0
  184. /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → bukT6Ux1YchPm2XqjaexX}/_ssgManifest.js +0 -0
@@ -50,6 +50,7 @@ import { handleCustomEngineVerifyPause, handleCustomEngineVerifyRetryOutcome, }
50
50
  import { handleCustomEngineReconcile } from "./workflow-custom-engine-reconcile.js";
51
51
  import { handleCustomEngineReconcileOutcome } from "./workflow-custom-engine-reconcile-outcome.js";
52
52
  import { formatLeaseConflictNotice } from "./lease-conflict-notice.js";
53
+ import { setAutoOutcomeWidget, unitVerb } from "../auto-dashboard.js";
53
54
  /**
54
55
  * Returns true if workerId is an active worker in this project whose OS
55
56
  * process no longer exists. Used to detect dead lease holders before
@@ -690,6 +691,24 @@ export async function autoLoop(ctx, pi, s, deps, options) {
690
691
  });
691
692
  if (reconcileFlow.action === "break")
692
693
  break;
694
+ if (s.stepMode) {
695
+ if (ctx.hasUI) {
696
+ ctx.ui.setWidget?.("gsd-progress", undefined);
697
+ setAutoOutcomeWidget(ctx, {
698
+ status: "step",
699
+ title: "Step complete",
700
+ detail: `Completed ${unitVerb(iterData.unitType)} ${iterData.unitId}.`,
701
+ unitLabel: `${unitVerb(iterData.unitType)} ${iterData.unitId}`,
702
+ nextAction: "Advance one step, or resume automatic mode.",
703
+ commands: ["/gsd next", "/gsd auto", "/gsd status for overview"],
704
+ startedAt: s.autoStartTime,
705
+ });
706
+ }
707
+ ctx.ui.setStatus("gsd-auto", "next");
708
+ ctx.ui.notify(`Step complete: ${unitVerb(iterData.unitType)} ${iterData.unitId}. Run /gsd next for the next step, or /gsd auto to continue automatically.`, "info");
709
+ s.preserveStepSurfaceAfterLoopExit = true;
710
+ break;
711
+ }
693
712
  continue;
694
713
  }
695
714
  if (!sidecarItem) {
@@ -1568,7 +1568,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1568
1568
  const dispatchKey = `${unitType}/${unitId}`;
1569
1569
  const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
1570
1570
  // Status bar (widget + preconditions deferred until after model selection — see #2899)
1571
- ctx.ui.setStatus("gsd-auto", "auto");
1571
+ ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1572
1572
  if (mid)
1573
1573
  deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
1574
1574
  // ── Safety harness: reset evidence + create checkpoint ──
@@ -132,6 +132,8 @@ export class AutoSession {
132
132
  // ── Isolation degradation ────────────────────────────────────────────
133
133
  /** Set to true when worktree creation fails; prevents merge of nonexistent branch. */
134
134
  isolationDegraded = false;
135
+ /** Temporary recovery mode for stranded work adopted from physical git evidence. */
136
+ strandedRecoveryIsolationMode = null;
135
137
  /** Project-root dirty snapshot captured before an isolated worktree unit runs. */
136
138
  rootWriteBaseline = null;
137
139
  // ── Merge guard ──────────────────────────────────────────────────────
@@ -285,6 +287,7 @@ export class AutoSession {
285
287
  this.lastGitActionFailure = null;
286
288
  this.lastGitActionStatus = null;
287
289
  this.isolationDegraded = false;
290
+ this.strandedRecoveryIsolationMode = null;
288
291
  this.rootWriteBaseline = null;
289
292
  this.milestoneMergedInPhases = false;
290
293
  this.milestoneStartShas = new Map();
@@ -21,7 +21,7 @@ import { invalidateAllCaches } from "./cache.js";
21
21
  import { writeLock, clearLock, readCrashLock, isLockProcessAlive } from "./crash-recovery.js";
22
22
  import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
23
23
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
24
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
24
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, nativeHasChanges, } from "./native-git-bridge.js";
25
25
  import { GitServiceImpl } from "./git-service.js";
26
26
  import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
27
27
  import { getAutoWorktreePath, checkoutBranchWithStashGuard } from "./auto-worktree.js";
@@ -126,17 +126,68 @@ export function resolveSurvivorRecoveryIsolationMode(isolationMode, phase) {
126
126
  return "branch";
127
127
  return isolationMode;
128
128
  }
129
- export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps = {}) {
129
+ function isBlockingStrandedWorkAction(action) {
130
+ return action.kind === "in-progress-stranded-work" && action.blocksAuto;
131
+ }
132
+ function detectWorktreeEvidence(basePath, milestoneId, hasChanges) {
133
+ const wtDir = getWorktreeDir(basePath, milestoneId);
134
+ const wtPath = getAutoWorktreePath(basePath, milestoneId);
135
+ let dirty = false;
136
+ if (wtPath) {
137
+ try {
138
+ dirty = hasChanges(wtPath);
139
+ }
140
+ catch {
141
+ dirty = false;
142
+ }
143
+ }
144
+ return {
145
+ path: wtPath,
146
+ dirExists: existsSync(wtDir),
147
+ dirty,
148
+ };
149
+ }
150
+ function strandedWorkMessage(args) {
151
+ const evidence = [];
152
+ if (args.branch && args.commitsAhead > 0) {
153
+ evidence.push(`branch ${args.branch} has ${args.commitsAhead} commit(s) ahead of ${args.mainBranch}`);
154
+ }
155
+ if (args.dirtyWorktree) {
156
+ evidence.push("the worktree has uncommitted changes");
157
+ }
158
+ if (evidence.length === 0) {
159
+ evidence.push("physical git evidence exists");
160
+ }
161
+ const wtSuffix = args.worktreeDirExists
162
+ ? ` Worktree directory at .gsd/worktrees/${args.milestoneId}/ holds live work.`
163
+ : "";
164
+ const recovery = args.recoveryMode === "worktree"
165
+ ? "Recovering will adopt the existing worktree."
166
+ : "Recovering will adopt the milestone branch.";
167
+ return (`Stranded work for in-progress milestone ${args.milestoneId}: ${evidence.join("; ")}.` +
168
+ wtSuffix +
169
+ ` ${recovery} Park or discard explicitly if abandoning.`);
170
+ }
171
+ export function auditOrphanedMilestoneBranches(basePath, _isolationMode, gitDeps = {}) {
130
172
  const recovered = [];
131
173
  const warnings = [];
174
+ const actions = [];
132
175
  const branchList = gitDeps.branchList ?? nativeBranchList;
133
176
  const branchExists = gitDeps.branchExists ?? nativeBranchExists;
134
- // Skip in none mode — no milestone branches are created
135
- if (isolationMode === "none")
136
- return { recovered, warnings };
177
+ const hasChanges = gitDeps.hasChanges ?? nativeHasChanges;
178
+ const pushAction = (action) => {
179
+ actions.push(action);
180
+ if (action.severity === "info") {
181
+ recovered.push(action.message);
182
+ }
183
+ else {
184
+ warnings.push(action.message);
185
+ }
186
+ };
137
187
  // Skip if DB not available — can't determine completion status
138
- if (!isDbAvailable())
139
- return { recovered, warnings };
188
+ if (!isDbAvailable()) {
189
+ return { recovered, warnings, actions, blockingStrandedWork: null };
190
+ }
140
191
  let milestoneBranches;
141
192
  let milestoneBranchListAvailable = true;
142
193
  try {
@@ -170,6 +221,7 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
170
221
  if (!milestone)
171
222
  continue;
172
223
  const isMerged = mergedBranches.has(branch);
224
+ const worktreeEvidence = detectWorktreeEvidence(basePath, milestoneId, hasChanges);
173
225
  // #4762 — in-progress milestone branch with unmerged commits ahead of
174
226
  // main. This is the pre-completion orphan case: auto-mode exited without
175
227
  // completing the milestone (pause, stop, crash, merge error, blocker) and
@@ -181,32 +233,45 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
181
233
  // Parked/other closed statuses go through the legacy complete/unmerged
182
234
  // path below where appropriate.
183
235
  if (!isClosedStatus(milestone.status)) {
184
- if (isMerged)
185
- continue; // nothing to recover
186
236
  let commitsAhead = 0;
187
237
  try {
188
238
  commitsAhead = nativeCommitCountBetween(basePath, mainBranch, branch);
189
239
  }
190
240
  catch {
191
- // Rev-walk failure — skip rather than noise
192
- continue;
241
+ commitsAhead = 0;
193
242
  }
194
- if (commitsAhead === 0)
243
+ if ((isMerged || commitsAhead === 0) && !worktreeEvidence.dirty)
195
244
  continue;
196
- const wtDir = getWorktreeDir(basePath, milestoneId);
197
- const wtDirExists = existsSync(wtDir);
198
- const wtSuffix = wtDirExists
199
- ? ` Worktree directory at .gsd/worktrees/${milestoneId}/ holds the live work.`
200
- : "";
201
- warnings.push(`Branch ${branch} has ${commitsAhead} commit(s) ahead of ${mainBranch} for in-progress milestone ${milestoneId}.` +
202
- wtSuffix +
203
- ` Run \`/gsd auto\` to resume, or merge manually if abandoning.`);
245
+ const recoveryMode = worktreeEvidence.path
246
+ ? "worktree"
247
+ : "branch";
248
+ const message = strandedWorkMessage({
249
+ milestoneId,
250
+ branch,
251
+ commitsAhead,
252
+ mainBranch,
253
+ dirtyWorktree: worktreeEvidence.dirty,
254
+ worktreeDirExists: worktreeEvidence.dirExists,
255
+ recoveryMode,
256
+ });
257
+ pushAction({
258
+ kind: "in-progress-stranded-work",
259
+ milestoneId,
260
+ branch,
261
+ commitsAhead,
262
+ dirtyWorktree: worktreeEvidence.dirty,
263
+ worktreeDirExists: worktreeEvidence.dirExists,
264
+ recoveryMode,
265
+ message,
266
+ severity: "warning",
267
+ blocksAuto: true,
268
+ });
204
269
  // #4764 telemetry
205
270
  try {
206
271
  emitWorktreeOrphaned(basePath, milestoneId, {
207
272
  reason: "in-progress-unmerged",
208
273
  commitsAhead,
209
- worktreeDirExists: wtDirExists,
274
+ worktreeDirExists: worktreeEvidence.dirExists,
210
275
  });
211
276
  }
212
277
  catch (err) {
@@ -223,7 +288,14 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
223
288
  // Branch is merged — safe to delete branch and clean up worktree dir
224
289
  try {
225
290
  nativeBranchDelete(basePath, branch, true);
226
- recovered.push(`Deleted merged branch ${branch} for completed milestone ${milestoneId}.`);
291
+ pushAction({
292
+ kind: "complete-merged-branch",
293
+ milestoneId,
294
+ branch,
295
+ message: `Deleted merged branch ${branch} for completed milestone ${milestoneId}.`,
296
+ severity: "info",
297
+ blocksAuto: false,
298
+ });
227
299
  }
228
300
  catch (err) {
229
301
  warnings.push(`Failed to delete merged branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
@@ -246,7 +318,15 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
246
318
  if (isInsideWorktreesDir(basePath, wtDir)) {
247
319
  try {
248
320
  rmSync(wtDir, { recursive: true, force: true });
249
- recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
321
+ pushAction({
322
+ kind: "complete-merged-worktree",
323
+ milestoneId,
324
+ branch,
325
+ worktreeDirExists: true,
326
+ message: `Removed orphaned worktree directory for ${milestoneId}.`,
327
+ severity: "info",
328
+ blocksAuto: false,
329
+ });
250
330
  }
251
331
  catch (err2) {
252
332
  warnings.push(`Failed to remove worktree directory for ${milestoneId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -257,14 +337,30 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
257
337
  }
258
338
  }
259
339
  else {
260
- recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
340
+ pushAction({
341
+ kind: "complete-merged-worktree",
342
+ milestoneId,
343
+ branch,
344
+ worktreeDirExists: true,
345
+ message: `Removed orphaned worktree directory for ${milestoneId}.`,
346
+ severity: "info",
347
+ blocksAuto: false,
348
+ });
261
349
  }
262
350
  }
263
351
  }
264
352
  else {
265
353
  // Branch is NOT merged — preserve for safety, warn the user
266
- warnings.push(`Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
267
- `This may contain unmerged work. Merge manually or run \`/gsd doctor fix\` to resolve.`);
354
+ pushAction({
355
+ kind: "complete-unmerged-branch",
356
+ milestoneId,
357
+ branch,
358
+ worktreeDirExists: worktreeEvidence.dirExists,
359
+ message: `Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
360
+ `This may contain unmerged work. Merge manually or run \`/gsd doctor fix\` to resolve.`,
361
+ severity: "warning",
362
+ blocksAuto: false,
363
+ });
268
364
  // #4764 telemetry
269
365
  try {
270
366
  emitWorktreeOrphaned(basePath, milestoneId, {
@@ -300,6 +396,43 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
300
396
  completedMilestones = [];
301
397
  }
302
398
  for (const m of completedMilestones) {
399
+ if (!isClosedStatus(m.status)) {
400
+ if (seenMilestoneIds.has(m.id))
401
+ continue;
402
+ const worktreeEvidence = detectWorktreeEvidence(basePath, m.id, hasChanges);
403
+ if (!worktreeEvidence.dirty)
404
+ continue;
405
+ const message = strandedWorkMessage({
406
+ milestoneId: m.id,
407
+ commitsAhead: 0,
408
+ mainBranch,
409
+ dirtyWorktree: true,
410
+ worktreeDirExists: worktreeEvidence.dirExists,
411
+ recoveryMode: "worktree",
412
+ });
413
+ pushAction({
414
+ kind: "in-progress-stranded-work",
415
+ milestoneId: m.id,
416
+ commitsAhead: 0,
417
+ dirtyWorktree: true,
418
+ worktreeDirExists: worktreeEvidence.dirExists,
419
+ recoveryMode: "worktree",
420
+ message,
421
+ severity: "warning",
422
+ blocksAuto: true,
423
+ });
424
+ try {
425
+ emitWorktreeOrphaned(basePath, m.id, {
426
+ reason: "in-progress-unmerged",
427
+ commitsAhead: 0,
428
+ worktreeDirExists: worktreeEvidence.dirExists,
429
+ });
430
+ }
431
+ catch (err) {
432
+ logWarning("engine", `worktree-orphaned telemetry failed for ${m.id}: ${err instanceof Error ? err.message : String(err)}`);
433
+ }
434
+ continue;
435
+ }
303
436
  if (m.status !== "complete")
304
437
  continue;
305
438
  if (seenMilestoneIds.has(m.id))
@@ -332,17 +465,36 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
332
465
  if (existsSync(wtDir)) {
333
466
  try {
334
467
  rmSync(wtDir, { recursive: true, force: true });
335
- recovered.push(`Removed orphaned worktree directory for ${m.id} (branch already deleted).`);
468
+ pushAction({
469
+ kind: "complete-branchless-worktree",
470
+ milestoneId: m.id,
471
+ worktreeDirExists: true,
472
+ message: `Removed orphaned worktree directory for ${m.id} (branch already deleted).`,
473
+ severity: "info",
474
+ blocksAuto: false,
475
+ });
336
476
  }
337
477
  catch (err) {
338
478
  warnings.push(`Failed to remove orphaned worktree directory for ${m.id}: ${err instanceof Error ? err.message : String(err)}`);
339
479
  }
340
480
  }
341
481
  else {
342
- recovered.push(`Removed orphaned worktree directory for ${m.id} (branch already deleted).`);
482
+ pushAction({
483
+ kind: "complete-branchless-worktree",
484
+ milestoneId: m.id,
485
+ worktreeDirExists: true,
486
+ message: `Removed orphaned worktree directory for ${m.id} (branch already deleted).`,
487
+ severity: "info",
488
+ blocksAuto: false,
489
+ });
343
490
  }
344
491
  }
345
- return { recovered, warnings };
492
+ return {
493
+ recovered,
494
+ warnings,
495
+ actions,
496
+ blockingStrandedWork: actions.find(isBlockingStrandedWorkAction) ?? null,
497
+ };
346
498
  }
347
499
  /**
348
500
  * Pure decision function for picking which orphan milestone the auto-loop
@@ -679,17 +831,27 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
679
831
  // was lost due to session ending between completion and teardown.
680
832
  // Must run after DB open and before worktree entry.
681
833
  let orphanAuditRecovered = false;
834
+ let strandedRecoveryActions = [];
835
+ let strandedRecoveryAction = null;
682
836
  try {
683
837
  const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode(base));
838
+ strandedRecoveryActions = auditResult.actions.filter(isBlockingStrandedWorkAction);
839
+ strandedRecoveryAction = strandedRecoveryActions[0] ?? null;
684
840
  for (const msg of auditResult.recovered) {
685
841
  ctx.ui.notify(`Orphan audit: ${msg}`, "info");
686
842
  }
687
843
  for (const msg of auditResult.warnings) {
688
- ctx.ui.notify(`Orphan audit: ${msg}`, "warning");
844
+ const prefix = msg.startsWith("Stranded work") ? "" : "Orphan audit: ";
845
+ ctx.ui.notify(`${prefix}${msg}`, "warning");
689
846
  }
690
847
  if (auditResult.recovered.length > 0) {
691
848
  orphanAuditRecovered = true;
692
- debugLog("orphan-audit", { recovered: auditResult.recovered, warnings: auditResult.warnings });
849
+ debugLog("orphan-audit", {
850
+ recovered: auditResult.recovered,
851
+ warnings: auditResult.warnings,
852
+ strandedRecoveryAction,
853
+ strandedRecoveryActions,
854
+ });
693
855
  }
694
856
  }
695
857
  catch (err) {
@@ -724,13 +886,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
724
886
  logWarning("bootstrap", `orphaned preflight-stash audit failed: ${err instanceof Error ? err.message : String(err)}`);
725
887
  }
726
888
  let state = await deriveState(base);
727
- if (process.env.GSD_HEADLESS === "1" &&
728
- orphanAuditRecovered &&
729
- !state.activeMilestone &&
730
- state.phase === "complete") {
731
- ctx.ui.notify("Auto-mode stopped (Recovered completed milestone cleanup; all milestones complete).", "info");
732
- return releaseLockAndReturn();
733
- }
734
889
  // Stale worktree state recovery (#654)
735
890
  if (state.activeMilestone &&
736
891
  shouldUseWorktreeIsolation(base) &&
@@ -740,6 +895,28 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
740
895
  state = await deriveState(wtPath);
741
896
  }
742
897
  }
898
+ const blockingStrandedRecoveryAction = state.activeMilestone
899
+ ? strandedRecoveryActions.find((action) => action.milestoneId !== state.activeMilestone?.id) ?? strandedRecoveryAction
900
+ : strandedRecoveryAction;
901
+ if (blockingStrandedRecoveryAction) {
902
+ if (!state.activeMilestone) {
903
+ ctx.ui.notify(`Stranded work for ${blockingStrandedRecoveryAction.milestoneId} blocks auto-mode, but that milestone is not active in project state. Park or discard it explicitly before continuing.`, "error");
904
+ return releaseLockAndReturn();
905
+ }
906
+ if (state.activeMilestone.id !== blockingStrandedRecoveryAction.milestoneId) {
907
+ ctx.ui.notify(`Stranded work for ${blockingStrandedRecoveryAction.milestoneId} blocks auto-mode before ${state.activeMilestone.id}. Recover, park, or discard ${blockingStrandedRecoveryAction.milestoneId} explicitly before continuing.`, "error");
908
+ return releaseLockAndReturn();
909
+ }
910
+ strandedRecoveryAction = blockingStrandedRecoveryAction;
911
+ ctx.ui.notify(`Recovering stranded work for ${strandedRecoveryAction.milestoneId} before dispatching new units.`, "info");
912
+ }
913
+ if (process.env.GSD_HEADLESS === "1" &&
914
+ orphanAuditRecovered &&
915
+ !state.activeMilestone &&
916
+ state.phase === "complete") {
917
+ ctx.ui.notify("Auto-mode stopped (Recovered completed milestone cleanup; all milestones complete).", "info");
918
+ return releaseLockAndReturn();
919
+ }
743
920
  // Milestone branch recovery (#601, #2358)
744
921
  // Detect survivor milestone branches in both pre-planning and complete phases.
745
922
  // In phase=complete, the milestone artifacts exist but finalization (merge,
@@ -768,7 +945,8 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
768
945
  // The worktree/branch was created but the milestone only has CONTEXT-DRAFT.md.
769
946
  // Route to the interactive discussion handler instead of falling through to
770
947
  // auto-mode, which would immediately stop with "needs discussion".
771
- if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "discuss") {
948
+ if (!strandedRecoveryAction &&
949
+ decideSurvivorAction(hasSurvivorBranch, state.phase) === "discuss") {
772
950
  const { showSmartEntry } = await import("./guided-flow.js");
773
951
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
774
952
  invalidateAllCaches();
@@ -848,14 +1026,14 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
848
1026
  const effectivePrefs = loadEffectiveGSDPreferences(base)?.preferences;
849
1027
  const { shouldRunDeepProjectSetup } = await import("./auto-dispatch.js");
850
1028
  const deepProjectStagePending = shouldRunDeepProjectSetup(state, effectivePrefs, base, { hasSurvivorBranch });
851
- if (deepProjectStagePending) {
1029
+ if (deepProjectStagePending && !strandedRecoveryAction) {
852
1030
  // Deep project-level setup runs before the first milestone exists. Let
853
1031
  // the auto loop dispatch workflow-preferences / project / requirements
854
1032
  // units instead of recursing back through showSmartEntry while this
855
1033
  // bootstrap still holds the session lock.
856
1034
  s.currentMilestoneId = null;
857
1035
  }
858
- if (!hasSurvivorBranch && !deepProjectStagePending) {
1036
+ if (!hasSurvivorBranch && !deepProjectStagePending && !strandedRecoveryAction) {
859
1037
  // No active work — start a new milestone via discuss flow
860
1038
  if (!state.activeMilestone || state.phase === "complete") {
861
1039
  // Guard against recursive dialog loop (#1348):
@@ -917,7 +1095,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
917
1095
  }
918
1096
  }
919
1097
  // Unreachable safety check
920
- if (!state.activeMilestone && !deepProjectStagePending) {
1098
+ if (!state.activeMilestone && !deepProjectStagePending && !strandedRecoveryAction) {
921
1099
  const { showSmartEntry } = await import("./guided-flow.js");
922
1100
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
923
1101
  return releaseLockAndReturn();
@@ -952,7 +1130,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
952
1130
  s.resourceVersionOnStart = readResourceVersion();
953
1131
  s.pendingQuickTasks = [];
954
1132
  s.currentUnit = null;
955
- s.currentMilestoneId ??= deepProjectStagePending ? null : state.activeMilestone?.id ?? null;
1133
+ s.currentMilestoneId ??=
1134
+ strandedRecoveryAction?.milestoneId ??
1135
+ (deepProjectStagePending ? null : state.activeMilestone?.id ?? null);
956
1136
  s.originalModelId = startModelSnapshot?.id ?? ctx.model?.id ?? null;
957
1137
  s.originalModelProvider = startModelSnapshot?.provider ?? ctx.model?.provider ?? null;
958
1138
  s.originalThinkingLevel = startThinkingSnapshot ?? null;
@@ -960,7 +1140,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
960
1140
  registerSigtermHandler(base);
961
1141
  // Capture integration branch
962
1142
  if (s.currentMilestoneId) {
963
- if (getIsolationMode(base) !== "none") {
1143
+ if (getIsolationMode(base) !== "none" || strandedRecoveryAction) {
964
1144
  captureIntegrationBranch(base, s.currentMilestoneId);
965
1145
  }
966
1146
  setActiveMilestoneId(base, s.currentMilestoneId);
@@ -970,7 +1150,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
970
1150
  // milestone/<MID>. Auto-checkout back to the integration branch.
971
1151
  const isolationMode = getIsolationMode(base);
972
1152
  const isRepo = nativeIsRepo(base);
973
- if (isolationMode === "none" && isRepo) {
1153
+ if (isolationMode === "none" && isRepo && !strandedRecoveryAction) {
974
1154
  try {
975
1155
  const currentBranch = nativeGetCurrentBranch(base);
976
1156
  const integrationBranch = nativeDetectMainBranch(base);
@@ -1001,12 +1181,15 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
1001
1181
  return symlinkRe.test(p);
1002
1182
  };
1003
1183
  if (s.currentMilestoneId &&
1004
- getIsolationMode(base) !== "none" &&
1184
+ (getIsolationMode(base) !== "none" || strandedRecoveryAction?.recoveryMode) &&
1005
1185
  !detectWorktreeName(base) &&
1006
1186
  !isUnderGsdWorktrees(base)) {
1007
- const enterResult = buildLifecycle().enterMilestone(s.currentMilestoneId, {
1008
- notify: ctx.ui.notify.bind(ctx.ui),
1009
- });
1187
+ const lifecycle = buildLifecycle();
1188
+ const enterResult = strandedRecoveryAction?.recoveryMode
1189
+ ? lifecycle.adoptStrandedMilestone(s.currentMilestoneId, base, { notify: ctx.ui.notify.bind(ctx.ui) }, { mode: strandedRecoveryAction.recoveryMode })
1190
+ : lifecycle.enterMilestone(s.currentMilestoneId, {
1191
+ notify: ctx.ui.notify.bind(ctx.ui),
1192
+ });
1010
1193
  if (!enterResult.ok) {
1011
1194
  s.active = false;
1012
1195
  if (enterResult.reason === "lease-conflict") {
@@ -23,6 +23,7 @@ import { debugLog } from "./debug-logger.js";
23
23
  import { logWarning, logError } from "./workflow-logger.js";
24
24
  import { loadEffectiveGSDPreferences } from "./preferences.js";
25
25
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
26
+ import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
26
27
  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";
27
28
  import { gsdHome } from "./gsd-home.js";
28
29
  import { createWorkspace } from "./workspace.js";
@@ -796,60 +797,7 @@ export function syncGsdStateToWorktree(mainBasePath, worktreePath_) {
796
797
  export function syncWorktreeStateBack(mainBasePath, worktreePath, milestoneId) {
797
798
  return _finalizeProjectionForMergeImpl(mainBasePath, worktreePath, milestoneId);
798
799
  }
799
- // ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
800
- /**
801
- * Run the user-configured post-create hook script after worktree creation.
802
- * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
803
- * Failure is non-fatal — returns the error message or null on success.
804
- *
805
- * Reads the hook path from git.worktree_post_create in preferences.
806
- * Pass hookPath directly to bypass preference loading (useful for testing).
807
- */
808
- export function runWorktreePostCreateHook(sourceDir, worktreeDir, hookPath) {
809
- if (hookPath === undefined) {
810
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
811
- hookPath = prefs?.worktree_post_create;
812
- }
813
- if (!hookPath)
814
- return null;
815
- // Resolve relative paths against the source project root.
816
- // On Windows, convert 8.3 short paths (e.g. RUNNER~1) to long paths
817
- // so execFileSync can locate the file correctly.
818
- let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
819
- if (!existsSync(resolved)) {
820
- return `Worktree post-create hook not found: ${resolved}`;
821
- }
822
- if (process.platform === "win32") {
823
- try {
824
- resolved = realpathSync.native(resolved);
825
- }
826
- catch (err) { /* keep original */
827
- logWarning("worktree", `realpath failed: ${err instanceof Error ? err.message : String(err)}`);
828
- }
829
- }
830
- try {
831
- // .bat/.cmd files on Windows require shell mode — execFileSync cannot
832
- // spawn them directly (EINVAL).
833
- const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
834
- execFileSync(resolved, [], {
835
- cwd: worktreeDir,
836
- env: {
837
- ...process.env,
838
- SOURCE_DIR: sourceDir,
839
- WORKTREE_DIR: worktreeDir,
840
- },
841
- stdio: ["ignore", "pipe", "pipe"],
842
- encoding: "utf-8",
843
- timeout: 30_000, // 30 second timeout
844
- shell: needsShell,
845
- });
846
- return null;
847
- }
848
- catch (err) {
849
- const msg = err instanceof Error ? err.message : String(err);
850
- return `Worktree post-create hook failed: ${msg}`;
851
- }
852
- }
800
+ export { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
853
801
  // ─── Auto-Worktree Branch Naming ───────────────────────────────────────────
854
802
  /** Returns the git branch name for a milestone worktree (`milestone/<MID>`). */
855
803
  export function autoWorktreeBranch(milestoneId) {
@@ -678,8 +678,9 @@ export function registerDbTools(pi) {
678
678
  promptSnippet: "Complete a GSD task (DB write + summary render + checkbox toggle)",
679
679
  promptGuidelines: [
680
680
  "Use gsd_task_complete (or gsd_complete_task) when a task is finished and needs to be recorded.",
681
- "All string fields are required. verificationEvidence is an array of objects with command, exitCode, verdict, durationMs.",
682
- "The tool validates required fields and returns an error message if any are missing.",
681
+ "Include verification whenever possible. If verification is omitted, the executor derives it from verificationEvidence when possible.",
682
+ "verificationEvidence is an array of objects with command, exitCode, verdict, durationMs.",
683
+ "The tool validates required fields and returns an error message if verification cannot be derived.",
683
684
  "On success, returns the summaryPath where the SUMMARY.md was written.",
684
685
  "Idempotent — calling with the same params twice will upsert (INSERT OR REPLACE) without error.",
685
686
  ],
@@ -690,7 +691,7 @@ export function registerDbTools(pi) {
690
691
  milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
691
692
  oneLiner: Type.String({ description: "One-line summary of what was accomplished" }),
692
693
  narrative: Type.String({ description: "Detailed narrative of what happened during the task" }),
693
- verification: Type.String({ description: "What was verified and how — commands run, tests passed, behavior confirmed" }),
694
+ verification: Type.Optional(Type.String({ description: "What was verified and how — commands run, tests passed, behavior confirmed. If omitted, derived from verificationEvidence when possible." })),
694
695
  // ── Enrichment metadata (optional — defaults to empty) ────────────
695
696
  deviations: Type.Optional(Type.String({ description: "Deviations from the task plan, or 'None.'" })),
696
697
  knownIssues: Type.Optional(Type.String({ description: "Known issues discovered but not fixed, or 'None.'" })),