@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
@@ -9,7 +9,7 @@ import { join } from "node:path";
9
9
  import assert from "node:assert/strict";
10
10
  import test from "node:test";
11
11
 
12
- import { relMilestoneFile, resolveMilestoneFile } from "../paths.ts";
12
+ import { normalizeRealPath, relMilestoneFile, relSliceFile, resolveMilestoneFile, targetMilestoneFile } from "../paths.ts";
13
13
 
14
14
  function makeFlatPhaseFixture(): { basePath: string; cleanup: () => void } {
15
15
  const basePath = mkdtempSync(join(tmpdir(), "flat-phase-validation-"));
@@ -33,42 +33,82 @@ function makeFlatPhaseFixture(): { basePath: string; cleanup: () => void } {
33
33
  return { basePath, cleanup: () => rmSync(basePath, { recursive: true, force: true }) };
34
34
  }
35
35
 
36
- test("flat-phase: relMilestoneFile resolves to the layout-aware path", () => {
36
+ test("flat-phase: relMilestoneFile resolves to the layout-aware path", (t) => {
37
37
  const { basePath, cleanup } = makeFlatPhaseFixture();
38
- try {
39
- const rel = relMilestoneFile(basePath, "M001", "VALIDATION");
40
- assert.equal(rel, ".gsd/phases/01-foo/01-VALIDATION.md");
41
- const abs = join(basePath, rel);
42
- assert.equal(existsSync(abs), true);
43
- } finally {
44
- cleanup();
45
- }
38
+ t.after(cleanup);
39
+
40
+ const rel = relMilestoneFile(basePath, "M001", "VALIDATION");
41
+ assert.equal(rel, ".gsd/phases/01-foo/01-VALIDATION.md");
42
+ const abs = join(basePath, rel);
43
+ assert.equal(existsSync(abs), true);
44
+ });
45
+
46
+ test("flat-phase: relMilestoneFile fallback uses the milestone title when no directory exists", (t) => {
47
+ const basePath = mkdtempSync(join(tmpdir(), "flat-phase-missing-dir-"));
48
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
49
+
50
+ const rel = relMilestoneFile(basePath, "M001", "ROADMAP", "Milestone");
51
+ assert.equal(rel, ".gsd/phases/01-milestone/01-ROADMAP.md");
46
52
  });
47
53
 
48
- test("flat-phase: resolveMilestoneFile finds the on-disk file", () => {
54
+ test("legacy layout: relMilestoneFile fallback stays in milestones when the target dir is missing", (t) => {
55
+ const basePath = mkdtempSync(join(tmpdir(), "legacy-missing-dir-"));
56
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
57
+ const existingLegacyDir = join(basePath, ".gsd", "milestones", "M002");
58
+ mkdirSync(existingLegacyDir, { recursive: true });
59
+ writeFileSync(join(existingLegacyDir, "M002-ROADMAP.md"), "# existing legacy roadmap\n");
60
+
61
+ const rel = relMilestoneFile(basePath, "M001", "ROADMAP", "Milestone");
62
+
63
+ assert.equal(rel, ".gsd/milestones/M001/M001-ROADMAP.md");
64
+ });
65
+
66
+ test("legacy layout: relSliceFile fallback uses slices directory when target milestone is missing", (t) => {
67
+ const basePath = mkdtempSync(join(tmpdir(), "legacy-missing-slice-"));
68
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
69
+ const existingLegacyDir = join(basePath, ".gsd", "milestones", "M002");
70
+ mkdirSync(existingLegacyDir, { recursive: true });
71
+ writeFileSync(join(existingLegacyDir, "M002-ROADMAP.md"), "# existing legacy roadmap\n");
72
+
73
+ const rel = relSliceFile(basePath, "M001", "S01", "PLAN", "Milestone");
74
+
75
+ assert.equal(rel, ".gsd/milestones/M001/slices/S01/S01-PLAN.md");
76
+ });
77
+
78
+ test("flat-phase: targetMilestoneFile ignores legacy-named compatibility files when writing", (t) => {
79
+ const basePath = mkdtempSync(join(tmpdir(), "flat-phase-legacy-named-file-"));
80
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
81
+ const phaseDir = join(basePath, ".gsd", "phases", "01-foo");
82
+ mkdirSync(phaseDir, { recursive: true });
83
+ writeFileSync(join(phaseDir, "M001-ROADMAP.md"), "# legacy named roadmap\n");
84
+
85
+ assert.equal(
86
+ targetMilestoneFile(basePath, "M001", "ROADMAP", "Foo"),
87
+ join(normalizeRealPath(phaseDir), "01-ROADMAP.md"),
88
+ );
89
+ assert.equal(relMilestoneFile(basePath, "M001", "ROADMAP", "Foo"), ".gsd/phases/01-foo/M001-ROADMAP.md");
90
+ });
91
+
92
+ test("flat-phase: resolveMilestoneFile finds the on-disk file", (t) => {
49
93
  const { basePath, cleanup } = makeFlatPhaseFixture();
50
- try {
51
- const abs = resolveMilestoneFile(basePath, "M001", "VALIDATION");
52
- assert.ok(abs, "resolveMilestoneFile must return a path for the flat-phase fixture");
53
- assert.equal(abs!.endsWith("/.gsd/phases/01-foo/01-VALIDATION.md"), true);
54
- assert.equal(existsSync(abs!), true);
55
- } finally {
56
- cleanup();
57
- }
94
+ t.after(cleanup);
95
+
96
+ const abs = resolveMilestoneFile(basePath, "M001", "VALIDATION");
97
+ assert.ok(abs, "resolveMilestoneFile must return a path for the flat-phase fixture");
98
+ assert.equal(abs!.endsWith("/.gsd/phases/01-foo/01-VALIDATION.md"), true);
99
+ assert.equal(existsSync(abs!), true);
58
100
  });
59
101
 
60
- test("flat-phase: the legacy hardcoded path does NOT resolve (regression for #876)", () => {
102
+ test("flat-phase: the legacy hardcoded path does NOT resolve (regression for #876)", (t) => {
61
103
  const { basePath, cleanup } = makeFlatPhaseFixture();
62
- try {
63
- // This is the path that auto-verification.ts:264-270 used to construct.
64
- // It must NOT exist on a flat-phase project that's the whole bug.
65
- const legacyHardcoded = join(basePath, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
66
- assert.equal(
67
- existsSync(legacyHardcoded),
68
- false,
69
- "legacy hardcoded path must not exist in flat-phase fixture — if it does, the test fixture is wrong",
70
- );
71
- } finally {
72
- cleanup();
73
- }
104
+ t.after(cleanup);
105
+
106
+ // This is the path that auto-verification.ts:264-270 used to construct.
107
+ // It must NOT exist on a flat-phase project — that's the whole bug.
108
+ const legacyHardcoded = join(basePath, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
109
+ assert.equal(
110
+ existsSync(legacyHardcoded),
111
+ false,
112
+ "legacy hardcoded path must not exist in flat-phase fixture — if it does, the test fixture is wrong",
113
+ );
74
114
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, rmSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, rmSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { spawnSync } from "node:child_process";
@@ -9,6 +9,7 @@ import {
9
9
  gsdProjectionRoot,
10
10
  gsdRoot,
11
11
  milestonesDir,
12
+ resolveGsdPathContract,
12
13
  resolveSliceFile,
13
14
  resolveTaskFile,
14
15
  _clearGsdRootCache,
@@ -166,4 +167,90 @@ describe('paths', () => {
166
167
  cleanup(root);
167
168
  }
168
169
  });
170
+
171
+ test('Case 9: flat-phase task SUMMARY resolves at phase root despite a stray slices/SID/ dir', () => {
172
+ const root = tmp();
173
+ try {
174
+ // Hybrid flat-phase milestone: phase dir holds flat TID-SUMMARY.md, but a
175
+ // stray slices/S01/ folder also exists on disk. resolveSlicePath then points
176
+ // under slices/, so layout (phases/ vs milestones/) — not slicePath===phaseDir
177
+ // — must decide the flat-phase task summary location.
178
+ const phaseDir = join(root, ".gsd", "phases", "01-foo");
179
+ mkdirSync(join(phaseDir, "slices", "S01"), { recursive: true });
180
+ const flatSummary = join(phaseDir, "T01-SUMMARY.md");
181
+ writeFileSync(flatSummary, "# task summary\n");
182
+
183
+ _clearGsdRootCache();
184
+
185
+ assert.deepStrictEqual(
186
+ resolveTaskFile(root, "M001", "S01", "T01", "SUMMARY"),
187
+ flatSummary,
188
+ "flat-phase TID-SUMMARY.md at phase root is found even when slices/S01/ exists",
189
+ );
190
+ } finally { cleanup(root); }
191
+ });
192
+
193
+ test('Case 10: resolveGsdPathContract canonicalizes a symlinked .gsd root (regression #1239)', () => {
194
+ // WSL / external-state shape: `<projectRoot>/.gsd` is a symlink pointing at a
195
+ // real directory on another volume. Before #1239 the DB path was built from
196
+ // the unresolved symlink, so the workflow SQLite handle opened through a
197
+ // move-prone path and could pick the wrong journal mode / SQLITE_READONLY_DBMOVED.
198
+ const root = tmp();
199
+ try {
200
+ const realState = join(root, "real-state"); // canonical symlink target
201
+ mkdirSync(join(realState, "milestones"), { recursive: true });
202
+ const projectRoot = join(root, "project");
203
+ mkdirSync(projectRoot, { recursive: true });
204
+ symlinkSync(realState, join(projectRoot, ".gsd"), "dir");
205
+
206
+ _clearGsdRootCache();
207
+ const contract = resolveGsdPathContract(projectRoot);
208
+
209
+ const canonicalGsd = realpathSync.native(join(projectRoot, ".gsd"));
210
+ assert.deepStrictEqual(contract.projectGsd, canonicalGsd,
211
+ "projectGsd is the realpath'd target, not the unresolved .gsd symlink");
212
+ assert.deepStrictEqual(contract.projectDb, join(canonicalGsd, "gsd.db"),
213
+ "projectDb is built from the canonical .gsd root");
214
+ assert.deepStrictEqual(contract.projectGsd, gsdProjectionRoot(projectRoot),
215
+ "projectGsd agrees with gsdProjectionRoot on the canonical root");
216
+ assert.notStrictEqual(contract.projectDb, join(projectRoot, ".gsd", "gsd.db"),
217
+ "regression #1239: DB must not open through the unresolved .gsd symlink");
218
+ } finally { cleanup(root); }
219
+ });
220
+
221
+ test('Case 11: resolveGsdPathContract canonicalizes the external-state worktree DB path through a symlinked store (regression #1239)', () => {
222
+ const root = tmp();
223
+ const originalStateDir = process.env.GSD_STATE_DIR;
224
+ try {
225
+ const stateDir = join(root, "state");
226
+ process.env.GSD_STATE_DIR = stateDir;
227
+
228
+ // The external-state project store `<state>/projects/<hash>` is itself a
229
+ // symlink to a real directory on another volume (the WSL `.gsd` shape).
230
+ const realProjectGsd = join(root, "real-projects", "abc123");
231
+ mkdirSync(realProjectGsd, { recursive: true });
232
+ mkdirSync(join(stateDir, "projects"), { recursive: true });
233
+ const storeSymlink = join(stateDir, "projects", "abc123");
234
+ symlinkSync(realProjectGsd, storeSymlink, "dir");
235
+
236
+ const wtRoot = join(storeSymlink, "worktrees", "M002");
237
+ mkdirSync(join(wtRoot, ".gsd"), { recursive: true });
238
+
239
+ _clearGsdRootCache();
240
+ const contract = resolveGsdPathContract(wtRoot);
241
+
242
+ const canonicalStore = realpathSync.native(storeSymlink);
243
+ assert.ok(contract.isWorktree, "recognized as an external-state worktree layout");
244
+ assert.deepStrictEqual(contract.projectGsd, canonicalStore,
245
+ "projectGsd is the realpath'd project store, not the unresolved symlink");
246
+ assert.deepStrictEqual(contract.projectDb, join(canonicalStore, "gsd.db"),
247
+ "projectDb is built from the canonical project store");
248
+ assert.notStrictEqual(contract.projectDb, join(storeSymlink, "gsd.db"),
249
+ "regression #1239: DB must not open through the unresolved external-state symlink");
250
+ } finally {
251
+ if (originalStateDir === undefined) delete process.env.GSD_STATE_DIR;
252
+ else process.env.GSD_STATE_DIR = originalStateDir;
253
+ cleanup(root);
254
+ }
255
+ });
169
256
  });
@@ -13,6 +13,7 @@ import {
13
13
  isCompletedMilestoneTerminal,
14
14
  repairMissingMilestoneSummaryProjection,
15
15
  } from "../milestone-closeout.js";
16
+ import { targetMilestoneFile } from "../paths.js";
16
17
  import type { DispatchContext } from "../auto-dispatch.js";
17
18
 
18
19
  /** Build a minimal DispatchContext for the dispatch-policy branches under test. */
@@ -196,7 +197,7 @@ test("repairMissingMilestoneSummaryProjection succeeds when milestone dir does n
196
197
  const repair = await repairMissingMilestoneSummaryProjection(base, "M042");
197
198
  assert.equal(repair.ok, true, "repair should report success when handler creates the SUMMARY");
198
199
  assert.ok(
199
- existsSync(join(base, ".gsd", "phases", "42-m042", "42-SUMMARY.md")),
200
+ existsSync(targetMilestoneFile(base, "M042", "SUMMARY", "Done")),
200
201
  "repair should write the SUMMARY artifact to the canonical projection path",
201
202
  );
202
203
  });
@@ -65,7 +65,6 @@ const ALLOWED_IMPORTERS = new Set([
65
65
  "gsd/visualizer-data.ts",
66
66
  // prompt context text (display strings injected into unit prompts)
67
67
  "gsd/auto-prompts.ts",
68
- // cold-path maintenance command (branch cleanup messaging)
69
68
  "gsd/commands-maintenance.ts",
70
69
  // display-only GitHub issue/PR body sync
71
70
  "github-sync/sync.ts",
@@ -16,6 +16,7 @@ import {
16
16
  runPreDispatchHooks,
17
17
  persistHookState,
18
18
  restoreHookState,
19
+ reconcileRestoredHookDispatch,
19
20
  clearPersistedHookState,
20
21
  getHookStatus,
21
22
  formatHookStatus,
@@ -225,6 +226,233 @@ test('Blocking hook restored from disk does not trust artifact without clean hoo
225
226
  }
226
227
  });
227
228
 
229
+ test('Restore reconciliation re-enqueues the lost hook dispatch (#1246)', () => {
230
+ resetHookState();
231
+ const base = createFixtureBase();
232
+ try {
233
+ writeHookPreferences(base, ` - name: plan-review
234
+ after:
235
+ - plan-slice
236
+ prompt: Review the plan for {milestoneId}/{sliceId}
237
+ artifact: PLAN-REVIEW.md
238
+ criticality: blocking
239
+ max_cycles: 2
240
+ `);
241
+ // Trigger unit completes: activeHook set + persisted, dispatch enqueued.
242
+ const dispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
243
+ assert.ok(dispatch, "gate dispatches on trigger unit completion");
244
+ assert.equal(dispatch.unitType, "hook/plan-review");
245
+ persistHookState(base);
246
+
247
+ // Pause/resume: activeHook restored, but the session-local sidecar queue is
248
+ // gone (never persisted).
249
+ resetHookState();
250
+ restoreHookState(base);
251
+ assert.ok(getActiveHook(), "activeHook restored from disk");
252
+
253
+ // Reconciliation re-enqueues the missing dispatch so the hook actually runs.
254
+ const sidecarQueue: any[] = [];
255
+ reconcileRestoredHookDispatch(base, sidecarQueue);
256
+ assert.equal(sidecarQueue.length, 1, "lost hook dispatch is re-enqueued");
257
+ assert.equal(sidecarQueue[0].kind, "hook");
258
+ assert.equal(sidecarQueue[0].unitType, "hook/plan-review");
259
+ assert.equal(sidecarQueue[0].unitId, "M002/S01");
260
+ assert.match(sidecarQueue[0].prompt, /Review the plan for M002\/S01/);
261
+ } finally {
262
+ resetHookState();
263
+ invalidateAllCaches();
264
+ rmSync(base, { recursive: true, force: true });
265
+ }
266
+ });
267
+
268
+ test('Restore reconciliation is a no-op when the dispatch is already queued (#1246)', () => {
269
+ resetHookState();
270
+ const base = createFixtureBase();
271
+ try {
272
+ writeHookPreferences(base, ` - name: plan-review
273
+ after:
274
+ - plan-slice
275
+ prompt: Review the plan
276
+ artifact: PLAN-REVIEW.md
277
+ criticality: blocking
278
+ max_cycles: 2
279
+ `);
280
+ const dispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
281
+ assert.ok(dispatch, "gate dispatches on trigger unit completion");
282
+ persistHookState(base);
283
+ resetHookState();
284
+ restoreHookState(base);
285
+
286
+ const sidecarQueue: any[] = [
287
+ { kind: "hook", unitType: "hook/plan-review", unitId: "M002/S01", prompt: "already here" },
288
+ ];
289
+ reconcileRestoredHookDispatch(base, sidecarQueue);
290
+ assert.equal(sidecarQueue.length, 1, "does not duplicate an existing hook dispatch");
291
+ assert.equal(sidecarQueue[0].prompt, "already here");
292
+ } finally {
293
+ resetHookState();
294
+ invalidateAllCaches();
295
+ rmSync(base, { recursive: true, force: true });
296
+ }
297
+ });
298
+
299
+ test('Restore reconciliation is a no-op with no active hook (#1246)', () => {
300
+ resetHookState();
301
+ const base = createFixtureBase();
302
+ try {
303
+ const sidecarQueue: any[] = [];
304
+ reconcileRestoredHookDispatch(base, sidecarQueue);
305
+ assert.equal(sidecarQueue.length, 0, "nothing enqueued when no active hook");
306
+ } finally {
307
+ resetHookState();
308
+ invalidateAllCaches();
309
+ rmSync(base, { recursive: true, force: true });
310
+ }
311
+ });
312
+
313
+ test('Blocking hook re-dispatches once after a lost dispatch at default max_cycles', () => {
314
+ resetHookState();
315
+ const base = createFixtureBase();
316
+ try {
317
+ // Default max_cycles (1): a cycle is charged at dispatch time.
318
+ writeHookPreferences(base, ` - name: plan-review
319
+ after:
320
+ - plan-slice
321
+ prompt: Review plan
322
+ artifact: PLAN-REVIEW.md
323
+ criticality: blocking
324
+ `);
325
+
326
+ const firstDispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
327
+ assert.ok(firstDispatch, "gate dispatches and consumes its cycle at dispatch");
328
+ assert.equal(firstDispatch.unitType, "hook/plan-review");
329
+ persistHookState(base);
330
+
331
+ // Simulate a lost dispatch: the hook never produced its own unit-end, the
332
+ // trigger unit resumes instead. The dispatch-consumed cycle is refunded once
333
+ // so the gate re-dispatches rather than hard-blocking on a hook that never ran.
334
+ resetHookState();
335
+ restoreHookState(base);
336
+ const redispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
337
+ assert.ok(redispatch, "lost dispatch re-dispatches the gate instead of blocking");
338
+ assert.equal(redispatch.unitType, "hook/plan-review");
339
+ assert.deepStrictEqual(consumeGateBlock(), null, "no gate block on the first lost dispatch");
340
+ persistHookState(base);
341
+
342
+ // A second lost dispatch consumes the budget and blocks — the refund is one-shot.
343
+ resetHookState();
344
+ restoreHookState(base);
345
+ const blocked = checkPostUnitHooks("plan-slice", "M002/S01", base);
346
+ assert.deepStrictEqual(blocked, null, "second lost dispatch exhausts the budget");
347
+ const block = consumeGateBlock();
348
+ assert.ok(block, "gate block recorded after the one-shot re-dispatch is used");
349
+ assert.equal(block.hookName, "plan-review");
350
+ assert.match(block.reason, /gate cycle budget exhausted/);
351
+ } finally {
352
+ resetHookState();
353
+ invalidateAllCaches();
354
+ rmSync(base, { recursive: true, force: true });
355
+ }
356
+ });
357
+
358
+ test('Blocking hook lost-dispatch refund is one-shot and bounded at max_cycles=2', () => {
359
+ resetHookState();
360
+ const base = createFixtureBase();
361
+ try {
362
+ writeHookPreferences(base, ` - name: plan-review
363
+ after:
364
+ - plan-slice
365
+ prompt: Review plan
366
+ artifact: PLAN-REVIEW.md
367
+ criticality: blocking
368
+ max_cycles: 2
369
+ `);
370
+
371
+ // Simulate a lost dispatch: persist, drop in-memory state, restore, then the
372
+ // trigger unit resumes instead of the hook producing its own unit-end.
373
+ const lose = () => {
374
+ persistHookState(base);
375
+ resetHookState();
376
+ restoreHookState(base);
377
+ return checkPostUnitHooks("plan-slice", "M002/S01", base);
378
+ };
379
+
380
+ assert.ok(checkPostUnitHooks("plan-slice", "M002/S01", base), "initial dispatch");
381
+
382
+ // Refund is one-shot, but max_cycles=2 still has real budget, so lost#2 also
383
+ // re-dispatches by consuming a genuine cycle (not another refund).
384
+ assert.ok(lose(), "lost#1 re-dispatches (one-shot refund)");
385
+ assert.equal(consumeGateBlock(), null, "no block after lost#1");
386
+ assert.ok(lose(), "lost#2 re-dispatches from real budget");
387
+ assert.equal(consumeGateBlock(), null, "no block after lost#2");
388
+
389
+ // Total dispatches are bounded to max_cycles + 1 (the single forgiven loss),
390
+ // so the third lost dispatch blocks. redispatchedGateKeys guarantees this
391
+ // terminates rather than looping forever on repeated lost resumes.
392
+ assert.equal(lose(), null, "lost#3 exhausts the bounded budget");
393
+ const block = consumeGateBlock();
394
+ assert.ok(block, "gate block recorded on lost#3");
395
+ assert.match(block.reason, /gate cycle budget exhausted/);
396
+ } finally {
397
+ resetHookState();
398
+ invalidateAllCaches();
399
+ rmSync(base, { recursive: true, force: true });
400
+ }
401
+ });
402
+
403
+ test('Blocking hook lost dispatch does not steal a real-run cycle at max_cycles=2', () => {
404
+ resetHookState();
405
+ const base = createFixtureBase();
406
+ try {
407
+ writeHookPreferences(base, ` - name: plan-review
408
+ after:
409
+ - plan-slice
410
+ prompt: Review plan
411
+ artifact: PLAN-REVIEW.md
412
+ criticality: blocking
413
+ max_cycles: 2
414
+ `);
415
+
416
+ // Dispatch, then lose it once (the one-shot refund is spent here).
417
+ assert.ok(checkPostUnitHooks("plan-slice", "M002/S01", base), "initial dispatch");
418
+ persistHookState(base);
419
+ resetHookState();
420
+ restoreHookState(base);
421
+ assert.ok(
422
+ checkPostUnitHooks("plan-slice", "M002/S01", base),
423
+ "lost dispatch re-dispatches (refund spent)",
424
+ );
425
+
426
+ // The hook now actually runs, each run reporting a failed verdict. Because a
427
+ // lost dispatch must not consume the hook's real-run budget, max_cycles=2
428
+ // still permits two genuine runs before the gate blocks — spending the
429
+ // refund early did not rob a later real cycle.
430
+ mkdirSync(join(base, ".gsd", "milestones", "M002", "slices", "S01"), { recursive: true });
431
+ writeFileSync(
432
+ resolveHookArtifactPath(base, "M002/S01", "PLAN-REVIEW.md"),
433
+ "---\nverdict: failed\n---\n\nRejected.\n",
434
+ "utf-8",
435
+ );
436
+ assert.ok(
437
+ checkPostUnitHooks("hook/plan-review", "M002/S01", base),
438
+ "real run #1 (failed verdict) re-dispatches",
439
+ );
440
+ assert.equal(consumeGateBlock(), null, "no block after the first real run");
441
+ assert.equal(
442
+ checkPostUnitHooks("hook/plan-review", "M002/S01", base),
443
+ null,
444
+ "real run #2 (failed verdict) exhausts the real budget",
445
+ );
446
+ const block = consumeGateBlock();
447
+ assert.ok(block, "gate block recorded only after two real runs");
448
+ assert.match(block.reason, /gate cycle budget exhausted/);
449
+ } finally {
450
+ resetHookState();
451
+ invalidateAllCaches();
452
+ rmSync(base, { recursive: true, force: true });
453
+ }
454
+ });
455
+
228
456
  test('Blocking hook needs-rework verdict requests trigger unit retry', () => {
229
457
  resetHookState();
230
458
  const base = createFixtureBase();
@@ -454,4 +682,45 @@ test('triggerHookManually: with configured hook', () => {
454
682
  }
455
683
  });
456
684
 
685
+ // ─── Hook dispatch results omit a primary-only model (#1229) ───────────────
686
+ // Both the auto-mode (checkPostUnitHooks → _startHook) and manual
687
+ // (triggerHookManually) dispatch paths must NOT carry a resolved primary model
688
+ // on the dispatch result. Emitting the primary alone discards the configured
689
+ // fallbacks[] chain: the auto path would re-apply it over a fallback already
690
+ // selected by selectAndApplyModel, and the manual path (dispatchHookUnit) now
691
+ // resolves the full chain itself. The dispatch result therefore leaves `model`
692
+ // unset so downstream resolution honors fallbacks.
693
+ test('Dispatch results omit primary-only model so fallbacks survive (#1229)', () => {
694
+ resetHookState();
695
+ const base = createFixtureBase();
696
+ try {
697
+ writeHookPreferences(base, ` - name: code-review
698
+ after:
699
+ - execute-task
700
+ prompt: Review the change
701
+ artifact: REVIEW-PASS.md
702
+ model:
703
+ model: primary-model
704
+ fallbacks:
705
+ - fallback-a
706
+ - fallback-b
707
+ `);
708
+
709
+ const autoDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
710
+ assert.ok(autoDispatch, "auto-mode dispatches the configured hook");
711
+ assert.equal(autoDispatch.unitType, "hook/code-review", "auto dispatch is hook-prefixed");
712
+ assert.equal(autoDispatch.model, undefined, "auto dispatch does not carry a primary-only model");
713
+
714
+ resetHookState();
715
+
716
+ const manualDispatch = triggerHookManually("code-review", "execute-task", "M001/S01/T01", base);
717
+ assert.ok(manualDispatch, "manual trigger dispatches the configured hook");
718
+ assert.equal(manualDispatch.unitType, "hook/code-review", "manual dispatch is hook-prefixed");
719
+ assert.equal(manualDispatch.model, undefined, "manual dispatch does not carry a primary-only model");
720
+ } finally {
721
+ resetHookState();
722
+ rmSync(base, { recursive: true, force: true });
723
+ }
724
+ });
725
+
457
726
  });
@@ -260,7 +260,14 @@ test("post-unit blocking gate pauses auto-mode on needs-attention verdict", asyn
260
260
  const result = await postUnitPostVerification(pctx);
261
261
  assert.equal(result, "stopped");
262
262
  assert.equal(pauseAuto.mock.callCount(), 1);
263
- assert.match(notifications.join("\n"), /Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01/);
263
+ // Regression (#1245): the block message must name BOTH the hook's trigger
264
+ // unit (execute-task M001/S01/T01) and the just-completed unit that surfaced
265
+ // the block (s.currentUnit = hook/review-arbiter M001/S01/T01), so the pause
266
+ // is unambiguous to triage. The pre-fix message stopped at the trigger unit.
267
+ assert.match(
268
+ notifications.join("\n"),
269
+ /Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01 \(detected on completion of hook\/review-arbiter M001\/S01\/T01\)/,
270
+ );
264
271
  assert.match(notifications.join("\n"), /\/gsd status/);
265
272
  } finally {
266
273
  process.chdir(originalCwd);
@@ -160,6 +160,60 @@ describe('readUatGateVerdict — DB fallback for orphaned ASSESSMENT', () => {
160
160
  assert.equal(result, null, 'roadmap-scoped assessments must not satisfy the UAT gate');
161
161
  });
162
162
 
163
+ test('a backfilled assessment does NOT satisfy the UAT gate (#1258)', async () => {
164
+ // The milestone-validation backfill fabricates a `verdict: PASS` ASSESSMENT
165
+ // for completed slices that never produced a real UAT result (e.g.
166
+ // artifact-driven UAT that was never dispatched). Filed under scope
167
+ // 'backfill', that placeholder must never be read as a genuine UAT sign-off —
168
+ // otherwise "never checked" is silently treated as "passed". Even with the
169
+ // fabricated file present on disk, the gate must fall through to the
170
+ // authoritative run-uat row (absent here) and return null.
171
+ const file = canonicalAssessmentPath(basePath);
172
+ fs.mkdirSync(path.dirname(file), { recursive: true });
173
+ fs.writeFileSync(file, RUNTIME_PASS_BODY);
174
+ insertAssessment({
175
+ path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
176
+ milestoneId: MID,
177
+ sliceId: SLICE,
178
+ status: 'pass',
179
+ scope: 'backfill',
180
+ fullContent: RUNTIME_PASS_BODY,
181
+ });
182
+
183
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
184
+
185
+ assert.equal(result, null, 'backfilled assessments must not satisfy the UAT gate');
186
+ });
187
+
188
+ test('a genuine run-uat row still resolves even when a backfilled file exists (#1258)', async () => {
189
+ // If a real UAT was later recorded, its authoritative run-uat row must win
190
+ // over the fabricated placeholder rather than being masked by it.
191
+ const file = canonicalAssessmentPath(basePath);
192
+ fs.mkdirSync(path.dirname(file), { recursive: true });
193
+ fs.writeFileSync(file, RUNTIME_PASS_BODY);
194
+ insertAssessment({
195
+ path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
196
+ milestoneId: MID,
197
+ sliceId: SLICE,
198
+ status: 'pass',
199
+ scope: 'backfill',
200
+ fullContent: RUNTIME_PASS_BODY,
201
+ });
202
+ insertAssessment({
203
+ path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
204
+ milestoneId: MID,
205
+ sliceId: SLICE,
206
+ status: 'pass',
207
+ scope: 'run-uat',
208
+ fullContent: RUNTIME_PASS_BODY,
209
+ });
210
+
211
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
212
+
213
+ assert.ok(result, 'a genuine run-uat sign-off must resolve');
214
+ assert.equal(result!.verdict, 'pass');
215
+ });
216
+
163
217
  test('returns null when no assessment and no file exist (fallback does not hallucinate)', async () => {
164
218
  const result = await readUatGateVerdict(basePath, MID, SLICE);
165
219
  assert.equal(result, null);