gsd-pi 2.41.0-dev.0acbce9 → 2.41.0-dev.3557dc4

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 (213) hide show
  1. package/dist/cli-web-branch.d.ts +6 -0
  2. package/dist/cli-web-branch.js +17 -0
  3. package/dist/onboarding.js +2 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +9 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +26 -8
  6. package/dist/resources/extensions/gsd/auto-dashboard.js +6 -2
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +19 -2
  8. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -0
  9. package/dist/resources/extensions/gsd/auto-recovery.js +12 -4
  10. package/dist/resources/extensions/gsd/auto-start.js +8 -3
  11. package/dist/resources/extensions/gsd/auto-worktree.js +147 -13
  12. package/dist/resources/extensions/gsd/auto.js +36 -1
  13. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +199 -164
  14. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +62 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -0
  17. package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  19. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  20. package/dist/resources/extensions/gsd/context-store.js +4 -3
  21. package/dist/resources/extensions/gsd/db-writer.js +5 -2
  22. package/dist/resources/extensions/gsd/detection.js +1 -1
  23. package/dist/resources/extensions/gsd/doctor.js +11 -1
  24. package/dist/resources/extensions/gsd/exit-command.js +12 -2
  25. package/dist/resources/extensions/gsd/export.js +9 -13
  26. package/dist/resources/extensions/gsd/extension-manifest.json +2 -2
  27. package/dist/resources/extensions/gsd/files.js +28 -11
  28. package/dist/resources/extensions/gsd/forensics.js +10 -3
  29. package/dist/resources/extensions/gsd/git-service.js +5 -1
  30. package/dist/resources/extensions/gsd/gsd-db.js +25 -8
  31. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  32. package/dist/resources/extensions/gsd/guided-flow.js +7 -3
  33. package/dist/resources/extensions/gsd/journal.js +85 -0
  34. package/dist/resources/extensions/gsd/md-importer.js +5 -0
  35. package/dist/resources/extensions/gsd/milestone-ids.js +1 -1
  36. package/dist/resources/extensions/gsd/native-git-bridge.js +2 -2
  37. package/dist/resources/extensions/gsd/post-unit-hooks.js +24 -412
  38. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  39. package/dist/resources/extensions/gsd/preferences.js +1 -0
  40. package/dist/resources/extensions/gsd/prompt-loader.js +34 -4
  41. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
  42. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  43. package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  45. package/dist/resources/extensions/gsd/repo-identity.js +46 -2
  46. package/dist/resources/extensions/gsd/rule-registry.js +489 -0
  47. package/dist/resources/extensions/gsd/rule-types.js +6 -0
  48. package/dist/resources/extensions/gsd/service-tier.js +138 -0
  49. package/dist/resources/extensions/gsd/structured-data-formatter.js +2 -1
  50. package/dist/resources/extensions/gsd/templates/decisions.md +2 -2
  51. package/dist/resources/extensions/gsd/workflow-templates.js +13 -1
  52. package/dist/resources/extensions/gsd/worktree-manager.js +20 -6
  53. package/dist/resources/extensions/gsd/worktree-resolver.js +19 -2
  54. package/dist/resources/extensions/subagent/index.js +7 -3
  55. package/dist/resources/extensions/voice/index.js +4 -4
  56. package/dist/web/standalone/.next/BUILD_ID +1 -1
  57. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  58. package/dist/web/standalone/.next/build-manifest.json +3 -3
  59. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  60. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  62. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/index.html +1 -1
  93. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  100. package/dist/web/standalone/.next/server/chunks/229.js +3 -3
  101. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  104. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  105. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  106. package/dist/web/standalone/.next/static/chunks/4024.c195dc1fdd2adbea.js +9 -0
  107. package/dist/web/standalone/.next/static/chunks/{webpack-9afaaebf6042a1d7.js → webpack-fa307370fcf9fb2c.js} +1 -1
  108. package/dist/web-mode.d.ts +2 -0
  109. package/dist/web-mode.js +29 -7
  110. package/package.json +1 -1
  111. package/packages/native/src/__tests__/text.test.mjs +33 -0
  112. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +3 -1
  113. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +10 -7
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  117. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +4 -2
  118. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +11 -7
  119. package/src/resources/extensions/gsd/auto/loop-deps.ts +5 -1
  120. package/src/resources/extensions/gsd/auto/loop.ts +10 -1
  121. package/src/resources/extensions/gsd/auto/phases.ts +28 -8
  122. package/src/resources/extensions/gsd/auto/types.ts +4 -0
  123. package/src/resources/extensions/gsd/auto-dashboard.ts +7 -2
  124. package/src/resources/extensions/gsd/auto-dispatch.ts +25 -5
  125. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -0
  126. package/src/resources/extensions/gsd/auto-recovery.ts +12 -4
  127. package/src/resources/extensions/gsd/auto-start.ts +8 -3
  128. package/src/resources/extensions/gsd/auto-worktree.ts +162 -18
  129. package/src/resources/extensions/gsd/auto.ts +40 -1
  130. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +209 -162
  131. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +62 -0
  132. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  133. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
  134. package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
  135. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  136. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  137. package/src/resources/extensions/gsd/context-store.ts +4 -3
  138. package/src/resources/extensions/gsd/db-writer.ts +6 -2
  139. package/src/resources/extensions/gsd/detection.ts +1 -1
  140. package/src/resources/extensions/gsd/doctor.ts +12 -1
  141. package/src/resources/extensions/gsd/exit-command.ts +14 -2
  142. package/src/resources/extensions/gsd/export.ts +8 -15
  143. package/src/resources/extensions/gsd/extension-manifest.json +2 -2
  144. package/src/resources/extensions/gsd/files.ts +29 -12
  145. package/src/resources/extensions/gsd/forensics.ts +9 -3
  146. package/src/resources/extensions/gsd/git-service.ts +5 -4
  147. package/src/resources/extensions/gsd/gsd-db.ts +37 -8
  148. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  149. package/src/resources/extensions/gsd/guided-flow.ts +7 -3
  150. package/src/resources/extensions/gsd/journal.ts +134 -0
  151. package/src/resources/extensions/gsd/md-importer.ts +6 -0
  152. package/src/resources/extensions/gsd/milestone-ids.ts +1 -1
  153. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -2
  154. package/src/resources/extensions/gsd/post-unit-hooks.ts +24 -462
  155. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  156. package/src/resources/extensions/gsd/preferences.ts +1 -0
  157. package/src/resources/extensions/gsd/prompt-loader.ts +35 -4
  158. package/src/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
  159. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  160. package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
  161. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  162. package/src/resources/extensions/gsd/repo-identity.ts +47 -2
  163. package/src/resources/extensions/gsd/rule-registry.ts +599 -0
  164. package/src/resources/extensions/gsd/rule-types.ts +68 -0
  165. package/src/resources/extensions/gsd/service-tier.ts +171 -0
  166. package/src/resources/extensions/gsd/structured-data-formatter.ts +3 -1
  167. package/src/resources/extensions/gsd/templates/decisions.md +2 -2
  168. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +3 -2
  169. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +85 -0
  170. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +202 -0
  171. package/src/resources/extensions/gsd/tests/context-store.test.ts +10 -5
  172. package/src/resources/extensions/gsd/tests/db-writer.test.ts +10 -0
  173. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +15 -10
  174. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +5 -4
  175. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +167 -0
  176. package/src/resources/extensions/gsd/tests/doctor-task-done-missing-summary-slice-loop.test.ts +174 -0
  177. package/src/resources/extensions/gsd/tests/exit-command.test.ts +55 -0
  178. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +8 -1
  179. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +7 -7
  180. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +513 -0
  181. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +147 -0
  182. package/src/resources/extensions/gsd/tests/journal.test.ts +386 -0
  183. package/src/resources/extensions/gsd/tests/md-importer.test.ts +31 -1
  184. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  185. package/src/resources/extensions/gsd/tests/milestone-id-reservation.test.ts +1 -1
  186. package/src/resources/extensions/gsd/tests/parsers.test.ts +110 -0
  187. package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -25
  188. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +3 -1
  189. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +61 -1
  190. package/src/resources/extensions/gsd/tests/routing-history.test.ts +11 -22
  191. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +413 -0
  192. package/src/resources/extensions/gsd/tests/service-tier.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +2 -2
  194. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +102 -0
  195. package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +4 -3
  196. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +117 -0
  197. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -1
  198. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +99 -0
  199. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -0
  200. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +4 -0
  201. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +178 -0
  202. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -3
  203. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +140 -0
  204. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +74 -0
  205. package/src/resources/extensions/gsd/types.ts +3 -0
  206. package/src/resources/extensions/gsd/workflow-templates.ts +12 -1
  207. package/src/resources/extensions/gsd/worktree-manager.ts +21 -6
  208. package/src/resources/extensions/gsd/worktree-resolver.ts +30 -9
  209. package/src/resources/extensions/subagent/index.ts +7 -3
  210. package/src/resources/extensions/voice/index.ts +4 -4
  211. package/dist/web/standalone/.next/static/chunks/4024.279c423e4661ece1.js +0 -9
  212. /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → JBSIr4fSfHXs5g5x2ZBSC}/_buildManifest.js +0 -0
  213. /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → JBSIr4fSfHXs5g5x2ZBSC}/_ssgManifest.js +0 -0
@@ -26,6 +26,7 @@ import { runUnit } from "./run-unit.js";
26
26
  import { debugLog } from "../debug-logger.js";
27
27
  import { gsdRoot } from "../paths.js";
28
28
  import { atomicWriteSync } from "../atomic-write.js";
29
+ import { PROJECT_FILES } from "../detection.js";
29
30
  import { join } from "node:path";
30
31
 
31
32
  // ─── generateMilestoneReport ──────────────────────────────────────────────────
@@ -192,6 +193,7 @@ export async function runPreDispatch(
192
193
 
193
194
  // ── Milestone transition ────────────────────────────────────────────
194
195
  if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
196
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "milestone-transition", data: { from: s.currentMilestoneId, to: mid } });
195
197
  ctx.ui.notify(
196
198
  `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
197
199
  "info",
@@ -386,6 +388,7 @@ export async function runPreDispatch(
386
388
  );
387
389
  }
388
390
  debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
391
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "no-active-milestone" } });
389
392
  return { action: "break", reason: "no-active-milestone" };
390
393
  }
391
394
 
@@ -454,6 +457,7 @@ export async function runPreDispatch(
454
457
  );
455
458
  await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
456
459
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
460
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
457
461
  return { action: "break", reason: "milestone-complete" };
458
462
  }
459
463
 
@@ -465,6 +469,7 @@ export async function runPreDispatch(
465
469
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
466
470
  deps.logCmuxEvent(prefs, blockerMsg, "error");
467
471
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
472
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
468
473
  return { action: "break", reason: "blocked" };
469
474
  }
470
475
 
@@ -497,6 +502,7 @@ export async function runDispatch(
497
502
  });
498
503
 
499
504
  if (dispatchResult.action === "stop") {
505
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-stop", rule: dispatchResult.matchedRule, data: { reason: dispatchResult.reason } });
500
506
  await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
501
507
  debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
502
508
  return { action: "break", reason: "dispatch-stop" };
@@ -508,6 +514,8 @@ export async function runDispatch(
508
514
  return { action: "continue" };
509
515
  }
510
516
 
517
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-match", rule: dispatchResult.matchedRule, data: { unitType: dispatchResult.unitType, unitId: dispatchResult.unitId } });
518
+
511
519
  let unitType = dispatchResult.unitType;
512
520
  let unitId = dispatchResult.unitId;
513
521
  let prompt = dispatchResult.prompt;
@@ -600,6 +608,7 @@ export async function runDispatch(
600
608
  `Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
601
609
  "info",
602
610
  );
611
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
603
612
  }
604
613
  if (preDispatchResult.action === "skip") {
605
614
  ctx.ui.notify(
@@ -809,25 +818,27 @@ export async function runUnitPhase(
809
818
  unitId,
810
819
  });
811
820
 
812
- // ── Worktree health check (#1833) ───────────────────────────────────
821
+ // ── Worktree health check (#1833, #1843) ────────────────────────────
813
822
  // Verify the working directory is a valid git checkout with project
814
823
  // files before dispatching work. A broken worktree causes agents to
815
824
  // hallucinate summaries since they cannot read or write any files.
825
+ // Uses the shared PROJECT_FILES list from detection.ts to support all
826
+ // ecosystems (Rust, Go, Python, Java, etc.), not just JS.
816
827
  if (s.basePath && unitType === "execute-task") {
817
828
  const gitMarker = join(s.basePath, ".git");
818
829
  const hasGit = deps.existsSync(gitMarker);
819
- const hasPackageJson = deps.existsSync(join(s.basePath, "package.json"));
820
- const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
821
830
  if (!hasGit) {
822
831
  const msg = `Worktree health check failed: ${s.basePath} has no .git — refusing to dispatch ${unitType} ${unitId}`;
823
- debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasGit, hasPackageJson, hasSrcDir });
832
+ debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasGit });
824
833
  ctx.ui.notify(msg, "error");
825
834
  await deps.stopAuto(ctx, pi, msg);
826
835
  return { action: "break", reason: "worktree-invalid" };
827
836
  }
828
- if (!hasPackageJson && !hasSrcDir) {
829
- const msg = `Worktree health check failed: ${s.basePath} has no package.json or src/ — refusing to dispatch ${unitType} ${unitId}`;
830
- debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasGit, hasPackageJson, hasSrcDir });
837
+ const hasProjectFile = PROJECT_FILES.some((f) => deps.existsSync(join(s.basePath, f)));
838
+ const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
839
+ if (!hasProjectFile && !hasSrcDir) {
840
+ const msg = `Worktree health check failed: ${s.basePath} has no recognized project files — refusing to dispatch ${unitType} ${unitId}`;
841
+ debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasProjectFile, hasSrcDir });
831
842
  ctx.ui.notify(msg, "error");
832
843
  await deps.stopAuto(ctx, pi, msg);
833
844
  return { action: "break", reason: "worktree-invalid" };
@@ -843,6 +854,8 @@ export async function runUnitPhase(
843
854
  const previousTier = s.currentUnitRouting?.tier;
844
855
 
845
856
  s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
857
+ const unitStartSeq = ic.nextSeq();
858
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
846
859
  deps.captureAvailableSkills();
847
860
  deps.writeUnitRuntimeRecord(
848
861
  s.basePath,
@@ -988,7 +1001,12 @@ export async function runUnitPhase(
988
1001
  unitId,
989
1002
  prefs,
990
1003
  buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
991
- buildRecoveryContext: () => ({}),
1004
+ buildRecoveryContext: () => ({
1005
+ basePath: s.basePath,
1006
+ verbose: s.verbose,
1007
+ currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
1008
+ unitRecoveryCount: s.unitRecoveryCount,
1009
+ }),
992
1010
  pauseAuto: deps.pauseAuto,
993
1011
  });
994
1012
 
@@ -1141,6 +1159,8 @@ export async function runUnitPhase(
1141
1159
  s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
1142
1160
  }
1143
1161
 
1162
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
1163
+
1144
1164
  return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
1145
1165
  }
1146
1166
 
@@ -69,6 +69,10 @@ export interface IterationContext {
69
69
  deps: LoopDeps;
70
70
  prefs: GSDPreferences | undefined;
71
71
  iteration: number;
72
+ /** UUID grouping all journal events for this iteration. */
73
+ flowId: string;
74
+ /** Returns the next monotonically increasing sequence number (1-based, reset per iteration). */
75
+ nextSeq: () => number;
72
76
  }
73
77
 
74
78
  export interface LoopState {
@@ -24,6 +24,7 @@ import { GLYPH, INDENT } from "../shared/mod.js";
24
24
  import { computeProgressScore } from "./progress-score.js";
25
25
  import { getActiveWorktreeName } from "./worktree-command.js";
26
26
  import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
27
+ import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js";
27
28
 
28
29
  // ─── UAT Slice Extraction ─────────────────────────────────────────────────────
29
30
 
@@ -460,6 +461,9 @@ export function updateProgressWidget(
460
461
  // Pre-fetch last commit for display
461
462
  refreshLastCommit(accessors.getBasePath());
462
463
 
464
+ // Cache the effective service tier at widget creation time (reads preferences)
465
+ const effectiveServiceTier = getEffectiveServiceTier();
466
+
463
467
  ctx.ui.setWidget("gsd-progress", (tui, theme) => {
464
468
  let pulseBright = true;
465
469
  let cachedLines: string[] | undefined;
@@ -572,9 +576,10 @@ export function updateProgressWidget(
572
576
  // Model display — shown in context section, not stats
573
577
  const modelId = cmdCtx?.model?.id ?? "";
574
578
  const modelProvider = cmdCtx?.model?.provider ?? "";
575
- const modelDisplay = modelProvider && modelId
579
+ const tierIcon = resolveServiceTierIcon(effectiveServiceTier, modelId);
580
+ const modelDisplay = (modelProvider && modelId
576
581
  ? `${modelProvider}/${modelId}`
577
- : modelId;
582
+ : modelId) + (tierIcon ? ` ${tierIcon}` : "");
578
583
 
579
584
  // ── Mode: off — return empty ──────────────────────────────────
580
585
  if (widgetMode === "off") {
@@ -54,9 +54,11 @@ export type DispatchAction =
54
54
  unitId: string;
55
55
  prompt: string;
56
56
  pauseAfterDispatch?: boolean;
57
+ /** Name of the matched dispatch rule from the unified registry (journal provenance). */
58
+ matchedRule?: string;
57
59
  }
58
- | { action: "stop"; reason: string; level: "info" | "warning" | "error" }
59
- | { action: "skip" };
60
+ | { action: "stop"; reason: string; level: "info" | "warning" | "error"; matchedRule?: string }
61
+ | { action: "skip"; matchedRule?: string };
60
62
 
61
63
  export interface DispatchContext {
62
64
  basePath: string;
@@ -67,7 +69,7 @@ export interface DispatchContext {
67
69
  session?: import("./auto/session.js").AutoSession;
68
70
  }
69
71
 
70
- interface DispatchRule {
72
+ export interface DispatchRule {
71
73
  /** Human-readable name for debugging and test identification */
72
74
  name: string;
73
75
  /** Return a DispatchAction if this rule matches, null to fall through */
@@ -88,7 +90,7 @@ const MAX_REWRITE_ATTEMPTS = 3;
88
90
 
89
91
  // ─── Rules ────────────────────────────────────────────────────────────────
90
92
 
91
- const DISPATCH_RULES: DispatchRule[] = [
93
+ export const DISPATCH_RULES: DispatchRule[] = [
92
94
  {
93
95
  name: "rewrite-docs (override gate)",
94
96
  match: async ({ mid, midTitle, state, basePath, session }) => {
@@ -608,18 +610,35 @@ const DISPATCH_RULES: DispatchRule[] = [
608
610
  },
609
611
  ];
610
612
 
613
+ import { getRegistry } from "./rule-registry.js";
614
+
611
615
  // ─── Resolver ─────────────────────────────────────────────────────────────
612
616
 
613
617
  /**
614
618
  * Evaluate dispatch rules in order. Returns the first matching action,
615
619
  * or a "stop" action if no rule matches (unhandled phase).
620
+ *
621
+ * Delegates to the RuleRegistry when initialized; falls back to inline
622
+ * loop over DISPATCH_RULES for backward compatibility (tests that import
623
+ * resolveDispatch directly without registry initialization).
616
624
  */
617
625
  export async function resolveDispatch(
618
626
  ctx: DispatchContext,
619
627
  ): Promise<DispatchAction> {
628
+ // Delegate to registry when available
629
+ try {
630
+ const registry = getRegistry();
631
+ return await registry.evaluateDispatch(ctx);
632
+ } catch {
633
+ // Registry not initialized — fall back to inline loop
634
+ }
635
+
620
636
  for (const rule of DISPATCH_RULES) {
621
637
  const result = await rule.match(ctx);
622
- if (result) return result;
638
+ if (result) {
639
+ if (result.action !== "skip") result.matchedRule = rule.name;
640
+ return result;
641
+ }
623
642
  }
624
643
 
625
644
  // No rule matched — unhandled phase
@@ -627,6 +646,7 @@ export async function resolveDispatch(
627
646
  action: "stop",
628
647
  reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
629
648
  level: "info",
649
+ matchedRule: "<no-match>",
630
650
  };
631
651
  }
632
652
 
@@ -59,6 +59,7 @@ import { existsSync, unlinkSync } from "node:fs";
59
59
  import { join } from "node:path";
60
60
  import { uncheckTaskInPlan } from "./undo.js";
61
61
  import { atomicWriteSync } from "./atomic-write.js";
62
+ import { _resetHasChangesCache } from "./native-git-bridge.js";
62
63
 
63
64
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
64
65
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
@@ -156,6 +157,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
156
157
  }
157
158
  }
158
159
 
160
+ // Invalidate the nativeHasChanges cache before auto-commit (#1853).
161
+ // The cache has a 10-second TTL and is keyed by basePath. A stale
162
+ // `false` result causes autoCommit to skip staging entirely, leaving
163
+ // code files only in the working tree where they are destroyed by
164
+ // `git worktree remove --force` during teardown.
165
+ _resetHasChangesCache();
166
+
159
167
  const commitMsg = autoCommitCurrentBranch(s.basePath, s.currentUnit.type, s.currentUnit.id, taskContext);
160
168
  if (commitMsg) {
161
169
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
@@ -319,10 +319,15 @@ export function verifyExpectedArtifact(
319
319
  // plan has no tasks, creating an infinite skip loop (#699).
320
320
  if (unitType === "plan-slice") {
321
321
  const planContent = readFileSync(absPath, "utf-8");
322
- if (!/^- \[[xX ]\] \*\*T\d+:/m.test(planContent)) return false;
322
+ // Accept checkbox-style (- [x] **T01: ...) or heading-style (### T01 -- / ### T01: / ### T01 —)
323
+ const hasCheckboxTask = /^- \[[xX ]\] \*\*T\d+:/m.test(planContent);
324
+ const hasHeadingTask = /^#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
325
+ if (!hasCheckboxTask && !hasHeadingTask) return false;
323
326
  }
324
327
 
325
- // execute-task must also have its checkbox marked [x] in the slice plan
328
+ // execute-task must also have its checkbox marked [x] in the slice plan.
329
+ // Heading-style plans (### T01 -- Title) have no checkbox — the task summary
330
+ // file existence (checked above via resolveExpectedArtifactPath) is sufficient.
326
331
  if (unitType === "execute-task") {
327
332
  const parts = unitId.split("/");
328
333
  const mid = parts[0];
@@ -333,8 +338,11 @@ export function verifyExpectedArtifact(
333
338
  if (planAbs && existsSync(planAbs)) {
334
339
  const planContent = readFileSync(planAbs, "utf-8");
335
340
  const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
336
- const re = new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m");
337
- if (!re.test(planContent)) return false;
341
+ const cbRe = new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m");
342
+ const hdRe = new RegExp(`^#{2,4}\\s+${escapedTid}\\s*(?:--|—|:)`, "m");
343
+ // Heading-style entries count as verified (no checkbox to toggle);
344
+ // checkbox-style entries require [x].
345
+ if (!cbRe.test(planContent) && !hdRe.test(planContent)) return false;
338
346
  }
339
347
  }
340
348
  }
@@ -20,7 +20,7 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
- import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
23
+ import { ensureGsdSymlink, isInheritedRepo, validateProjectId } from "./repo-identity.js";
24
24
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
25
25
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
26
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
@@ -140,8 +140,13 @@ export async function bootstrapAutoSession(
140
140
  return releaseLockAndReturn();
141
141
  }
142
142
 
143
- // Ensure git repo exists
144
- if (!nativeIsRepo(base)) {
143
+ // Ensure git repo exists.
144
+ // Guard against inherited repos: if `base` is a subdirectory of another
145
+ // git repo that has no .gsd (i.e. the parent project was never initialised
146
+ // with GSD), create a fresh git repo at `base` so it gets its own identity
147
+ // hash. Without this, repoIdentity() resolves to the parent repo's hash
148
+ // and loads milestones from an unrelated project (#1639).
149
+ if (!nativeIsRepo(base) || isInheritedRepo(base)) {
145
150
  const mainBranch =
146
151
  loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
147
152
  nativeInit(base, mainBranch);
@@ -25,12 +25,13 @@ import {
25
25
  isDbAvailable,
26
26
  } from "./gsd-db.js";
27
27
  import { atomicWriteSync } from "./atomic-write.js";
28
- import { execSync, execFileSync } from "node:child_process";
28
+ import { execFileSync } from "node:child_process";
29
29
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
30
30
  import { gsdRoot } from "./paths.js";
31
31
  import {
32
32
  createWorktree,
33
33
  removeWorktree,
34
+ resolveGitDir,
34
35
  worktreePath,
35
36
  } from "./worktree-manager.js";
36
37
  import {
@@ -57,6 +58,8 @@ import {
57
58
  nativeBranchDelete,
58
59
  nativeBranchExists,
59
60
  nativeDiffNumstat,
61
+ nativeUpdateRef,
62
+ nativeIsAncestor,
60
63
  } from "./native-git-bridge.js";
61
64
 
62
65
  // ─── Module State ──────────────────────────────────────────────────────────
@@ -182,7 +185,7 @@ export function syncGsdStateToWorktree(
182
185
  const mainMilestones = readdirSync(mainMilestonesDir, {
183
186
  withFileTypes: true,
184
187
  })
185
- .filter((d) => d.isDirectory() && /^M\d{3}/.test(d.name))
188
+ .filter((d) => d.isDirectory())
186
189
  .map((d) => d.name);
187
190
 
188
191
  for (const mid of mainMilestones) {
@@ -339,7 +342,7 @@ export function syncWorktreeStateBack(
339
342
 
340
343
  try {
341
344
  const wtMilestones = readdirSync(wtMilestonesDir, { withFileTypes: true })
342
- .filter((d) => d.isDirectory() && /^M\d{3}/.test(d.name))
345
+ .filter((d) => d.isDirectory())
343
346
  .map((d) => d.name);
344
347
 
345
348
  for (const mid of wtMilestones) {
@@ -468,14 +471,22 @@ export function runWorktreePostCreateHook(
468
471
  }
469
472
  if (!hookPath) return null;
470
473
 
471
- // Resolve relative paths against the source project root
472
- const resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
474
+ // Resolve relative paths against the source project root.
475
+ // On Windows, convert 8.3 short paths (e.g. RUNNER~1) to long paths
476
+ // so execFileSync can locate the file correctly.
477
+ let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
473
478
  if (!existsSync(resolved)) {
474
479
  return `Worktree post-create hook not found: ${resolved}`;
475
480
  }
481
+ if (process.platform === "win32") {
482
+ try { resolved = realpathSync.native(resolved); } catch { /* keep original */ }
483
+ }
476
484
 
477
485
  try {
478
- execSync(resolved, {
486
+ // .bat/.cmd files on Windows require shell mode — execFileSync cannot
487
+ // spawn them directly (EINVAL).
488
+ const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
489
+ execFileSync(resolved, [], {
479
490
  cwd: worktreeDir,
480
491
  env: {
481
492
  ...process.env,
@@ -485,6 +496,7 @@ export function runWorktreePostCreateHook(
485
496
  stdio: ["ignore", "pipe", "pipe"],
486
497
  encoding: "utf-8",
487
498
  timeout: 30_000, // 30 second timeout
499
+ shell: needsShell,
488
500
  });
489
501
  return null;
490
502
  } catch (err) {
@@ -761,6 +773,24 @@ export function teardownAutoWorktree(
761
773
  branch,
762
774
  deleteBranch: !preserveBranch,
763
775
  });
776
+
777
+ // Verify cleanup succeeded — warn if the worktree directory is still on disk.
778
+ // On Windows, bash-based cleanup can silently fail when paths contain
779
+ // backslashes (#1436), leaving ~1 GB+ orphaned directories.
780
+ const wtDir = worktreePath(originalBasePath, milestoneId);
781
+ if (existsSync(wtDir)) {
782
+ console.error(
783
+ `[GSD] WARNING: Worktree directory still exists after teardown: ${wtDir}\n` +
784
+ ` This is likely an orphaned directory consuming disk space.\n` +
785
+ ` Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`,
786
+ );
787
+ // Attempt a direct filesystem removal as a fallback
788
+ try {
789
+ rmSync(wtDir, { recursive: true, force: true });
790
+ } catch {
791
+ // Non-fatal — the warning above tells the user how to clean up
792
+ }
793
+ }
764
794
  }
765
795
 
766
796
  /**
@@ -940,7 +970,7 @@ export function mergeMilestoneToMain(
940
970
  originalBasePath_: string,
941
971
  milestoneId: string,
942
972
  roadmapContent: string,
943
- ): { commitMessage: string; pushed: boolean; prCreated: boolean } {
973
+ ): { commitMessage: string; pushed: boolean; prCreated: boolean; codeFilesChanged: boolean } {
944
974
  const worktreeCwd = process.cwd();
945
975
  const milestoneBranch = autoWorktreeBranch(milestoneId);
946
976
 
@@ -1002,6 +1032,62 @@ export function mergeMilestoneToMain(
1002
1032
  }
1003
1033
  const commitMessage = subject + body;
1004
1034
 
1035
+ // 6b. Reconcile worktree HEAD with milestone branch ref (#1846).
1036
+ // When the worktree HEAD detaches and advances past the named branch,
1037
+ // the branch ref becomes stale. Squash-merging the stale ref silently
1038
+ // orphans all commits between the branch ref and the actual worktree HEAD.
1039
+ // Fix: fast-forward the branch ref to the worktree HEAD before merging.
1040
+ // Only applies when merging from an actual worktree (worktreeCwd differs
1041
+ // from originalBasePath_).
1042
+ if (worktreeCwd !== originalBasePath_) {
1043
+ try {
1044
+ const worktreeHead = execFileSync("git", ["rev-parse", "HEAD"], {
1045
+ cwd: worktreeCwd,
1046
+ stdio: ["ignore", "pipe", "pipe"],
1047
+ encoding: "utf-8",
1048
+ }).trim();
1049
+ const branchHead = execFileSync("git", ["rev-parse", milestoneBranch], {
1050
+ cwd: originalBasePath_,
1051
+ stdio: ["ignore", "pipe", "pipe"],
1052
+ encoding: "utf-8",
1053
+ }).trim();
1054
+
1055
+ if (worktreeHead && branchHead && worktreeHead !== branchHead) {
1056
+ if (nativeIsAncestor(originalBasePath_, branchHead, worktreeHead)) {
1057
+ // Worktree HEAD is strictly ahead — fast-forward the branch ref
1058
+ nativeUpdateRef(
1059
+ originalBasePath_,
1060
+ `refs/heads/${milestoneBranch}`,
1061
+ worktreeHead,
1062
+ );
1063
+ debugLog("mergeMilestoneToMain", {
1064
+ action: "fast-forward-branch-ref",
1065
+ milestoneBranch,
1066
+ oldRef: branchHead.slice(0, 8),
1067
+ newRef: worktreeHead.slice(0, 8),
1068
+ });
1069
+ } else {
1070
+ // Diverged — fail loudly rather than silently losing commits
1071
+ process.chdir(previousCwd);
1072
+ throw new GSDError(
1073
+ GSD_GIT_ERROR,
1074
+ `Worktree HEAD (${worktreeHead.slice(0, 8)}) diverged from ` +
1075
+ `${milestoneBranch} (${branchHead.slice(0, 8)}). ` +
1076
+ `Manual reconciliation required before merge.`,
1077
+ );
1078
+ }
1079
+ }
1080
+ } catch (err) {
1081
+ // Re-throw GSDError (divergence); swallow rev-parse failures
1082
+ // (e.g. worktree dir already removed by external cleanup)
1083
+ if (err instanceof GSDError) throw err;
1084
+ debugLog("mergeMilestoneToMain", {
1085
+ action: "reconcile-skipped",
1086
+ reason: String(err),
1087
+ });
1088
+ }
1089
+ }
1090
+
1005
1091
  // 7. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
1006
1092
  const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
1007
1093
 
@@ -1066,6 +1152,16 @@ export function mergeMilestoneToMain(
1066
1152
  const commitResult = nativeCommit(originalBasePath_, commitMessage);
1067
1153
  const nothingToCommit = commitResult === null;
1068
1154
 
1155
+ // 8a. Clean up SQUASH_MSG left by git merge --squash (#1853).
1156
+ // git only removes SQUASH_MSG when the commit reads it directly (plain
1157
+ // `git commit`). nativeCommit uses `-F -` (stdin) or libgit2, neither
1158
+ // of which trigger git's SQUASH_MSG cleanup. If left on disk, doctor
1159
+ // reports `corrupt_merge_state` on every subsequent run.
1160
+ try {
1161
+ const squashMsgPath = join(resolveGitDir(originalBasePath_), "SQUASH_MSG");
1162
+ if (existsSync(squashMsgPath)) unlinkSync(squashMsgPath);
1163
+ } catch { /* best-effort */ }
1164
+
1069
1165
  // 8b. Safety check (#1792): if nothing was committed, verify the milestone
1070
1166
  // work is already on the integration branch before allowing teardown.
1071
1167
  // Compare only non-.gsd/ paths — .gsd/ state files diverge normally and
@@ -1091,12 +1187,33 @@ export function mergeMilestoneToMain(
1091
1187
  }
1092
1188
  }
1093
1189
 
1190
+ // 8c. Detect whether any non-.gsd/ code files were actually merged (#1906).
1191
+ // When a milestone only produced .gsd/ metadata (summaries, roadmaps) but no
1192
+ // real code, the user sees "milestone complete" but nothing changed in their
1193
+ // codebase. Surface this so the caller can warn the user.
1194
+ let codeFilesChanged = false;
1195
+ if (!nothingToCommit) {
1196
+ try {
1197
+ const mergedFiles = nativeDiffNumstat(
1198
+ originalBasePath_,
1199
+ "HEAD~1",
1200
+ "HEAD",
1201
+ );
1202
+ codeFilesChanged = mergedFiles.some(
1203
+ (entry) => !entry.path.startsWith(".gsd/"),
1204
+ );
1205
+ } catch {
1206
+ // If HEAD~1 doesn't exist (first commit), assume code was changed
1207
+ codeFilesChanged = true;
1208
+ }
1209
+ }
1210
+
1094
1211
  // 9. Auto-push if enabled
1095
1212
  let pushed = false;
1096
1213
  if (prefs.auto_push === true && !nothingToCommit) {
1097
1214
  const remote = prefs.remote ?? "origin";
1098
1215
  try {
1099
- execSync(`git push ${remote} ${mainBranch}`, {
1216
+ execFileSync("git", ["push", remote, mainBranch], {
1100
1217
  cwd: originalBasePath_,
1101
1218
  stdio: ["ignore", "pipe", "pipe"],
1102
1219
  encoding: "utf-8",
@@ -1114,20 +1231,23 @@ export function mergeMilestoneToMain(
1114
1231
  const prTarget = prefs.pr_target_branch ?? mainBranch;
1115
1232
  try {
1116
1233
  // Push the milestone branch to remote first
1117
- execSync(`git push ${remote} ${milestoneBranch}`, {
1234
+ execFileSync("git", ["push", remote, milestoneBranch], {
1118
1235
  cwd: originalBasePath_,
1119
1236
  stdio: ["ignore", "pipe", "pipe"],
1120
1237
  encoding: "utf-8",
1121
1238
  });
1122
1239
  // Create PR via gh CLI
1123
- execSync(
1124
- `gh pr create --base "${prTarget}" --head "${milestoneBranch}" --title "Milestone ${milestoneId} complete" --body "Auto-created by GSD on milestone completion."`,
1125
- {
1126
- cwd: originalBasePath_,
1127
- stdio: ["ignore", "pipe", "pipe"],
1128
- encoding: "utf-8",
1129
- },
1130
- );
1240
+ execFileSync("gh", [
1241
+ "pr", "create",
1242
+ "--base", prTarget,
1243
+ "--head", milestoneBranch,
1244
+ "--title", `Milestone ${milestoneId} complete`,
1245
+ "--body", "Auto-created by GSD on milestone completion.",
1246
+ ], {
1247
+ cwd: originalBasePath_,
1248
+ stdio: ["ignore", "pipe", "pipe"],
1249
+ encoding: "utf-8",
1250
+ });
1131
1251
  prCreated = true;
1132
1252
  } catch {
1133
1253
  // PR creation failure is non-fatal — gh may not be installed or authenticated
@@ -1138,6 +1258,30 @@ export function mergeMilestoneToMain(
1138
1258
  // throws only when the milestone has unanchored code changes, passes
1139
1259
  // through when the code is genuinely already on the integration branch.
1140
1260
 
1261
+ // 10a. Pre-teardown safety net (#1853): if the worktree still has uncommitted
1262
+ // changes (e.g. nativeHasChanges cache returned stale false, or auto-commit
1263
+ // silently failed), force one final commit so code is not destroyed by
1264
+ // `git worktree remove --force`.
1265
+ if (existsSync(worktreeCwd)) {
1266
+ try {
1267
+ const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
1268
+ if (dirtyCheck) {
1269
+ debugLog("mergeMilestoneToMain", {
1270
+ phase: "pre-teardown-dirty",
1271
+ worktreeCwd,
1272
+ status: dirtyCheck.slice(0, 200),
1273
+ });
1274
+ nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
1275
+ nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
1276
+ }
1277
+ } catch (e) {
1278
+ debugLog("mergeMilestoneToMain", {
1279
+ phase: "pre-teardown-commit-error",
1280
+ error: String(e),
1281
+ });
1282
+ }
1283
+ }
1284
+
1141
1285
  // 11. Remove worktree directory first (must happen before branch deletion)
1142
1286
  try {
1143
1287
  removeWorktree(originalBasePath_, milestoneId, {
@@ -1159,5 +1303,5 @@ export function mergeMilestoneToMain(
1159
1303
  originalBase = null;
1160
1304
  nudgeGitBranchCache(previousCwd);
1161
1305
 
1162
- return { commitMessage, pushed, prCreated };
1306
+ return { commitMessage, pushed, prCreated, codeFilesChanged };
1163
1307
  }