gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.e9d88a536

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 (203) hide show
  1. package/README.md +8 -5
  2. package/dist/headless-recover.d.ts +23 -0
  3. package/dist/headless-recover.js +93 -0
  4. package/dist/headless.js +9 -0
  5. package/dist/help-text.js +1 -0
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +4 -56
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -27
  10. package/dist/resources/extensions/gsd/auto-start.js +1 -8
  11. package/dist/resources/extensions/gsd/auto-worktree.js +59 -176
  12. package/dist/resources/extensions/gsd/auto.js +24 -6
  13. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
  14. package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
  15. package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
  16. package/dist/resources/extensions/gsd/commands-logs.js +2 -2
  17. package/dist/resources/extensions/gsd/commands-scan.js +2 -2
  18. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  19. package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
  20. package/dist/resources/extensions/gsd/db-writer.js +16 -85
  21. package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
  22. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
  23. package/dist/resources/extensions/gsd/gsd-db.js +74 -8
  24. package/dist/resources/extensions/gsd/guided-flow.js +31 -8
  25. package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
  26. package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
  27. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
  28. package/dist/resources/extensions/gsd/paths.js +35 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  30. package/dist/resources/extensions/gsd/queue-order.js +6 -1
  31. package/dist/resources/extensions/gsd/rethink.js +2 -2
  32. package/dist/resources/extensions/gsd/state.js +91 -372
  33. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
  34. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
  35. package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
  36. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
  37. package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
  38. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
  40. package/dist/resources/extensions/gsd/worktree-command.js +4 -3
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/index.html +1 -1
  95. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  102. package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
  103. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  104. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  106. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  107. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/package.json +1 -1
  109. package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
  110. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js +56 -2
  112. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  113. package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
  114. package/packages/mcp-server/src/workflow-tools.ts +61 -2
  115. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  116. package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
  117. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -60
  118. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -26
  119. package/src/resources/extensions/gsd/auto-start.ts +1 -8
  120. package/src/resources/extensions/gsd/auto-worktree.ts +61 -204
  121. package/src/resources/extensions/gsd/auto.ts +23 -6
  122. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
  123. package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
  124. package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
  125. package/src/resources/extensions/gsd/commands-logs.ts +2 -2
  126. package/src/resources/extensions/gsd/commands-scan.ts +2 -2
  127. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  128. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
  129. package/src/resources/extensions/gsd/db-writer.ts +16 -83
  130. package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
  131. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
  132. package/src/resources/extensions/gsd/gsd-db.ts +85 -8
  133. package/src/resources/extensions/gsd/guided-flow.ts +35 -8
  134. package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
  135. package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
  136. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
  137. package/src/resources/extensions/gsd/paths.ts +55 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  139. package/src/resources/extensions/gsd/queue-order.ts +6 -1
  140. package/src/resources/extensions/gsd/rethink.ts +2 -2
  141. package/src/resources/extensions/gsd/state.ts +91 -389
  142. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
  143. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
  144. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
  145. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
  147. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
  148. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
  149. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  150. package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
  151. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
  153. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
  154. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
  155. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
  156. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
  157. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
  158. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
  159. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
  160. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
  161. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
  162. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
  164. package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
  165. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
  166. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
  167. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
  168. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
  169. package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
  170. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
  171. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
  172. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
  173. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
  174. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
  175. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
  176. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
  177. package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
  178. package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
  179. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
  180. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  181. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
  182. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
  183. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
  184. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
  185. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
  186. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
  187. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
  188. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
  189. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +24 -1
  190. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
  191. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
  192. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
  193. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
  194. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
  195. package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
  196. package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
  197. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  198. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
  199. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
  200. package/src/resources/extensions/gsd/worktree-command.ts +4 -3
  201. package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
  202. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_buildManifest.js +0 -0
  203. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Post-unit processing for auto-loop — auto-commit, doctor run,
3
- * state rebuild, worktree sync, DB dual-write, hooks, triage, and
3
+ * state rebuild, projection checks, DB tool closeout, hooks, triage, and
4
4
  * quick-task dispatch.
5
5
  *
6
6
  * Split into two functions called sequentially by auto-loop with
@@ -42,7 +42,7 @@ import {
42
42
  } from "./auto-recovery.js";
43
43
  import { regenerateIfMissing } from "./workflow-projections.js";
44
44
  import { syncStateToProjectRoot } from "./auto-worktree.js";
45
- import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, updateSliceStatus, _getAdapter } from "./gsd-db.js";
45
+ import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter } from "./gsd-db.js";
46
46
  import { renderPlanCheckboxes } from "./markdown-renderer.js";
47
47
  import { consumeSignal } from "./session-status-io.js";
48
48
  import {
@@ -160,9 +160,8 @@ export interface RogueFileWrite {
160
160
  * the completion tool. A "rogue" file is one that exists on disk but has
161
161
  * no corresponding DB row with status "complete".
162
162
  *
163
- * This is a safety-net diagnostic (D003). The existing migrateFromMarkdown()
164
- * in postUnitPostVerification() eventually ingests rogue files, but explicit
165
- * detection provides immediate diagnostics so operators know the prompt failed.
163
+ * This is a safety-net diagnostic (D003). Runtime detection never imports
164
+ * markdown into the DB; explicit migration/import/recovery commands own that.
166
165
  */
167
166
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
167
  function hasNonEmptyFields(row: Record<string, any> | null, fields: string[]): boolean {
@@ -201,14 +200,7 @@ export function detectRogueFileWrites(
201
200
 
202
201
  const dbRow = getSlice(mid, sid);
203
202
  if (!dbRow || dbRow.status !== "complete") {
204
- // Auto-remediate: SUMMARY exists on disk but DB is stale — sync DB to
205
- // match filesystem instead of reporting as rogue (#3633).
206
- try {
207
- updateSliceStatus(mid, sid, "complete", new Date().toISOString());
208
- } catch {
209
- // If DB update fails, fall back to rogue detection so the issue is visible
210
- rogues.push({ path: summaryPath, unitType, unitId });
211
- }
203
+ rogues.push({ path: summaryPath, unitType, unitId });
212
204
  }
213
205
  } else if (unitType === "plan-milestone") {
214
206
  if (!mid) return [];
@@ -738,7 +730,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
738
730
  "error",
739
731
  );
740
732
  // Stop auto AND signal the outer postUnit flow to exit early.
741
- // Without the flag, subsequent hooks (triage, rogue detection,
733
+ // Without the flag, subsequent hooks (triage,
742
734
  // DB writes) would keep running against a conflicted main
743
735
  // checkout after the loop was already told to stop.
744
736
  const { stopAuto } = await import("./auto.js");
@@ -758,7 +750,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
758
750
  }
759
751
  });
760
752
  // Exit early after stopAuto so the rest of post-unit processing
761
- // (triage, rogue detection, hook dispatch, DB writes) doesn't run
753
+ // (triage, hook dispatch, DB writes) doesn't run
762
754
  // against a conflicted main checkout. Return "dispatched" to match
763
755
  // the convention used by other stop/pauseAuto paths in this function
764
756
  // (see signal handling earlier: stop/pause also return "dispatched").
@@ -813,17 +805,6 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
813
805
  }
814
806
  }
815
807
 
816
- // Rogue file detection — safety net for LLM bypassing completion tools (D003)
817
- try {
818
- const rogueFiles = detectRogueFileWrites(s.currentUnit.type, s.currentUnit.id, s.basePath);
819
- for (const rogue of rogueFiles) {
820
- logWarning("engine", "rogue file write detected", { path: rogue.path, unitId: rogue.unitId });
821
- ctx.ui.notify(`Rogue file write detected: ${rogue.path}`, "warning");
822
- }
823
- } catch (e) {
824
- debugLog("postUnit", { phase: "rogue-detection", error: String(e) });
825
- }
826
-
827
808
  // ── Safety harness: post-unit validation ──
828
809
  try {
829
810
  const { loadEffectiveGSDPreferences } = await import("./preferences.js");
@@ -855,18 +855,11 @@ export async function bootstrapAutoSession(
855
855
  const gsdDbPath = resolveProjectRootDbPath(s.basePath);
856
856
  const gsdDirPath = join(s.basePath, ".gsd");
857
857
  if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
858
- const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
859
- const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
860
- const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
861
858
  try {
862
859
  const { openDatabase: openDb } = await import("./gsd-db.js");
863
860
  openDb(gsdDbPath);
864
- if (hasDecisions || hasRequirements || hasMilestones) {
865
- const { migrateFromMarkdown } = await import("./md-importer.js");
866
- migrateFromMarkdown(s.basePath);
867
- }
868
861
  } catch (err) {
869
- logError("engine", `auto-migration failed: ${(err as Error).message}`);
862
+ logError("engine", `failed to initialize project database: ${(err as Error).message}`);
870
863
  }
871
864
  }
872
865
  if (existsSync(gsdDbPath) && !isDbAvailable()) {
@@ -32,7 +32,7 @@ import {
32
32
  import { atomicWriteSync } from "./atomic-write.js";
33
33
  import { execFileSync } from "node:child_process";
34
34
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
35
- import { gsdRoot } from "./paths.js";
35
+ import { gsdRoot, resolveGsdPathContract } from "./paths.js";
36
36
  import {
37
37
  createWorktree,
38
38
  removeWorktree,
@@ -90,9 +90,8 @@ const LEGACY_DEEP_SETUP_RUNTIME_UNIT_FILES = new Set([
90
90
  // ─── Shared Constants & Helpers ─────────────────────────────────────────────
91
91
 
92
92
  /**
93
- * Root-level .gsd/ state files synced between worktree and project root.
94
- * Single source of truth used by syncGsdStateToWorktree, syncWorktreeStateBack,
95
- * and the dispatch-level sync functions.
93
+ * Root-level .gsd/ projections copied from project root into worktrees for
94
+ * compatibility. Project root remains the canonical state/projection root.
96
95
  */
97
96
  const ROOT_STATE_FILES = [
98
97
  "DECISIONS.md",
@@ -110,6 +109,11 @@ const ROOT_STATE_FILES = [
110
109
  // because the project root is authoritative for preferences (#2684).
111
110
  ] as const;
112
111
 
112
+ const ROOT_DIAGNOSTIC_FILES = [
113
+ "completed-units.json",
114
+ "metrics.json",
115
+ ] as const;
116
+
113
117
  /**
114
118
  * Pop a stash entry by tracking the unique marker embedded in its message so
115
119
  * concurrent stash operations against the same project root cannot cause us to
@@ -194,7 +198,7 @@ const VERDICT_RE = /verdict:\s*[\w-]+/i;
194
198
  * destination when the source copy contains a `verdict:` field.
195
199
  *
196
200
  * This is the targeted fix for the UAT stuck-loop (#2821): the main
197
- * safeCopyRecursive uses force:false to protect worktree-authoritative
201
+ * safeCopyRecursive uses force:false to protect worktree-local projection
198
202
  * files (#1886), but ASSESSMENT files written by run-uat must be
199
203
  * forward-synced when the project root has a verdict. Without this,
200
204
  * the worktree retains a stale FAIL or missing ASSESSMENT and
@@ -272,9 +276,9 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void
272
276
  }
273
277
  }
274
278
 
275
- // Clean up entire synced milestone directory and runtime/units.
276
- // syncStateToProjectRoot() copies these into the project root during
277
- // execution. If they remain as untracked files when we attempt
279
+ // Clean up legacy synced milestone directories and runtime/units.
280
+ // Older versions copied these into the project root during execution.
281
+ // If they remain as untracked files when we attempt
278
282
  // `git merge --squash`, git rejects the merge with "local changes would
279
283
  // be overwritten", causing silent data loss (#1738).
280
284
  const syncedDirs = [
@@ -349,8 +353,9 @@ export function syncProjectRootToWorktree(
349
353
  if (!worktreePath_ || !projectRoot || worktreePath_ === projectRoot) return;
350
354
  if (!milestoneId) return;
351
355
 
352
- const prGsd = join(projectRoot, ".gsd");
353
- const wtGsd = join(worktreePath_, ".gsd");
356
+ const contract = resolveGsdPathContract(worktreePath_, projectRoot);
357
+ const prGsd = contract.projectGsd;
358
+ const wtGsd = contract.worktreeGsd ?? join(worktreePath_, ".gsd");
354
359
 
355
360
  // When .gsd is a symlink to the same external directory in both locations,
356
361
  // cpSync rejects the copy because source === destination (ERR_FS_CP_EINVAL).
@@ -359,7 +364,7 @@ export function syncProjectRootToWorktree(
359
364
 
360
365
  // Copy milestone directory from project root to worktree — additive only.
361
366
  // force:false prevents cpSync from overwriting existing worktree files.
362
- // Without this, worktree-authoritative files (e.g. VALIDATION.md written
367
+ // Without this, worktree-local files (e.g. VALIDATION.md written
363
368
  // by validate-milestone) get clobbered by stale project root copies,
364
369
  // causing an infinite re-validation loop (#1886).
365
370
  safeCopyRecursive(
@@ -369,7 +374,7 @@ export function syncProjectRootToWorktree(
369
374
  );
370
375
 
371
376
  // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
372
- // The additive-only copy above preserves worktree-authoritative files, but
377
+ // The additive-only copy above preserves worktree-local files, but
373
378
  // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
374
379
  // syncs it to the project root, the worktree may retain a stale copy (e.g.
375
380
  // verdict:fail while the project root has verdict:pass from a retry). On
@@ -390,11 +395,9 @@ export function syncProjectRootToWorktree(
390
395
  { force: true },
391
396
  );
392
397
 
393
- // Delete worktree gsd.db ONLY if it is empty (0 bytes).
394
- // An empty DB is stale/corrupt and should be rebuilt (#853).
395
- // A non-empty DB was populated by gsd-migrate on respawn and must be
396
- // preserved — deleting it truncates the file to 0 bytes when
397
- // openDatabase re-creates it, causing "no such table" failures (#2815).
398
+ // Delete a legacy worktree-local gsd.db ONLY if it is empty (0 bytes).
399
+ // Runtime opens contract.projectDb; this cleanup only removes corrupt
400
+ // pre-upgrade local DB projections.
398
401
  try {
399
402
  const wtDb = join(wtGsd, "gsd.db");
400
403
  let deleteSidecars = false;
@@ -428,9 +431,10 @@ export function syncProjectRootToWorktree(
428
431
  }
429
432
 
430
433
  /**
431
- * Sync dispatch-critical .gsd/ state files from worktree to project root.
434
+ * Sync worktree diagnostics from worktree to project root.
432
435
  * Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
433
- * Copies: STATE.md + active milestone directory (roadmap, slice plans, task summaries).
436
+ * DB/project-root state remains authoritative; markdown projections are not
437
+ * copied from the worktree back to the project root.
434
438
  * Non-fatal — sync failure should never block dispatch.
435
439
  */
436
440
  export function syncStateToProjectRoot(
@@ -441,31 +445,25 @@ export function syncStateToProjectRoot(
441
445
  if (!worktreePath_ || !projectRoot || worktreePath_ === projectRoot) return;
442
446
  if (!milestoneId) return;
443
447
 
444
- const wtGsd = join(worktreePath_, ".gsd");
445
- const prGsd = join(projectRoot, ".gsd");
448
+ const contract = resolveGsdPathContract(worktreePath_, projectRoot);
449
+ const wtGsd = contract.worktreeGsd ?? join(worktreePath_, ".gsd");
450
+ const prGsd = contract.projectGsd;
446
451
 
447
452
  // When .gsd is a symlink to the same external directory in both locations,
448
453
  // cpSync rejects the copy because source === destination (ERR_FS_CP_EINVAL).
449
454
  // Compare realpaths and skip when they resolve to the same physical path (#2184).
450
455
  if (isSamePath(wtGsd, prGsd)) return;
451
456
 
452
- // 1. STATE.md the quick-glance status used by initial deriveState()
453
- safeCopy(join(wtGsd, "STATE.md"), join(prGsd, "STATE.md"), { force: true });
454
-
455
- // 2. Milestone directory — ROADMAP, slice PLANs, task summaries
456
- // Copy the entire milestone .gsd subtree so deriveState reads current checkboxes
457
- safeCopyRecursive(
458
- join(wtGsd, "milestones", milestoneId),
459
- join(prGsd, "milestones", milestoneId),
460
- { force: true },
461
- );
462
-
463
- // 3. metrics.json — session cost/token tracking (#2313).
457
+ // metrics.jsonsession cost/token tracking (#2313).
464
458
  // Without this, metrics accumulated in the worktree are invisible from the
465
459
  // project root and never appear in the dashboard or skill-health reports.
466
460
  safeCopy(join(wtGsd, "metrics.json"), join(prGsd, "metrics.json"), { force: true });
467
461
 
468
- // 4. Runtime records unit dispatch state used by selfHealRuntimeRecords().
462
+ // completed-units.jsonruntime completion diagnostics used to avoid
463
+ // re-dispatching work already completed in an isolated worktree.
464
+ safeCopy(join(wtGsd, "completed-units.json"), join(prGsd, "completed-units.json"), { force: true });
465
+
466
+ // Runtime records — unit dispatch diagnostics used by selfHealRuntimeRecords().
469
467
  // Without this, a crash during a unit leaves the runtime record only in the
470
468
  // worktree. If the next session resolves basePath before worktree re-entry,
471
469
  // selfHeal can't find or clear the stale record (#769).
@@ -638,15 +636,17 @@ export function cleanStaleRuntimeUnits(
638
636
  * missing milestones, CONTEXT, ROADMAP, DECISIONS, REQUIREMENTS, and
639
637
  * PROJECT files from the main repo's .gsd/ into the worktree's .gsd/.
640
638
  *
641
- * Only adds missing content — never overwrites existing files in the worktree
642
- * (the worktree's execution state is authoritative for in-progress work).
639
+ * Only adds missing content — never overwrites existing files in the worktree.
640
+ * Worktree files are compatibility projections; DB/project root remains
641
+ * authoritative for runtime state.
643
642
  */
644
643
  export function syncGsdStateToWorktree(
645
644
  mainBasePath: string,
646
645
  worktreePath_: string,
647
646
  ): { synced: string[] } {
648
- const mainGsd = gsdRoot(mainBasePath);
649
- const wtGsd = gsdRoot(worktreePath_);
647
+ const contract = resolveGsdPathContract(worktreePath_, mainBasePath);
648
+ const mainGsd = contract.projectGsd;
649
+ const wtGsd = contract.worktreeGsd ?? join(worktreePath_, ".gsd");
650
650
  const synced: string[] = [];
651
651
 
652
652
  // If both resolve to the same directory (symlink), no sync needed
@@ -790,32 +790,26 @@ export function syncGsdStateToWorktree(
790
790
  }
791
791
 
792
792
  /**
793
- * Sync milestone artifacts from worktree back to the main external state directory.
794
- * Called before milestone merge to ensure completion artifacts (SUMMARY, VALIDATION,
795
- * updated ROADMAP) are visible from the project root (#1412).
793
+ * Sync compatibility artifacts from worktree back to the main external state
794
+ * directory. Canonical workflow state lives in the project DB; worktree .gsd
795
+ * content is legacy projection/diagnostic data only.
796
796
  *
797
797
  * Syncs:
798
- * 1. Root-level .gsd/ files (REQUIREMENTS, PROJECT, DECISIONS, KNOWLEDGE,
799
- * OVERRIDES) the worktree's versions overwrite main's because the
800
- * worktree is the authoritative execution context.
801
- * 2. ALL milestone directories found in the worktree — not just the
802
- * current milestoneId. The complete-milestone unit may create artifacts
803
- * for the *next* milestone (CONTEXT, ROADMAP, new requirements) which
804
- * must survive worktree teardown.
798
+ * 1. Legacy worktree DBs are reconciled into the canonical project DB.
799
+ * 2. Runtime diagnostic files may be copied for operator visibility.
805
800
  *
806
- * History: Originally only synced milestones/<milestoneId>/ and assumed
807
- * root-level files would be carried by the squash merge. In practice,
808
- * .gsd/ files are often untracked (gitignored or never committed), so the
809
- * squash merge carries nothing. This caused next-milestone artifacts and
810
- * updated REQUIREMENTS/PROJECT to be silently lost on teardown.
801
+ * Markdown milestone directories are projections and are not copied from
802
+ * worktrees into the project root. Current workflow state must arrive through
803
+ * the shared project DB or the pre-upgrade DB reconciliation path above.
811
804
  */
812
805
  export function syncWorktreeStateBack(
813
806
  mainBasePath: string,
814
807
  worktreePath: string,
815
808
  milestoneId: string,
816
809
  ): { synced: string[] } {
817
- const mainGsd = gsdRoot(mainBasePath);
818
- const wtGsd = gsdRoot(worktreePath);
810
+ const contract = resolveGsdPathContract(worktreePath, mainBasePath);
811
+ const mainGsd = contract.projectGsd;
812
+ const wtGsd = contract.worktreeGsd ?? join(worktreePath, ".gsd");
819
813
  const synced: string[] = [];
820
814
 
821
815
  // If both resolve to the same directory (symlink), no sync needed
@@ -829,7 +823,7 @@ export function syncWorktreeStateBack(
829
823
  // files. This handles in-flight worktrees that were created before the
830
824
  // upgrade to shared WAL mode.
831
825
  const wtLocalDb = join(wtGsd, "gsd.db");
832
- const mainDb = join(mainGsd, "gsd.db");
826
+ const mainDb = contract.projectDb;
833
827
  if (existsSync(wtLocalDb) && existsSync(mainDb)) {
834
828
  try {
835
829
  reconcileWorktreeDb(mainDb, wtLocalDb);
@@ -840,13 +834,10 @@ export function syncWorktreeStateBack(
840
834
  }
841
835
  }
842
836
 
843
- // ── 1. Sync root-level .gsd/ files back ──────────────────────────────
844
- // The worktree is authoritative — complete-milestone updates REQUIREMENTS,
845
- // PROJECT, etc. These must overwrite main's copies so they survive teardown.
846
- // Also includes QUEUE.md, completed-units.json, and metrics.json which are
847
- // written during milestone closeout and lost on teardown without explicit sync
848
- // (#1787, #2313).
849
- for (const f of ROOT_STATE_FILES) {
837
+ // ── 1. Sync root-level diagnostic files back ─────────────────────────
838
+ // Markdown/JSON state projections remain project-root/DB authoritative.
839
+ // These diagnostic files are copied for observability only.
840
+ for (const f of ROOT_DIAGNOSTIC_FILES) {
850
841
  const src = join(wtGsd, f);
851
842
  const dst = join(mainGsd, f);
852
843
  if (existsSync(src)) {
@@ -860,121 +851,8 @@ export function syncWorktreeStateBack(
860
851
  }
861
852
  }
862
853
 
863
- // ── 2. Sync ALL milestone directories ────────────────────────────────
864
- // The complete-milestone unit may create next-milestone artifacts (e.g.
865
- // M007 setup while closing M006). We must sync every milestone directory
866
- // in the worktree, not just the current one.
867
- const wtMilestonesDir = join(wtGsd, "milestones");
868
- if (!existsSync(wtMilestonesDir)) return { synced };
869
-
870
- try {
871
- const wtMilestones = readdirSync(wtMilestonesDir, { withFileTypes: true })
872
- .filter((d) => d.isDirectory())
873
- .map((d) => d.name);
874
-
875
- for (const mid of wtMilestones) {
876
- // Skip the current milestone being merged — its files are already in the
877
- // milestone branch and would conflict with the squash merge (#3641).
878
- if (mid === milestoneId) continue;
879
- syncMilestoneDir(wtGsd, mainGsd, mid, synced);
880
- }
881
- } catch (err) {
882
- /* non-fatal */
883
- logWarning("worktree", `milestone sync-back failed: ${err instanceof Error ? err.message : String(err)}`);
884
- }
885
-
886
- return { synced };
887
- }
888
-
889
- function syncCurrentMilestoneStateAfterMerge(
890
- mainBasePath: string,
891
- worktreePath: string,
892
- milestoneId: string,
893
- ): { synced: string[] } {
894
- const mainGsd = gsdRoot(mainBasePath);
895
- const wtGsd = gsdRoot(worktreePath);
896
- const synced: string[] = [];
897
-
898
- if (isSamePath(mainGsd, wtGsd)) return { synced };
899
- if (!existsSync(wtGsd) || !existsSync(mainGsd)) return { synced };
900
-
901
- syncMilestoneDir(wtGsd, mainGsd, milestoneId, synced);
902
854
  return { synced };
903
855
  }
904
-
905
- /**
906
- * Sync a single milestone directory from worktree to main.
907
- * Copies milestone-level .md files, slice-level files, and task summaries.
908
- */
909
- /** Copy matching files from srcDir to dstDir (non-fatal per file). */
910
- function syncDirFiles(
911
- srcDir: string,
912
- dstDir: string,
913
- filter: (name: string) => boolean,
914
- synced: string[],
915
- prefix: string,
916
- ): void {
917
- try {
918
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
919
- if (!entry.isFile() || !filter(entry.name)) continue;
920
- try {
921
- cpSync(join(srcDir, entry.name), join(dstDir, entry.name), { force: true });
922
- synced.push(`${prefix}${entry.name}`);
923
- } catch (err) {
924
- /* non-fatal */
925
- logWarning("worktree", `file copy failed (${prefix}${entry.name}): ${err instanceof Error ? err.message : String(err)}`);
926
- }
927
- }
928
- } catch (err) {
929
- /* non-fatal — srcDir may not be readable */
930
- logWarning("worktree", `directory read failed: ${err instanceof Error ? err.message : String(err)}`);
931
- }
932
- }
933
-
934
- function syncMilestoneDir(
935
- wtGsd: string,
936
- mainGsd: string,
937
- mid: string,
938
- synced: string[],
939
- ): void {
940
- const wtMilestoneDir = join(wtGsd, "milestones", mid);
941
- const mainMilestoneDir = join(mainGsd, "milestones", mid);
942
-
943
- if (!existsSync(wtMilestoneDir)) return;
944
- mkdirSync(mainMilestoneDir, { recursive: true });
945
-
946
- const isMd = (name: string): boolean => name.endsWith(".md");
947
-
948
- // Sync milestone-level files (SUMMARY, VALIDATION, ROADMAP, CONTEXT)
949
- syncDirFiles(wtMilestoneDir, mainMilestoneDir, isMd, synced, `milestones/${mid}/`);
950
-
951
- // Sync slice-level files (summaries, UATs) and task summaries (#1678)
952
- const wtSlicesDir = join(wtMilestoneDir, "slices");
953
- const mainSlicesDir = join(mainMilestoneDir, "slices");
954
- if (!existsSync(wtSlicesDir)) return;
955
-
956
- try {
957
- for (const sliceEntry of readdirSync(wtSlicesDir, { withFileTypes: true })) {
958
- if (!sliceEntry.isDirectory()) continue;
959
- const sid = sliceEntry.name;
960
- const wtSliceDir = join(wtSlicesDir, sid);
961
- const mainSliceDir = join(mainSlicesDir, sid);
962
- mkdirSync(mainSliceDir, { recursive: true });
963
-
964
- syncDirFiles(wtSliceDir, mainSliceDir, isMd, synced, `milestones/${mid}/slices/${sid}/`);
965
-
966
- const wtTasksDir = join(wtSliceDir, "tasks");
967
- const mainTasksDir = join(mainSliceDir, "tasks");
968
- if (existsSync(wtTasksDir)) {
969
- mkdirSync(mainTasksDir, { recursive: true });
970
- syncDirFiles(wtTasksDir, mainTasksDir, isMd, synced, `milestones/${mid}/slices/${sid}/tasks/`);
971
- }
972
- }
973
- } catch (err) {
974
- /* non-fatal */
975
- logWarning("worktree", `milestone slice sync failed (${mid}): ${err instanceof Error ? err.message : String(err)}`);
976
- }
977
- }
978
856
  // ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
979
857
 
980
858
  /**
@@ -1160,10 +1038,9 @@ export function enterBranchModeForMilestone(
1160
1038
  * directory at the project root and apply any [x] checkbox states that are
1161
1039
  * ahead of the worktree version (forward-only: never downgrade [x] → [ ]).
1162
1040
  *
1163
- * This is safe because syncStateToProjectRoot() is the authoritative source
1164
- * of post-task state at the project root it writes the same [x] the LLM
1165
- * produced, then the auto-commit follows. If the commit never happened, the
1166
- * filesystem copy is still valid and correct.
1041
+ * This is forward-only compatibility for legacy projection copies. The DB
1042
+ * remains authoritative; this never downgrades checked boxes in a local
1043
+ * worktree projection.
1167
1044
  */
1168
1045
  function reconcilePlanCheckboxes(
1169
1046
  projectRoot: string,
@@ -1710,9 +1587,10 @@ export function mergeMilestoneToMain(
1710
1587
  // database (#2823).
1711
1588
  if (isDbAvailable()) {
1712
1589
  try {
1713
- const worktreeDbPath = join(worktreeCwd, ".gsd", "gsd.db");
1714
- const mainDbPath = join(originalBasePath_, ".gsd", "gsd.db");
1715
- if (!isSamePath(worktreeDbPath, mainDbPath)) {
1590
+ const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1591
+ const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1592
+ const mainDbPath = contract.projectDb;
1593
+ if (existsSync(worktreeDbPath) && !isSamePath(worktreeDbPath, mainDbPath)) {
1716
1594
  reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1717
1595
  }
1718
1596
  } catch (err) {
@@ -2248,27 +2126,6 @@ export function mergeMilestoneToMain(
2248
2126
  // 9a-iii. Restore sheltered queued milestone directories (#2505).
2249
2127
  restoreShelter();
2250
2128
 
2251
- // 9a-iv. Preserve current milestone artifacts that may be untracked in git.
2252
- // syncWorktreeStateBack intentionally skips the current milestone before the
2253
- // squash merge to avoid conflicting with the merge content. Once the squash
2254
- // commit is complete, copy those files back so summaries, validation, and
2255
- // task outputs survive worktree teardown in external/.gitignored .gsd setups.
2256
- try {
2257
- const { synced } = syncCurrentMilestoneStateAfterMerge(
2258
- originalBasePath_,
2259
- worktreeCwd,
2260
- milestoneId,
2261
- );
2262
- if (synced.length > 0) {
2263
- debugLog("mergeMilestoneToMain", {
2264
- phase: "current-milestone-sync-after-merge",
2265
- synced: synced.length,
2266
- });
2267
- }
2268
- } catch (err) {
2269
- logWarning("worktree", `current milestone sync after merge failed: ${err instanceof Error ? err.message : String(err)}`);
2270
- }
2271
-
2272
2129
  // 9b. Safety check (#1792): if nothing was committed, verify the milestone
2273
2130
  // work is already on the integration branch before allowing teardown.
2274
2131
  // Compare only non-.gsd/ paths — .gsd/ state files diverge normally and
@@ -175,6 +175,7 @@ import { getErrorMessage } from "./error-utils.js";
175
175
  import { recoverFailedMigration } from "./migrate-external.js";
176
176
  import { initRegistry, convertDispatchRules } from "./rule-registry.js";
177
177
  import { emitJournalEvent as _emitJournalEvent, type JournalEntry } from "./journal.js";
178
+ import { isClosedStatus } from "./status-guards.js";
178
179
  import {
179
180
  type AutoDashboardData,
180
181
  updateProgressWidget as _updateProgressWidget,
@@ -194,6 +195,7 @@ import {
194
195
  import { isDbAvailable, getMilestone } from "./gsd-db.js";
195
196
  import { countPendingCaptures } from "./captures.js";
196
197
  import { CMUX_CHANNELS, type CmuxLogLevel } from "../shared/cmux-events.js";
198
+ import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
197
199
 
198
200
  function makeCmuxEmitters(pi: ExtensionAPI) {
199
201
  return {
@@ -1506,14 +1508,29 @@ export async function startAuto(
1506
1508
  );
1507
1509
  if (shouldResumePausedSession) {
1508
1510
  // Validate the milestone still exists and isn't already complete (#1664).
1511
+ // DB status is authoritative when available; SUMMARY.md is a legacy
1512
+ // fallback only for unmigrated/offline projects.
1509
1513
  const mDir = resolveMilestonePath(base, meta.milestoneId);
1510
- const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
1511
1514
  let summaryIsTerminal = false;
1512
- if (summaryFile) {
1513
- try {
1514
- summaryIsTerminal = classifyMilestoneSummaryContent(readFileSync(summaryFile, "utf-8")) !== "failure";
1515
- } catch {
1516
- summaryIsTerminal = false;
1515
+ let dbAvailable = isDbAvailable();
1516
+ let milestoneRow = dbAvailable ? getMilestone(meta.milestoneId) : null;
1517
+ if (!milestoneRow) {
1518
+ const opened = await ensureDbOpen(base);
1519
+ dbAvailable = opened || isDbAvailable();
1520
+ if (dbAvailable) {
1521
+ milestoneRow = getMilestone(meta.milestoneId);
1522
+ }
1523
+ }
1524
+ if (dbAvailable) {
1525
+ summaryIsTerminal = !!milestoneRow && isClosedStatus(milestoneRow.status);
1526
+ } else {
1527
+ const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
1528
+ if (summaryFile) {
1529
+ try {
1530
+ summaryIsTerminal = classifyMilestoneSummaryContent(readFileSync(summaryFile, "utf-8")) !== "failure";
1531
+ } catch {
1532
+ summaryIsTerminal = false;
1533
+ }
1517
1534
  }
1518
1535
  }
1519
1536
  if (!mDir || summaryIsTerminal) {