@opengsd/gsd-pi 1.5.0-dev.4fb2d460 → 1.5.0-dev.c4c8885f

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 (234) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
  3. package/dist/resources/extensions/gsd/activity-log.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
  5. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +53 -15
  7. package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
  8. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
  9. package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
  10. package/dist/resources/extensions/gsd/auto-start.js +53 -30
  11. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
  12. package/dist/resources/extensions/gsd/auto-timers.js +9 -2
  13. package/dist/resources/extensions/gsd/auto.js +97 -31
  14. package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
  15. package/dist/resources/extensions/gsd/config-overlay.js +7 -2
  16. package/dist/resources/extensions/gsd/db/engine.js +33 -0
  17. package/dist/resources/extensions/gsd/db-adapter.js +30 -3
  18. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  19. package/dist/resources/extensions/gsd/journal.js +1 -1
  20. package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
  21. package/dist/resources/extensions/gsd/metrics.js +2 -2
  22. package/dist/resources/extensions/gsd/paths.js +70 -49
  23. package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
  24. package/dist/resources/extensions/gsd/preferences-models.js +66 -15
  25. package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
  26. package/dist/resources/extensions/gsd/rule-registry.js +106 -10
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
  29. package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
  30. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
  31. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
  32. package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
  33. package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
  34. package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
  35. package/dist/resources/extensions/gsd/uok/audit.js +3 -0
  36. package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
  37. package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
  38. package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
  39. package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
  40. package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
  41. package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
  42. package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
  43. package/dist/resources/extensions/gsd/workspace.js +5 -14
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
  45. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  46. package/dist/web/standalone/.next/BUILD_ID +1 -1
  47. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  48. package/dist/web/standalone/.next/build-manifest.json +2 -2
  49. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  50. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/index.html +1 -1
  101. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  108. package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
  109. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  110. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  112. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  113. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  114. package/dist/worktree-cli-create.d.ts +10 -0
  115. package/dist/worktree-cli-create.js +11 -0
  116. package/dist/worktree-cli-format.d.ts +12 -0
  117. package/dist/worktree-cli-format.js +38 -0
  118. package/dist/worktree-cli-plan.d.ts +16 -0
  119. package/dist/worktree-cli-plan.js +19 -0
  120. package/dist/worktree-cli-session.d.ts +13 -0
  121. package/dist/worktree-cli-session.js +18 -0
  122. package/dist/worktree-cli-status.d.ts +35 -0
  123. package/dist/worktree-cli-status.js +54 -0
  124. package/dist/worktree-cli.d.ts +1 -16
  125. package/dist/worktree-cli.js +28 -122
  126. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  127. package/integrations/hermes/pyproject.toml +1 -1
  128. package/package.json +1 -1
  129. package/packages/cloud-mcp-gateway/package.json +2 -2
  130. package/packages/contracts/package.json +1 -1
  131. package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
  132. package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
  133. package/packages/daemon/dist/orchestrator-agent.js +113 -0
  134. package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
  135. package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
  136. package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
  137. package/packages/daemon/dist/orchestrator-tools.js +137 -0
  138. package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
  139. package/packages/daemon/dist/orchestrator.d.ts +4 -56
  140. package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
  141. package/packages/daemon/dist/orchestrator.js +13 -303
  142. package/packages/daemon/dist/orchestrator.js.map +1 -1
  143. package/packages/daemon/dist/orchestrator.test.js +11 -1
  144. package/packages/daemon/dist/orchestrator.test.js.map +1 -1
  145. package/packages/daemon/package.json +4 -4
  146. package/packages/gsd-agent-core/package.json +5 -5
  147. package/packages/gsd-agent-modes/package.json +7 -7
  148. package/packages/mcp-server/package.json +4 -4
  149. package/packages/native/package.json +1 -1
  150. package/packages/pi-agent-core/package.json +1 -1
  151. package/packages/pi-ai/package.json +1 -1
  152. package/packages/pi-coding-agent/package.json +7 -7
  153. package/packages/pi-tui/package.json +2 -2
  154. package/packages/rpc-client/package.json +2 -2
  155. package/pkg/package.json +1 -1
  156. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
  157. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
  158. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
  159. package/src/resources/extensions/gsd/activity-log.ts +1 -1
  160. package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
  161. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -1
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +59 -14
  163. package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
  164. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
  165. package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
  166. package/src/resources/extensions/gsd/auto-start.ts +57 -28
  167. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
  168. package/src/resources/extensions/gsd/auto-timers.ts +9 -2
  169. package/src/resources/extensions/gsd/auto.ts +103 -30
  170. package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
  171. package/src/resources/extensions/gsd/config-overlay.ts +7 -1
  172. package/src/resources/extensions/gsd/db/engine.ts +36 -0
  173. package/src/resources/extensions/gsd/db-adapter.ts +29 -3
  174. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  175. package/src/resources/extensions/gsd/journal.ts +1 -1
  176. package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
  177. package/src/resources/extensions/gsd/metrics.ts +2 -2
  178. package/src/resources/extensions/gsd/paths.ts +87 -48
  179. package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
  180. package/src/resources/extensions/gsd/preferences-models.ts +74 -20
  181. package/src/resources/extensions/gsd/preferences-types.ts +24 -1
  182. package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
  183. package/src/resources/extensions/gsd/rule-registry.ts +111 -9
  184. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
  185. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
  186. package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
  187. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
  188. package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
  189. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
  190. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
  191. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
  192. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
  193. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
  194. package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
  195. package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
  196. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
  197. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
  198. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
  199. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
  200. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  201. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
  202. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
  203. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
  204. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
  205. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
  206. package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
  207. package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
  208. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
  209. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
  210. package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
  211. package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
  212. package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
  213. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
  214. package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
  215. package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
  217. package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
  218. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
  219. package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
  220. package/src/resources/extensions/gsd/types.ts +23 -4
  221. package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
  222. package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
  223. package/src/resources/extensions/gsd/uok/audit.ts +2 -0
  224. package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
  225. package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
  226. package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
  227. package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
  228. package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
  229. package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
  230. package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
  231. package/src/resources/extensions/gsd/workspace.ts +5 -21
  232. package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
  233. /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
  234. /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import {
@@ -8,6 +8,7 @@ import {
8
8
  maybeWriteParallelResearchCostSpikeBlocker,
9
9
  } from "../auto-post-unit.ts";
10
10
  import { resolveExpectedArtifactPath } from "../auto-recovery.ts";
11
+ import { _clearGsdRootCache, clearPathCache } from "../paths.ts";
11
12
 
12
13
  test("missing execute-task artifact includes completion contract and completion-tool hint", () => {
13
14
  const base = mkdtempSync(join(tmpdir(), "gsd-artifact-diag-"));
@@ -56,3 +57,49 @@ test("parallel research cost spike writes durable PARALLEL-BLOCKER", () => {
56
57
  rmSync(base, { recursive: true, force: true });
57
58
  }
58
59
  });
60
+
61
+ // ── #1238: symlinked project root must not produce a ../ path walk ────────────
62
+ //
63
+ // On WSL (and any layout where the project root is reached through a symlink),
64
+ // artifact resolution canonicalizes through gsdRoot (realpathSync), so the
65
+ // resolved artifact path lives under the REAL root while `basePath` is still the
66
+ // symlink. Before the fix, relative(symlinkBase, realArtifactPath) walked up out
67
+ // of the symlink (../../…/home/…) and the diagnostic reported that broken path as
68
+ // "not found on disk". Normalizing basePath to its real path first keeps the
69
+ // displayed path anchored at the real .gsd root.
70
+
71
+ test("symlinked project root yields a clean .gsd-relative artifact path, not a ../ walk (#1238)", () => {
72
+ const realRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-symlink-real-")));
73
+ const linkParent = realpathSync(mkdtempSync(join(tmpdir(), "gsd-symlink-link-")));
74
+ const linkRoot = join(linkParent, "project");
75
+ try {
76
+ // Content-bearing legacy milestone dir so plan-milestone resolves the
77
+ // canonical (realpath) ROADMAP path under realRoot/.gsd/milestones/M001/.
78
+ const milestoneDir = join(realRoot, ".gsd", "milestones", "M001");
79
+ mkdirSync(milestoneDir, { recursive: true });
80
+ writeFileSync(join(milestoneDir, "M001-CONTEXT.md"), "# context\n");
81
+ // The ROADMAP intentionally does not exist — this hits the "not found on
82
+ // disk" branch, the exact symptom in #1238.
83
+ symlinkSync(realRoot, linkRoot);
84
+
85
+ _clearGsdRootCache();
86
+ clearPathCache();
87
+
88
+ const msg = _describeArtifactVerificationFailureForTest("plan-milestone", "M001", linkRoot);
89
+
90
+ assert.match(msg, /was not found on disk after unit execution/);
91
+ // The displayed path must be anchored at the real .gsd root, not a broken
92
+ // parent-walk out of the symlinked base.
93
+ assert.doesNotMatch(msg, /\.\.[/\\]/, "must not contain a ../ walk out of the symlinked base");
94
+ assert.match(
95
+ msg,
96
+ /\.gsd[/\\]milestones[/\\]M001[/\\]M001-ROADMAP\.md/,
97
+ "must show the clean .gsd-relative ROADMAP path",
98
+ );
99
+ } finally {
100
+ _clearGsdRootCache();
101
+ clearPathCache();
102
+ rmSync(linkParent, { recursive: true, force: true });
103
+ rmSync(realRoot, { recursive: true, force: true });
104
+ }
105
+ });
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
- import { closeDatabase, getAllMilestones, getMilestone, insertMilestone, isDbAvailable, openDatabase } from "../gsd-db.ts";
7
+ import { _getAdapter, closeDatabase, getAllMilestones, getMilestone, insertMilestone, isDbAvailable, openDatabase } from "../gsd-db.ts";
8
8
  import { reconcileMergedMilestonesFromJournal, reconcileProjectMilestonesFromDisk } from "../auto-start.ts";
9
9
  import { emitWorktreeMerged } from "../worktree-telemetry.ts";
10
10
 
@@ -69,3 +69,31 @@ test("#5389: bootstrap reconciles PROJECT.md milestones that are missing from DB
69
69
  rmSync(base, { recursive: true, force: true });
70
70
  }
71
71
  });
72
+
73
+ test("#1236: bootstrap merged-milestone reconciliation degrades to a warning instead of aborting when the DB is degraded", () => {
74
+ const base = mkdtempSync(join(tmpdir(), "gsd-merged-reconcile-degraded-"));
75
+ try {
76
+ mkdirSync(join(base, ".gsd"), { recursive: true });
77
+ openDatabase(join(base, ".gsd", "gsd.db"));
78
+ insertMilestone({ id: "M001", title: "Merged Milestone", status: "active" });
79
+
80
+ emitWorktreeMerged(base, "M001", { reason: "milestone-complete", conflict: false });
81
+
82
+ // Simulate a degraded DB: the connection stays "available" (isDbAvailable()
83
+ // remains true because the handle is non-null), but the milestones table is
84
+ // gone, so the reconciler's DB access throws partway through bootstrap.
85
+ _getAdapter()!.exec("DROP TABLE milestones");
86
+ assert.equal(isDbAvailable(), true);
87
+
88
+ // Regression (#1236): this reconciler was previously unguarded, so a
89
+ // degraded-DB failure threw and aborted the rest of `/gsd auto` bootstrap.
90
+ // It must now catch, warn, and return 0, matching its sibling
91
+ // reconcileProjectMilestonesFromDisk. Reaching the assertion proves it did
92
+ // not throw.
93
+ const closed = reconcileMergedMilestonesFromJournal(base);
94
+ assert.equal(closed, 0);
95
+ } finally {
96
+ if (isDbAvailable()) closeDatabase();
97
+ rmSync(base, { recursive: true, force: true });
98
+ }
99
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Idle/hard timeout recovery honors the in-flight-tool guard (#1229 follow-up).
3
+ *
4
+ * `recoverTimedOutUnit` steers a still-running unit with `deliverAs: "steer"`
5
+ * and, before that, may swap the session model via
6
+ * `applySupervisorModelIfConfigured`. Swapping the model and triggering a turn
7
+ * while non-interactive tool calls are still open skips tool results and trips
8
+ * provider errors (#3512) — the same failure the soft wrap-up and continue-here
9
+ * timers guard against. This test pins that the idle and hard steering branches
10
+ * gate both the model swap and `triggerTurn` on `getInFlightToolCount() === 0`.
11
+ */
12
+
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { tmpdir } from "node:os";
18
+
19
+ import { recoverTimedOutUnit, type RecoveryContext } from "../auto-timeout-recovery.ts";
20
+ import { clearGSDPreferencesCache } from "../preferences.ts";
21
+ import {
22
+ markToolStart,
23
+ markToolEnd,
24
+ clearInFlightTools,
25
+ } from "../auto-tool-tracking.ts";
26
+
27
+ const SUPERVISOR_PREFS = [
28
+ "auto_supervisor:",
29
+ " soft_timeout_minutes: 1",
30
+ " idle_timeout_minutes: 100",
31
+ " hard_timeout_minutes: 100",
32
+ " model:",
33
+ " model: supervisor-primary",
34
+ " fallbacks: [supervisor-fb]",
35
+ ];
36
+
37
+ interface Harness {
38
+ home: string;
39
+ base: string;
40
+ ctx: any;
41
+ pi: any;
42
+ rctx: RecoveryContext;
43
+ setModelCalls: unknown[][];
44
+ sendMessageCalls: unknown[][];
45
+ }
46
+
47
+ function makeHarness(): Harness {
48
+ const home = mkdtempSync(join(tmpdir(), "gsd-recovery-guard-home-"));
49
+ const base = mkdtempSync(join(tmpdir(), "gsd-recovery-guard-base-"));
50
+ process.env.GSD_HOME = home;
51
+ writeFileSync(join(home, "preferences.md"), ["---", ...SUPERVISOR_PREFS, "---", ""].join("\n"));
52
+ clearGSDPreferencesCache();
53
+
54
+ const setModelCalls: unknown[][] = [];
55
+ const sendMessageCalls: unknown[][] = [];
56
+
57
+ const ctx = {
58
+ ui: { notify: () => {} },
59
+ model: { provider: "anthropic" },
60
+ modelRegistry: {
61
+ getAvailable: () => [
62
+ { id: "supervisor-primary", provider: "anthropic" },
63
+ { id: "supervisor-fb", provider: "anthropic" },
64
+ ],
65
+ },
66
+ } as any;
67
+
68
+ const pi = {
69
+ sendMessage: (...args: unknown[]) => {
70
+ sendMessageCalls.push(args);
71
+ },
72
+ setModel: async (...args: unknown[]) => {
73
+ setModelCalls.push(args);
74
+ return true;
75
+ },
76
+ getThinkingLevel: () => "off",
77
+ setThinkingLevel: () => {},
78
+ } as any;
79
+
80
+ const rctx: RecoveryContext = {
81
+ basePath: base,
82
+ verbose: false,
83
+ currentUnitStartedAt: 1234,
84
+ unitRecoveryCount: new Map(),
85
+ };
86
+
87
+ return { home, base, ctx, pi, rctx, setModelCalls, sendMessageCalls };
88
+ }
89
+
90
+ function cleanup(h: Harness): void {
91
+ clearInFlightTools();
92
+ clearGSDPreferencesCache();
93
+ delete process.env.GSD_HOME;
94
+ rmSync(h.home, { recursive: true, force: true });
95
+ rmSync(h.base, { recursive: true, force: true });
96
+ }
97
+
98
+ // "discuss-project" resolves to a PROJECT.md that never exists in the temp base,
99
+ // so recovery skips the "already on disk" shortcut and reaches the steering
100
+ // branch with recoveryAttempts (0) < maxRecoveryAttempts.
101
+ for (const reason of ["idle", "hard"] as const) {
102
+ test(`${reason} recovery skips the supervisor model swap while a tool is in flight`, async () => {
103
+ const h = makeHarness();
104
+ try {
105
+ markToolStart("call-1", true, "some_tool");
106
+
107
+ const outcome = await recoverTimedOutUnit(
108
+ h.ctx,
109
+ h.pi,
110
+ "discuss-project",
111
+ "P01",
112
+ reason,
113
+ h.rctx,
114
+ );
115
+
116
+ assert.equal(outcome, "recovered");
117
+ assert.equal(
118
+ h.setModelCalls.length,
119
+ 0,
120
+ "model must NOT swap while a tool is in flight",
121
+ );
122
+ assert.equal(h.sendMessageCalls.length, 1, "steer message still sent (queued)");
123
+ const opts = h.sendMessageCalls[0][1] as { triggerTurn?: boolean; deliverAs?: string };
124
+ assert.equal(opts.triggerTurn, false, "turn is not triggered mid-tool-call");
125
+ assert.equal(opts.deliverAs, "steer");
126
+
127
+ markToolEnd("call-1");
128
+ } finally {
129
+ cleanup(h);
130
+ }
131
+ });
132
+
133
+ test(`${reason} recovery applies the supervisor model swap when no tools are in flight`, async () => {
134
+ const h = makeHarness();
135
+ try {
136
+ const outcome = await recoverTimedOutUnit(
137
+ h.ctx,
138
+ h.pi,
139
+ "discuss-project",
140
+ "P01",
141
+ reason,
142
+ h.rctx,
143
+ );
144
+
145
+ assert.equal(outcome, "recovered");
146
+ assert.equal(h.setModelCalls.length, 1, "model swaps to the supervisor model");
147
+ assert.deepEqual(
148
+ h.setModelCalls[0][0],
149
+ { id: "supervisor-primary", provider: "anthropic" },
150
+ "swaps to the configured supervisor primary",
151
+ );
152
+ assert.equal(h.sendMessageCalls.length, 1);
153
+ const opts = h.sendMessageCalls[0][1] as { triggerTurn?: boolean };
154
+ assert.equal(opts.triggerTurn, true, "turn is triggered when no tools are open");
155
+ } finally {
156
+ cleanup(h);
157
+ }
158
+ });
159
+ }
@@ -18,6 +18,7 @@ import { randomUUID } from "node:crypto";
18
18
  import { handleVerdict, parseValidationFile } from "../commands-verdict.ts";
19
19
  import { openDatabase, closeDatabase, _getAdapter, insertAssessment } from "../gsd-db.ts";
20
20
  import { invalidateStateCache } from "../state.ts";
21
+ import { resolveMilestoneFile } from "../paths.ts";
21
22
 
22
23
  interface NotifyCall {
23
24
  message: string;
@@ -363,7 +364,8 @@ test("handleVerdict pass override works when validation only exists in DB", asyn
363
364
  const { ctx, calls } = makeMockCtx();
364
365
  await handleVerdict('pass --milestone M001 --rationale "reviewed and accepted"', ctx, base);
365
366
 
366
- const validationPath = join(base, ".gsd", "phases", "01-m001", "01-VALIDATION.md");
367
+ const validationPath = resolveMilestoneFile(base, "M001", "VALIDATION");
368
+ assert.ok(validationPath, "DB-backed validation should be materialized to disk");
367
369
  const rewritten = readFileSync(validationPath, "utf-8");
368
370
  assert.match(rewritten, /^verdict: pass$/m, "DB-backed validation should be rewritten as pass");
369
371
  assert.match(rewritten, /reviewed and accepted/, "explicit rationale should be persisted");
@@ -12,6 +12,7 @@ import {
12
12
  writeCompatMarker,
13
13
  normalizeForHash,
14
14
  computeProjectionSha,
15
+ pruneOrphanedProjectionEntries,
15
16
  EMPTY_MARKER,
16
17
  compatMarkerPath,
17
18
  } from "../compat/compat-marker.ts";
@@ -103,3 +104,87 @@ test("compatMarkerPath resolves under .gsd", () => {
103
104
  const base = makeTmpBase();
104
105
  assert.equal(compatMarkerPath(base), join(base, ".gsd", ".compat.json"));
105
106
  });
107
+
108
+ test("pruneOrphanedProjectionEntries drops entries whose backing file is gone (#1257)", () => {
109
+ const base = makeTmpBase();
110
+ // Live projection: file exists on disk.
111
+ const liveRel = "phases/29-frontend-code-debt-cleanup/29-ROADMAP.md";
112
+ mkdirSync(join(base, ".gsd", "phases", "29-frontend-code-debt-cleanup"), { recursive: true });
113
+ writeFileSync(join(base, ".gsd", liveRel), "# M029\n", "utf-8");
114
+ // Phantom projection: directory was renamed, file no longer exists.
115
+ const ghostRel = "phases/29-new-milestone-m029/29-ROADMAP.md";
116
+
117
+ writeCompatMarker(base, {
118
+ schema: 2,
119
+ lastWriter: "gsd-pi",
120
+ lastProjectedAt: "2026-07-05T05:35:19.379Z",
121
+ projections: {
122
+ [liveRel]: { sha: "live000000000000", entities: ["M029"] },
123
+ [ghostRel]: { sha: "ghost00000000000", entities: ["M029"] },
124
+ },
125
+ piVersion: "1.4.0",
126
+ });
127
+
128
+ const removed = pruneOrphanedProjectionEntries(base);
129
+ assert.equal(removed, 1);
130
+
131
+ const marker = readCompatMarker(base);
132
+ assert.ok(marker.projections[liveRel], "live projection must survive");
133
+ assert.equal(marker.projections[ghostRel], undefined, "phantom projection must be pruned");
134
+ });
135
+
136
+ test("pruneOrphanedProjectionEntries also prunes .planning projections/passthrough", () => {
137
+ const base = makeTmpBase();
138
+ mkdirSync(join(base, ".planning"), { recursive: true });
139
+ writeFileSync(join(base, ".planning", "ROADMAP.md"), "# roadmap\n", "utf-8");
140
+
141
+ writeCompatMarker(base, {
142
+ schema: 2,
143
+ lastWriter: "gsd-pi",
144
+ lastProjectedAt: "2026-07-05T05:35:19.379Z",
145
+ projections: {},
146
+ planning: {
147
+ active: true,
148
+ layout: "flat-phases",
149
+ projections: {
150
+ "ROADMAP.md": { sha: "live000000000000", entities: [] },
151
+ "gone/PLAN.md": { sha: "ghost00000000000", entities: [] },
152
+ },
153
+ passthrough: {
154
+ "gone/PATTERNS.md": { sha: "ghost11111111111", entities: [] },
155
+ },
156
+ },
157
+ piVersion: "1.4.0",
158
+ });
159
+
160
+ const removed = pruneOrphanedProjectionEntries(base);
161
+ assert.equal(removed, 2);
162
+
163
+ const marker = readCompatMarker(base);
164
+ assert.ok(marker.planning!.projections["ROADMAP.md"], "live planning projection must survive");
165
+ assert.equal(marker.planning!.projections["gone/PLAN.md"], undefined);
166
+ assert.equal(marker.planning!.passthrough["gone/PATTERNS.md"], undefined);
167
+ });
168
+
169
+ test("pruneOrphanedProjectionEntries is a no-op when nothing is orphaned", () => {
170
+ const base = makeTmpBase();
171
+ const rel = "phases/01-phase/01-01-PLAN.md";
172
+ mkdirSync(join(base, ".gsd", "phases", "01-phase"), { recursive: true });
173
+ writeFileSync(join(base, ".gsd", rel), "# plan\n", "utf-8");
174
+ writeCompatMarker(base, {
175
+ schema: 2,
176
+ lastWriter: "gsd-pi",
177
+ lastProjectedAt: "2026-07-05T05:35:19.379Z",
178
+ projections: { [rel]: { sha: "abc000000000000", entities: ["M001"] } },
179
+ piVersion: "1.4.0",
180
+ });
181
+
182
+ assert.equal(pruneOrphanedProjectionEntries(base), 0);
183
+ });
184
+
185
+ test("pruneOrphanedProjectionEntries returns 0 and writes nothing when marker is missing", () => {
186
+ const base = makeTmpBase();
187
+ assert.equal(pruneOrphanedProjectionEntries(base), 0);
188
+ const files = readdirSync(join(base, ".gsd"));
189
+ assert.ok(!files.includes(".compat.json"), "must not create a marker where none existed");
190
+ });
@@ -422,6 +422,51 @@ console.log('\n=== complete-milestone: existing SUMMARY.md preserved (#4598) ===
422
422
  cleanup(dbPath);
423
423
  }
424
424
 
425
+ // ═══════════════════════════════════════════════════════════════════════════
426
+ // complete-milestone: flat-phase compatibility SUMMARY.md is not hidden
427
+ // ═══════════════════════════════════════════════════════════════════════════
428
+
429
+ console.log('\n=== complete-milestone: flat-phase legacy-named SUMMARY.md preserved (#4598) ===');
430
+ {
431
+ const dbPath = tempDbPath();
432
+ openDatabase(dbPath);
433
+ const basePath = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-flat-complete-milestone-'));
434
+ const phaseDir = path.join(basePath, '.gsd', 'phases', '01-test-milestone');
435
+ fs.mkdirSync(path.join(phaseDir, 'slices', 'S01', 'tasks'), { recursive: true });
436
+ seedCompletedMilestone({ basePath, validationVerdict: 'pass' });
437
+
438
+ // Flat-phase projects may still contain legacy-named compatibility files.
439
+ // The no-overwrite guard must preserve those richer summaries instead of
440
+ // generating a canonical sibling that later readers would prefer.
441
+ const summaryPath = path.join(phaseDir, 'M001-SUMMARY.md');
442
+ const canonicalSummaryPath = path.join(phaseDir, '01-SUMMARY.md');
443
+ const sentinel = '# M001: Pre-existing flat-phase richer summary\n\nDO NOT OVERWRITE ME\n';
444
+ fs.writeFileSync(summaryPath, sentinel, 'utf-8');
445
+
446
+ const result = await handleCompleteMilestone(makeValidParams(), basePath);
447
+ assertTrue(!('error' in result), 'completion should still succeed when flat-phase compatibility SUMMARY.md exists');
448
+ if (!('error' in result)) {
449
+ assertEq(
450
+ result.summaryPath,
451
+ fs.realpathSync(summaryPath),
452
+ 'completion should report the preserved existing SUMMARY path',
453
+ );
454
+ assertEq(
455
+ fs.readFileSync(summaryPath, 'utf-8'),
456
+ sentinel,
457
+ 'flat-phase compatibility SUMMARY.md must be preserved, not hidden by a canonical rewrite',
458
+ );
459
+ assertTrue(
460
+ !fs.existsSync(canonicalSummaryPath),
461
+ 'completion must not create a canonical sibling over an existing compatibility summary',
462
+ );
463
+ assertEq(getMilestone('M001')!.status, 'complete', 'milestone should still be marked complete in DB');
464
+ }
465
+
466
+ cleanupDir(basePath);
467
+ cleanup(dbPath);
468
+ }
469
+
425
470
  // ═══════════════════════════════════════════════════════════════════════════
426
471
 
427
472
  report();
@@ -33,6 +33,45 @@ test("normalizeDbRows normalizes every row", () => {
33
33
  assert.deepEqual(normalizeDbRows([first, second]), [{ id: "S01" }, { id: "S02" }]);
34
34
  });
35
35
 
36
+ test("createDbAdapter strips named-parameter key prefixes for provider compatibility", () => {
37
+ const received: unknown[][] = [];
38
+ const rawStatement = {
39
+ run: (...params: unknown[]) => {
40
+ received.push(params);
41
+ return { changes: 1 };
42
+ },
43
+ get: (...params: unknown[]) => {
44
+ received.push(params);
45
+ return undefined;
46
+ },
47
+ all: (...params: unknown[]) => {
48
+ received.push(params);
49
+ return [];
50
+ },
51
+ };
52
+ const rawDb = {
53
+ exec: () => {},
54
+ prepare: () => rawStatement,
55
+ close: () => {},
56
+ };
57
+ const adapter = createDbAdapter(rawDb);
58
+ const stmt = adapter.prepare("INSERT INTO t (version, applied_at) VALUES (:version, :applied_at)");
59
+
60
+ stmt.run({ ":version": 1, "@name": "a", "$flag": true, bare: "kept" });
61
+ assert.deepEqual(received.pop(), [{ version: 1, name: "a", flag: true, bare: "kept" }]);
62
+
63
+ // Non-binding params pass through untouched: positional scalars, arrays,
64
+ // and binary blobs must not be treated as named-binding objects.
65
+ const blob = new Uint8Array([1, 2, 3]);
66
+ stmt.get("T01", blob);
67
+ const passthrough = received.pop() as unknown[];
68
+ assert.equal(passthrough[0], "T01");
69
+ assert.equal(passthrough[1], blob);
70
+
71
+ stmt.all({ plain: "unchanged" });
72
+ assert.deepEqual(received.pop(), [{ plain: "unchanged" }]);
73
+ });
74
+
36
75
  test("createDbAdapter caches prepared statements and clears cache on close", () => {
37
76
  const calls: unknown[] = [];
38
77
  const rawStatement = {
@@ -0,0 +1,66 @@
1
+ // gsd-pi — DB writability probe regression tests (#1234).
2
+ //
3
+ // Auto-start's DB gate only checked that the provider opened, not that the
4
+ // handle can actually write. A schema-current DB does zero writes during open,
5
+ // so a read-only / DBMOVED handle passed the open-only check and failed much
6
+ // later at the first authoritative write with an opaque "readonly database"
7
+ // error. `probeDbWritable` forces a real page write so that failure class is
8
+ // caught at the natural checkpoint.
9
+
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import { openDatabase, closeDatabase, probeDbWritable, _getAdapter } from "../gsd-db.ts";
17
+ import type { DbAdapter } from "../db-adapter.ts";
18
+
19
+ function makeBase(): string {
20
+ const dir = mkdtempSync(join(tmpdir(), "gsd-db-writable-probe-"));
21
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ test("probeDbWritable succeeds against a freshly opened writable database", () => {
26
+ const base = makeBase();
27
+ assert.equal(openDatabase(join(base, ".gsd", "gsd.db")), true);
28
+ try {
29
+ assert.deepEqual(probeDbWritable(), { ok: true });
30
+ } finally {
31
+ closeDatabase();
32
+ rmSync(base, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ test("probeDbWritable reports not-ok with detail when no database is open", () => {
37
+ closeDatabase();
38
+ const result = probeDbWritable();
39
+ assert.equal(result.ok, false);
40
+ assert.match(result.detail ?? "", /No database is open/u);
41
+ });
42
+
43
+ test("probeDbWritable reports not-ok when the page write fails (simulating a read-only handle)", () => {
44
+ const base = makeBase();
45
+ assert.equal(openDatabase(join(base, ".gsd", "gsd.db")), true);
46
+ const adapter = _getAdapter() as DbAdapter;
47
+ assert.ok(adapter);
48
+
49
+ // The read (prepare/get) still works; only the page-dirtying write throws,
50
+ // mirroring SQLITE_READONLY_DBMOVED which fails once a page is dirtied.
51
+ const origExec = adapter.exec.bind(adapter);
52
+ adapter.exec = (sql: string): void => {
53
+ if (sql.includes("user_version =")) throw new Error("attempt to write a readonly database");
54
+ return origExec(sql);
55
+ };
56
+
57
+ try {
58
+ const result = probeDbWritable();
59
+ assert.equal(result.ok, false);
60
+ assert.match(result.detail ?? "", /readonly database/u);
61
+ } finally {
62
+ adapter.exec = origExec;
63
+ closeDatabase();
64
+ rmSync(base, { recursive: true, force: true });
65
+ }
66
+ });
@@ -7,7 +7,7 @@ import { join } from "node:path";
7
7
  import { tmpdir } from "node:os";
8
8
  import { randomUUID } from "node:crypto";
9
9
 
10
- import { renderPlanFromDb, renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.ts";
10
+ import { renderPlanFromDb, renderRoadmapFromDb, renderAssessmentFromDb, renderReplanFromDb } from "../markdown-renderer.ts";
11
11
  import { clearPathCache } from "../paths.ts";
12
12
  import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask } from "../gsd-db.ts";
13
13
 
@@ -83,6 +83,27 @@ test("renderPlanFromDb does NOT write per-task plan files", async () => {
83
83
  assert.equal(result.taskPlanPaths.length, 0);
84
84
  });
85
85
 
86
+
87
+ // Replan uses the same slice-level target rules as assessment: flat-phase
88
+ // writes must stay at the phase root even if an old slices/SID/ dir exists.
89
+ test("renderReplanFromDb writes to the phase root even when a stray slices/SID/ dir exists", async () => {
90
+ const base = makeTmp();
91
+ const plan = await renderPlanFromDb(base, "M001", "S01");
92
+ const phaseDir = plan.planPath.slice(0, plan.planPath.lastIndexOf("01-01-PLAN.md"));
93
+ mkdirSync(join(phaseDir, "slices", "S01"), { recursive: true });
94
+ clearPathCache();
95
+
96
+ const result = await renderReplanFromDb(base, "M001", "S01", {
97
+ blockerTaskId: "T01",
98
+ blockerDescription: "Need a better path target.",
99
+ whatChanged: "Centralized slice writer path resolution.",
100
+ });
101
+
102
+ assert.match(result.replanPath, /phases[/\\]01-[^/\\]+[/\\]01-01-REPLAN\.md$/);
103
+ assert.ok(!result.replanPath.includes(join("slices", "S01")), "must not write under slices/S01/");
104
+ assert.ok(existsSync(result.replanPath), "replan file should exist at the phase root");
105
+ });
106
+
86
107
  // Regression for #1190: a stray slices/SID/ subdir inside a flat-phase milestone
87
108
  // must not divert the ASSESSMENT write into slices/SID/ (a hybrid path that
88
109
  // auto-mode verification cannot find). It must land at the phase root.