gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5

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 (182) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +73 -30
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
  8. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  10. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  11. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  12. package/dist/resources/extensions/gsd/auto.js +7 -2
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  17. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  18. package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
  19. package/dist/resources/extensions/gsd/git-service.js +6 -2
  20. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  21. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  22. package/dist/resources/extensions/gsd/guided-flow.js +95 -116
  23. package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
  24. package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
  25. package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  29. package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  31. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  32. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
  34. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  36. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  37. package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
  40. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +1 -1
  77. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  79. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  80. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  84. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  85. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  86. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  90. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
  91. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  93. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  94. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  95. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  96. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  97. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  98. package/packages/pi-tui/dist/terminal.js +12 -0
  99. package/packages/pi-tui/dist/terminal.js.map +1 -1
  100. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  101. package/packages/pi-tui/src/terminal.ts +11 -0
  102. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  103. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  104. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  105. package/src/resources/extensions/gsd/auto/phases.ts +83 -37
  106. package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
  107. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  108. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
  109. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  110. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  111. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  112. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  113. package/src/resources/extensions/gsd/auto.ts +8 -2
  114. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
  115. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
  116. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  117. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  118. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  119. package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
  120. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  121. package/src/resources/extensions/gsd/git-service.ts +6 -3
  122. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  124. package/src/resources/extensions/gsd/guided-flow.ts +128 -133
  125. package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
  126. package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
  127. package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
  128. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  131. package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  134. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  135. package/src/resources/extensions/gsd/prompts/queue.md +4 -4
  136. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  137. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  139. package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
  140. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
  141. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
  142. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  143. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  145. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
  146. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
  147. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  148. package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
  149. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  150. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
  151. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
  152. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  153. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  154. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
  155. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  156. package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
  159. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
  160. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
  161. package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
  162. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
  163. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  164. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  165. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  166. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
  167. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
  168. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
  171. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  172. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  173. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  174. package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
  175. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
  176. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
  177. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  178. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  179. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  180. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  181. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
@@ -457,9 +457,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
457
457
  const attempts = incrementUatCount(basePath, mid, sliceId);
458
458
  if (attempts > MAX_UAT_ATTEMPTS) {
459
459
  return {
460
- action: "stop" as const,
461
- reason: `run-uat for ${mid}/${sliceId} has been dispatched ${attempts - 1} times without producing a verdict. Verification commands may be broken — fix the UAT spec or manually write an ASSESSMENT verdict.`,
462
- level: "warning" as const,
460
+ action: "skip" as const,
463
461
  };
464
462
  }
465
463
  const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
@@ -1330,16 +1328,16 @@ export const DISPATCH_RULES: DispatchRule[] = [
1330
1328
  }
1331
1329
  }
1332
1330
 
1333
- // Safety guard (#2675, #5747): block completion when VALIDATION
1334
- // verdict is non-passing. The state machine treats these verdicts as
1335
- // terminal, but completing-milestone should NOT proceed — remediation
1336
- // or human attention is needed.
1331
+ // Safety guard (#2675, #5747, #5920): block completion when VALIDATION
1332
+ // verdict is anything other than pass. The state machine treats these
1333
+ // verdicts as terminal, but completing-milestone should NOT proceed —
1334
+ // remediation or human attention is needed.
1337
1335
  const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
1338
1336
  if (validationFile) {
1339
1337
  const validationContent = await loadFile(validationFile);
1340
1338
  if (validationContent) {
1341
1339
  const verdict = extractVerdict(validationContent);
1342
- if (verdict === "needs-remediation" || verdict === "needs-attention") {
1340
+ if (verdict !== "pass") {
1343
1341
  return {
1344
1342
  action: "stop",
1345
1343
  reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
@@ -1359,16 +1357,12 @@ export const DISPATCH_RULES: DispatchRule[] = [
1359
1357
  };
1360
1358
  }
1361
1359
 
1362
- // Safety guard (#1703): verify the milestone produced implementation
1363
- // artifacts (non-.gsd/ files). A milestone with only plan files and
1364
- // zero implementation code should not be marked complete.
1360
+ // Safety signal (#1703, #5097): detect milestones with only .gsd/
1361
+ // artifacts. This no longer hard-blocks completion because some
1362
+ // milestones are intentionally planning/documentation-only.
1365
1363
  const artifactCheck = hasImplementationArtifacts(basePath, mid);
1366
1364
  if (artifactCheck === "absent") {
1367
- return {
1368
- action: "stop",
1369
- reason: `Cannot complete milestone ${mid}: no implementation files found outside .gsd/. The milestone has only plan files — actual code changes are required.`,
1370
- level: "error",
1371
- };
1365
+ logWarning("dispatch", `Milestone ${mid} has no implementation files outside .gsd/ — continuing complete-milestone dispatch (planning-only/documentation-only milestone).`);
1372
1366
  }
1373
1367
  if (artifactCheck === "unknown") {
1374
1368
  logWarning("dispatch", `Implementation artifact check inconclusive for ${mid} — proceeding (git context unavailable)`);
@@ -201,9 +201,15 @@ export function hasImplementationArtifacts(basePath: string, milestoneId?: strin
201
201
  // Strategy: check `git diff --name-only` against the merge-base with the
202
202
  // main branch. This captures ALL files changed during the milestone's
203
203
  // lifetime while running on a milestone branch.
204
- const integrationBranch = milestoneId
205
- ? readIntegrationBranch(basePath, milestoneId) ?? detectMainBranch(basePath)
206
- : detectMainBranch(basePath);
204
+ const recordedIntegrationBranch = milestoneId
205
+ ? readIntegrationBranch(basePath, milestoneId)
206
+ : null;
207
+ let integrationBranch: string;
208
+ if (recordedIntegrationBranch?.startsWith("milestone/")) {
209
+ integrationBranch = detectMainBranch(basePath);
210
+ } else {
211
+ integrationBranch = recordedIntegrationBranch ?? detectMainBranch(basePath);
212
+ }
207
213
  const currentBranch = getCurrentBranch(basePath);
208
214
  const branchDiff = getChangedFilesSinceBranch(basePath, integrationBranch);
209
215
  if (!branchDiff.ok) return "unknown";
@@ -556,27 +562,55 @@ function commitMatchesMilestone(basePath: string, message: string, milestoneId:
556
562
  // rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
557
563
  // either the commit touched this milestone's artifacts, or — for projects
558
564
  // where .gsd/ is gitignored/external (#5033) — the message explicitly
559
- // names the milestone or local GSD state proves the task belongs here.
565
+ // names the milestone, local GSD state proves the task belongs here, or the
566
+ // commit is implementation-bearing evidence itself (#5100).
560
567
  if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
561
568
  if (files.some((file) => isMilestoneArtifactPath(file, milestoneId))) return true;
562
569
  if (commitMessageMentionsMilestone(message, milestoneId)) return true;
563
- if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId)) return true;
570
+ const taskTrailerOwnership = getTaskOwnershipStatus(basePath, message, milestoneId);
571
+ if (taskTrailerOwnership === true) return true;
572
+ if (taskTrailerOwnership === false) return false;
573
+ // taskTrailerOwnership === null: unknown ownership. Apply fallback only
574
+ // in this case to avoid cross-milestone attribution.
575
+ if (MILESTONE_ID_RE.test(milestoneId) && classifyImplementationFiles(files) === "present") return true;
564
576
  }
565
577
 
566
578
  return false;
567
579
  }
568
580
 
569
- function commitTaskTrailerBelongsToMilestone(basePath: string, message: string, milestoneId: string): boolean {
581
+ /**
582
+ * Tri-state task ownership probe.
583
+ * true => DB or local files confirm this milestone owns the task.
584
+ * false => DB is available and this milestone is registered, but task is absent.
585
+ * null => ownership unknown (milestone not in DB yet, or no DB + no local files).
586
+ */
587
+ function getTaskOwnershipStatus(
588
+ basePath: string,
589
+ message: string,
590
+ milestoneId: string,
591
+ ): true | false | null {
570
592
  const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
571
- if (!match) return false;
593
+ if (!match) return null;
572
594
  const [, sliceId, taskId] = match;
573
595
 
574
- if (getTask(milestoneId, sliceId, taskId)) return true;
596
+ if (isDbAvailable()) {
597
+ if (!getMilestone(milestoneId)) return null;
598
+ return getTask(milestoneId, sliceId, taskId) ? true : false;
599
+ }
575
600
 
601
+ // DB unavailable: fallback to local task-file presence.
576
602
  const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
577
- if (!tasksDir) return false;
578
- return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
579
- || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
603
+ if (
604
+ tasksDir
605
+ && (
606
+ existsSync(join(tasksDir, `${taskId}-PLAN.md`))
607
+ || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`))
608
+ )
609
+ ) {
610
+ return true;
611
+ }
612
+
613
+ return null;
580
614
  }
581
615
 
582
616
  function commitMessageMentionsMilestone(message: string, milestoneId: string): boolean {
@@ -42,7 +42,6 @@ import {
42
42
  nativeCommit,
43
43
  nativeGetCurrentBranch,
44
44
  nativeDetectMainBranch,
45
- nativeCheckoutBranch,
46
45
  nativeBranchList,
47
46
  nativeBranchExists,
48
47
  nativeBranchListMerged,
@@ -56,7 +55,7 @@ import {
56
55
  detectWorktreeName,
57
56
  setActiveMilestoneId,
58
57
  } from "./worktree.js";
59
- import { getAutoWorktreePath, isInAutoWorktree } from "./auto-worktree.js";
58
+ import { getAutoWorktreePath, isInAutoWorktree, checkoutBranchWithStashGuard } from "./auto-worktree.js";
60
59
  import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
61
60
  import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
62
61
  import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
@@ -1170,7 +1169,7 @@ export async function bootstrapAutoSession(
1170
1169
  isRepo,
1171
1170
  );
1172
1171
  if (branchToCheckout) {
1173
- nativeCheckoutBranch(base, branchToCheckout);
1172
+ checkoutBranchWithStashGuard(base, branchToCheckout, "isolation-none-recovery");
1174
1173
  logWarning("bootstrap", `Returned to "${branchToCheckout}" — HEAD was on stale milestone branch "${currentBranch}" (isolation: none does not use milestone branches).`);
1175
1174
  }
1176
1175
  } catch (err) {
@@ -105,11 +105,31 @@ async function runValidateMilestonePostCheck(
105
105
  const { milestone: mid } = parseUnitId(s.currentUnit.id);
106
106
  if (!mid) return "continue";
107
107
 
108
+ const setToolFailureRetry = (message: string): VerificationResult => {
109
+ const retryKey = verificationRetryKey(s.currentUnit!.type, s.currentUnit!.id);
110
+ const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
111
+ s.verificationRetryCount.set(retryKey, attempt);
112
+ s.pendingVerificationRetry = {
113
+ unitId: s.currentUnit!.id,
114
+ failureContext: message,
115
+ attempt,
116
+ };
117
+ return "retry";
118
+ };
119
+
108
120
  const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
109
- if (!validationFile) return "continue";
121
+ if (!validationFile) {
122
+ return setToolFailureRetry(
123
+ "You must call gsd_validate_milestone to persist the validation results. No VALIDATION.md was created.",
124
+ );
125
+ }
110
126
 
111
127
  const validationContent = await loadFile(validationFile);
112
- if (!validationContent) return "continue";
128
+ if (!validationContent) {
129
+ return setToolFailureRetry(
130
+ "You must call gsd_validate_milestone to persist the validation results. VALIDATION.md exists but is empty.",
131
+ );
132
+ }
113
133
 
114
134
  const verdict = extractVerdict(validationContent);
115
135
  if (verdict !== "needs-remediation") {
@@ -1007,7 +1007,73 @@ export function enterBranchModeForMilestone(
1007
1007
  });
1008
1008
  }
1009
1009
 
1010
- nativeCheckoutBranch(basePath, branch);
1010
+ checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
1011
+ }
1012
+
1013
+ export function checkoutBranchWithStashGuard(
1014
+ basePath: string,
1015
+ branch: string,
1016
+ reason: string,
1017
+ ): void {
1018
+ let stashMarker: string | null = null;
1019
+ let stashed = false;
1020
+
1021
+ const status = nativeWorkingTreeStatus(basePath).trim();
1022
+ if (status.length > 0) {
1023
+ stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
1024
+ const stashListBefore = execFileSync("git", ["stash", "list"], {
1025
+ cwd: basePath,
1026
+ stdio: ["ignore", "pipe", "pipe"],
1027
+ encoding: "utf-8",
1028
+ });
1029
+ execFileSync(
1030
+ "git",
1031
+ ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`],
1032
+ {
1033
+ cwd: basePath,
1034
+ stdio: ["ignore", "pipe", "pipe"],
1035
+ encoding: "utf-8",
1036
+ },
1037
+ );
1038
+ const stashListAfter = execFileSync("git", ["stash", "list"], {
1039
+ cwd: basePath,
1040
+ stdio: ["ignore", "pipe", "pipe"],
1041
+ encoding: "utf-8",
1042
+ });
1043
+ stashed = stashListAfter !== stashListBefore;
1044
+ }
1045
+
1046
+ // Checkout and stash-restore are split so we can distinguish two failure
1047
+ // modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
1048
+ // (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
1049
+ // the working-tree changes remain in the stash list. We surface a distinct
1050
+ // error in case (b) so callers don't assume the branch switch was rolled back.
1051
+ try {
1052
+ nativeCheckoutBranch(basePath, branch);
1053
+ } catch (checkoutErr) {
1054
+ if (stashed) {
1055
+ try {
1056
+ popStashByRef(basePath, stashMarker);
1057
+ } catch (restoreErr) {
1058
+ logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
1059
+ }
1060
+ }
1061
+ throw checkoutErr;
1062
+ }
1063
+
1064
+ if (stashed) {
1065
+ try {
1066
+ popStashByRef(basePath, stashMarker);
1067
+ } catch (popErr) {
1068
+ const msg = popErr instanceof Error ? popErr.message : String(popErr);
1069
+ const wrapped = new Error(
1070
+ `checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`,
1071
+ );
1072
+ const ref = (popErr as { stashRef?: string } | null)?.stashRef;
1073
+ if (ref) (wrapped as { stashRef?: string }).stashRef = ref;
1074
+ throw wrapped;
1075
+ }
1076
+ }
1011
1077
  }
1012
1078
 
1013
1079
  // ─── Public API ────────────────────────────────────────────────────────────
@@ -1992,14 +2058,6 @@ export function mergeMilestoneToMain(
1992
2058
  logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
1993
2059
  }
1994
2060
 
1995
- if (needsDbCycle && dbPathToReopen) {
1996
- try {
1997
- openDatabase(dbPathToReopen);
1998
- } catch (err) {
1999
- logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
2000
- }
2001
- }
2002
-
2003
2061
  // 7b. Clean up stale merge state before attempting squash merge (#2912).
2004
2062
  // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
2005
2063
  // or interrupted operation) causes `git merge --squash` to refuse with
@@ -2009,6 +2067,13 @@ export function mergeMilestoneToMain(
2009
2067
 
2010
2068
  // 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
2011
2069
  const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
2070
+ if (needsDbCycle && dbPathToReopen) {
2071
+ try {
2072
+ openDatabase(dbPathToReopen);
2073
+ } catch (err) {
2074
+ logWarning("worktree", `post-merge db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
2075
+ }
2076
+ }
2012
2077
 
2013
2078
  if (!mergeResult.success) {
2014
2079
  // Dirty working tree — the merge was rejected before it started (e.g.
@@ -317,6 +317,13 @@ import { normalizeRealPath } from "./paths.js";
317
317
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
318
318
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
319
319
 
320
+ export function formatAutoStopNotification(prefix: string, totals: { cost: number; tokens: { total: number } }, unitCount: number): string {
321
+ return [
322
+ `${prefix}.`,
323
+ `Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${unitCount} units`,
324
+ ].join("\n");
325
+ }
326
+
320
327
  /**
321
328
  * Phase B — register this auto-mode process in the workers table so other
322
329
  * workers and janitors can detect liveness via heartbeat. Best-effort: if
@@ -1417,7 +1424,7 @@ export async function stopAuto(
1417
1424
  if (ledger && ledger.units.length > 0) {
1418
1425
  const totals = getProjectTotals(ledger.units);
1419
1426
  ctx?.ui.notify(
1420
- `${notificationPrefix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
1427
+ formatAutoStopNotification(notificationPrefix, totals, ledger.units.length),
1421
1428
  "info",
1422
1429
  );
1423
1430
  } else {
@@ -1724,7 +1731,6 @@ export async function pauseAuto(
1724
1731
  restoreProjectRootEnv();
1725
1732
  restoreMilestoneLockEnv();
1726
1733
  s.pendingVerificationRetry = null;
1727
- s.verificationRetryCount.clear();
1728
1734
  ctx?.ui.setStatus("gsd-auto", "paused");
1729
1735
  ctx?.ui.setWidget("gsd-progress", undefined);
1730
1736
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
@@ -32,6 +32,7 @@ import { shouldIgnoreAgentEndForActiveUnit } from "../auto/unit-runner-events.js
32
32
  import { resolveModelId } from "../auto-model-selection.js";
33
33
  import { resolveProjectRoot } from "../worktree.js";
34
34
  import { clearDiscussionFlowState } from "./write-gate.js";
35
+ import { clearGuidedUnitContext } from "../guided-unit-context.js";
35
36
  import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
36
37
  import {
37
38
  classifyError,
@@ -99,6 +100,14 @@ export function isUserInitiatedAbortMessage(message: string | undefined | null):
99
100
  return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
100
101
  }
101
102
 
103
+ export function shouldDeferTransientErrorToCoreRetry(
104
+ cls: ErrorClass,
105
+ rawErrorMsg: string,
106
+ ): boolean {
107
+ if (!isTransient(cls) || cls.kind === "rate-limit") return false;
108
+ return !/retry failed after \d+ attempts:/i.test(rawErrorMsg);
109
+ }
110
+
102
111
  function isBareClaudeCodeSessionSwitchAbortMarker(message: string | undefined | null): boolean {
103
112
  if (!message) return false;
104
113
  const normalized = message.trim().replace(/\s+/g, " ").toLowerCase();
@@ -201,6 +210,14 @@ export function resolveAgentEndErrorDisplay(
201
210
  return rawErrorMsg;
202
211
  }
203
212
 
213
+ export function isTerminalDeletedWorktreeProviderError(
214
+ message: string | undefined | null,
215
+ ): boolean {
216
+ if (!message) return false;
217
+ if (!/\bdoes not exist\b/i.test(message)) return false;
218
+ return /[/\\]\.gsd[/\\](?:projects[/\\][^/\\]+[/\\])?worktrees[/\\][^/\\\s"']+/i.test(message);
219
+ }
220
+
204
221
  async function pauseTransientWithBackoff(
205
222
  cls: ErrorClass,
206
223
  pi: ExtensionAPI,
@@ -249,9 +266,11 @@ export async function handleAgentEnd(
249
266
  // falsely report files as missing — producing a spurious "ready signal
250
267
  // rejected" loop even though the files are on disk.
251
268
  clearPathCache();
269
+ const basePath = resolveAgentEndBasePath();
270
+ clearGuidedUnitContext(basePath);
252
271
 
253
272
  try {
254
- if (await checkDeepProjectSetupAfterTurn(event, ctx, resolveAgentEndBasePath())) {
273
+ if (await checkDeepProjectSetupAfterTurn(event, ctx, basePath)) {
255
274
  return;
256
275
  }
257
276
  } catch (err) {
@@ -259,8 +278,8 @@ export async function handleAgentEnd(
259
278
  logWarning("bootstrap", `checkDeepProjectSetupAfterTurn failed: ${message}`);
260
279
  }
261
280
 
262
- if (checkAutoStartAfterDiscuss()) {
263
- clearDiscussionFlowState(resolveAgentEndBasePath() ?? process.cwd());
281
+ if (checkAutoStartAfterDiscuss(basePath)) {
282
+ clearDiscussionFlowState(basePath ?? process.cwd());
264
283
  return;
265
284
  }
266
285
 
@@ -268,14 +287,14 @@ export async function handleAgentEnd(
268
287
  // are missing, `checkAutoStartAfterDiscuss` returns false silently. Surface
269
288
  // that and nudge the LLM to complete the writes before the user hits the
270
289
  // downstream "All milestones complete" warning loop.
271
- if (maybeHandleReadyPhraseWithoutFiles(event)) return;
290
+ if (maybeHandleReadyPhraseWithoutFiles(event, basePath)) return;
272
291
 
273
292
  // #4573 — Empty-turn recovery: if the LLM announced intent in prose but
274
293
  // emitted no tool calls, nudge it to execute. Fires only when auto-mode is
275
294
  // active or a discussion autostart is pending (non-auto interactive discuss
276
295
  // is user-driven). Runs before `isAutoActive` early return so pending
277
296
  // discussions (where isAutoActive may be false) still get recovered.
278
- if (maybeHandleEmptyIntentTurn(event, isAutoActive())) return;
297
+ if (maybeHandleEmptyIntentTurn(event, isAutoActive(), basePath)) return;
279
298
 
280
299
  if (!isAutoActive()) return;
281
300
 
@@ -360,6 +379,17 @@ export async function handleAgentEnd(
360
379
  rawErrorMsg,
361
380
  "content" in lastMsg ? lastMsg.content : undefined,
362
381
  );
382
+ if (
383
+ isAutoCompletionStopInProgress() &&
384
+ isTerminalDeletedWorktreeProviderError(`${rawErrorMsg}\n${displayMsg}`)
385
+ ) {
386
+ resetRetryState(retryState);
387
+ logWarning(
388
+ "bootstrap",
389
+ `Ignoring stale deleted-worktree provider error during terminal completion reroot: ${displayMsg || rawErrorMsg}`,
390
+ );
391
+ return;
392
+ }
363
393
  const errorDetail = displayMsg ? `: ${displayMsg}` : "";
364
394
  const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
365
395
 
@@ -466,7 +496,7 @@ export async function handleAgentEnd(
466
496
  // Core retries transient failures in-session after this handler.
467
497
  // Keep that behavior for non-rate-limit classes to avoid pause/retry races,
468
498
  // but let rate-limit continue into model fallback logic below (#4373).
469
- if (isTransient(cls) && cls.kind !== "rate-limit") {
499
+ if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg)) {
470
500
  return;
471
501
  }
472
502
 
@@ -31,6 +31,7 @@ import { resolveWorktreeProjectRoot } from "../worktree-root.js";
31
31
  import { extractSubagentAgentClasses } from "./subagent-input.js";
32
32
  import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserApprovalQuestion } from "../user-input-boundary.js";
33
33
  import { resolveSkillManifest } from "../skill-manifest.js";
34
+ import { getGuidedUnitContext } from "../guided-unit-context.js";
34
35
 
35
36
  let approvalQuestionAbortInFlight = false;
36
37
 
@@ -772,7 +773,8 @@ export function registerHooks(
772
773
  // subagent dispatch. Closes the b23 bug class where a discuss-milestone
773
774
  // turn used the host Edit tool to modify user source files.
774
775
  const dash = getAutoRuntimeSnapshot();
775
- const activeUnitType = dash.currentUnit?.type;
776
+ const guidedUnit = getGuidedUnitContext(discussionBasePath);
777
+ const activeUnitType = dash.currentUnit?.type ?? guidedUnit?.unitType;
776
778
  if (activeUnitType) {
777
779
  const manifest = resolveManifest(activeUnitType);
778
780
  if (manifest) {
@@ -791,7 +793,7 @@ export function registerHooks(
791
793
  const planningGuard = shouldBlockPlanningUnit(
792
794
  event.toolName,
793
795
  planningInput,
794
- dash.basePath || discussionBasePath,
796
+ dash.basePath || guidedUnit?.basePath || discussionBasePath,
795
797
  activeUnitType,
796
798
  manifest.tools,
797
799
  agentClasses,
@@ -587,10 +587,15 @@ async function configureModels(ctx: ExtensionCommandContext, prefs: Record<strin
587
587
  const models: Record<string, unknown> = (prefs.models as Record<string, unknown>) ?? {};
588
588
 
589
589
  const availableModels = ctx.modelRegistry.getAvailable();
590
- if (availableModels.length > 0) {
590
+ const getAllWithDiscovered = (ctx.modelRegistry as { getAllWithDiscovered?: () => typeof availableModels }).getAllWithDiscovered;
591
+ const availableProviders = new Set(availableModels.map((m) => m.provider));
592
+ const selectableModels = typeof getAllWithDiscovered === "function"
593
+ ? getAllWithDiscovered().filter((m) => availableProviders.has(m.provider))
594
+ : availableModels;
595
+ if (selectableModels.length > 0) {
591
596
  // Group models by provider, sorted alphabetically
592
- const byProvider = new Map<string, typeof availableModels>();
593
- for (const m of availableModels) {
597
+ const byProvider = new Map<string, typeof selectableModels>();
598
+ for (const m of selectableModels) {
594
599
  let group = byProvider.get(m.provider);
595
600
  if (!group) {
596
601
  group = [];
@@ -31,8 +31,10 @@ import {
31
31
  findStaleWorkerForProject,
32
32
  getAllAutoWorkers,
33
33
  markWorkerCrashed,
34
+ markWorkerStopping,
34
35
  type AutoWorkerRow,
35
36
  } from "./db/auto-workers.js";
37
+ import { forceReleaseLeasesForWorker } from "./db/milestone-leases.js";
36
38
  import { markLatestActiveForWorkerCanceled, type DispatchStatus } from "./db/unit-dispatches.js";
37
39
  import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
38
40
  import { _getAdapter, isDbAvailable } from "./gsd-db.js";
@@ -219,9 +221,21 @@ export function clearLock(basePath: string): void {
219
221
  if (!isDbAvailable()) return;
220
222
  try {
221
223
  const projectRoot = normalizeRealPath(basePath);
224
+ const staleWorker = findStaleWorkerForProject(projectRoot);
225
+ if (staleWorker) {
226
+ markWorkerCrashed(staleWorker.worker_id);
227
+ forceReleaseLeasesForWorker(staleWorker.worker_id);
228
+ deleteRuntimeKv("worker", staleWorker.worker_id, SESSION_FILE_KV_KEY);
229
+ return;
230
+ }
222
231
  const worker = findActiveWorkerForCurrentProcess(projectRoot);
223
- if (!worker) return;
224
- deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
232
+ if (worker) deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
233
+
234
+ const stale = findStaleWorkerForProject(projectRoot);
235
+ if (stale) {
236
+ markWorkerStopping(stale.worker_id);
237
+ deleteRuntimeKv("worker", stale.worker_id, SESSION_FILE_KV_KEY);
238
+ }
225
239
  } catch {
226
240
  // Best-effort.
227
241
  }
@@ -255,6 +255,32 @@ export function releaseMilestoneLease(
255
255
  });
256
256
  }
257
257
 
258
+ /**
259
+ * Force-release all held leases for a worker.
260
+ *
261
+ * Used by crash recovery once PID liveness has confirmed the worker is dead.
262
+ * No fencing token is required because this path is cleanup-only for a
263
+ * non-running process.
264
+ */
265
+ export function forceReleaseLeasesForWorker(workerId: string): number {
266
+ if (!isDbAvailable()) return 0;
267
+ const db = _getAdapter()!;
268
+ let changes = 0;
269
+ transaction(() => {
270
+ const result = db.prepare(
271
+ `UPDATE milestone_leases
272
+ SET status = 'released'
273
+ WHERE worker_id = :worker_id
274
+ AND status = 'held'`,
275
+ ).run({ ":worker_id": workerId });
276
+ changes =
277
+ typeof (result as { changes?: unknown }).changes === "number"
278
+ ? (result as { changes: number }).changes
279
+ : 0;
280
+ });
281
+ return changes;
282
+ }
283
+
258
284
  /**
259
285
  * Read current lease row for diagnostics. Returns null if no row exists.
260
286
  */
@@ -9,7 +9,7 @@ import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
9
9
  import { isDbAvailable, getMilestone } from "./gsd-db.js";
10
10
  import { resolveMilestoneFile } from "./paths.js";
11
11
  import { deriveState, isMilestoneComplete } from "./state.js";
12
- import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
12
+ import { createWorktree, listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
13
13
  import { abortAndReset } from "./git-self-heal.js";
14
14
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
15
15
  import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
@@ -54,6 +54,19 @@ function isSameOrNestedPath(candidate: string, container: string): boolean {
54
54
  normalizedCandidate.startsWith(`${normalizedContainer}/`);
55
55
  }
56
56
 
57
+ function hasProjectContentOnDisk(dirPath: string): boolean {
58
+ try {
59
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
60
+ if (entry.name === ".git" || entry.name === ".gsd") continue;
61
+ if (entry.name === ".DS_Store") continue;
62
+ return true;
63
+ }
64
+ } catch {
65
+ return false;
66
+ }
67
+ return false;
68
+ }
69
+
57
70
  function getSnapshotDiffCheckFailure(basePath: string): string | null {
58
71
  const failures: string[] = [];
59
72
 
@@ -123,6 +136,37 @@ export async function checkGitHealth(
123
136
  ? await isCompletedMilestoneTerminal(basePath, milestoneId)
124
137
  : false;
125
138
 
139
+ if (!isComplete && !hasProjectContentOnDisk(wt.path) && hasProjectContentOnDisk(basePath)) {
140
+ issues.push({
141
+ severity: "error",
142
+ code: "worktree_empty_with_project_content",
143
+ scope: "milestone",
144
+ unitId: milestoneId,
145
+ message: `Worktree ${wt.path} has no project content, but project root ${basePath} does. Run doctor --fix to recreate the worktree.`,
146
+ fixable: true,
147
+ });
148
+
149
+ if (shouldFix("worktree_empty_with_project_content")) {
150
+ try {
151
+ nativeWorktreeRemove(basePath, wt.path, true);
152
+ const recreated = createWorktree(basePath, milestoneId, {
153
+ branch: wt.branch,
154
+ reuseExistingBranch: true,
155
+ });
156
+ const reset = spawnSync("git", ["reset", "--hard"], {
157
+ cwd: recreated.path,
158
+ encoding: "utf-8",
159
+ });
160
+ if (reset.status !== 0) {
161
+ throw new Error(reset.stderr || reset.error?.message || "git reset --hard failed");
162
+ }
163
+ fixesApplied.push(`recreated empty worktree ${wt.path}`);
164
+ } catch {
165
+ fixesApplied.push(`failed to recreate empty worktree ${wt.path}`);
166
+ }
167
+ }
168
+ }
169
+
126
170
  if (isComplete) {
127
171
  issues.push({
128
172
  severity: "warning",
@@ -51,6 +51,7 @@ export type DoctorIssueCode =
51
51
  // Git / worktree integrity checks
52
52
  | "integration_branch_missing"
53
53
  | "worktree_directory_orphaned"
54
+ | "worktree_empty_with_project_content"
54
55
  // GSD state structural checks
55
56
  | "circular_slice_dependency"
56
57
  | "orphaned_slice_directory"
@@ -388,6 +388,9 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
388
388
  }
389
389
  }
390
390
 
391
+ /** Re-export for backward compatibility — canonical definitions in branch-patterns.ts */
392
+ export { QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
393
+
391
394
  /**
392
395
  * Persist the integration branch for a milestone.
393
396
  *
@@ -398,14 +401,14 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
398
401
  *
399
402
  * The file is committed immediately so the metadata is persisted in git.
400
403
  */
401
- /** Re-export for backward compatibility — canonical definitions in branch-patterns.ts */
402
- export { QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
403
-
404
404
  export function writeIntegrationBranch(
405
405
  basePath: string,
406
406
  milestoneId: string,
407
407
  branch: string,
408
408
  ): void {
409
+ // Never persist milestone branches as integration targets.
410
+ // They are ephemeral execution branches and can cause self-diff corruption.
411
+ if (branch.startsWith("milestone/")) return;
409
412
  // Don't record slice branches as the integration target
410
413
  if (SLICE_BRANCH_RE.test(branch)) return;
411
414
  // Don't record quick-task branches — they are ephemeral and merge back