@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
@@ -2,7 +2,7 @@
2
2
  // File Purpose: Verifies UOK kernel path selection and legacy fallback telemetry.
3
3
  import test from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
5
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { tmpdir } from "node:os";
8
8
 
@@ -14,6 +14,16 @@ import type { LoopDeps } from "../auto/loop-deps.ts";
14
14
  import { gsdRoot } from "../paths.ts";
15
15
  import type { GSDPreferences } from "../preferences.ts";
16
16
  import { getLegacyTelemetry, resetLegacyTelemetry } from "../legacy-telemetry.ts";
17
+ import { _getAdapter, closeDatabase, openDatabase } from "../gsd-db.ts";
18
+ import { UokGateRunner } from "../uok/gate-runner.ts";
19
+ import { applyModelPolicyFilter } from "../uok/model-policy.ts";
20
+ import {
21
+ clearUnifiedAuditOverrideForTests,
22
+ isUnifiedAuditEnabled,
23
+ setUnifiedAuditEnabled,
24
+ } from "../uok/audit-toggle.ts";
25
+ import { writeEscalationArtifact } from "../escalation.ts";
26
+ import { peekLogs, _resetLogs } from "../workflow-logger.ts";
17
27
 
18
28
  function makeBasePath(): string {
19
29
  return mkdtempSync(join(tmpdir(), "gsd-uok-kernel-"));
@@ -104,7 +114,8 @@ test("runAutoLoopWithUok uses kernel path by default and records uok-kernel pari
104
114
  assert.ok(args.calls.kernelDeps);
105
115
  assert.notEqual(args.calls.kernelDeps, args.deps);
106
116
  assert.ok(args.calls.kernelDeps?.uokObserver);
107
- assert.equal(process.env.GSD_UOK_AUDIT_UNIFIED, "0");
117
+ assert.equal(isUnifiedAuditEnabled(), true);
118
+ assert.equal(isUnifiedAuditEnabled(basePath), false);
108
119
 
109
120
  const events = readParityEvents(basePath);
110
121
  assert.equal(events.length, 2);
@@ -120,6 +131,37 @@ test("runAutoLoopWithUok uses kernel path by default and records uok-kernel pari
120
131
  }
121
132
  });
122
133
 
134
+ test("runAutoLoopWithUok keeps audit disabled for legacy-wrapper while restoring process override", async () => {
135
+ const basePath = makeBasePath();
136
+ clearUnifiedAuditOverrideForTests();
137
+ try {
138
+ resetLegacyTelemetry();
139
+ const args = makeArgs(basePath, {
140
+ uok: {
141
+ enabled: false,
142
+ audit_unified: { enabled: false },
143
+ },
144
+ });
145
+ await runAutoLoopWithUok(args);
146
+
147
+ assert.equal(args.calls.kernel, 0);
148
+ assert.equal(args.calls.legacy, 1);
149
+ assert.equal(isUnifiedAuditEnabled(), true);
150
+ assert.equal(isUnifiedAuditEnabled(basePath), false);
151
+
152
+ const events = readParityEvents(basePath);
153
+ assert.equal(events.length, 2);
154
+ assert.equal(events[0]?.path, "legacy-wrapper");
155
+ assert.equal(events[1]?.path, "legacy-wrapper");
156
+ assert.equal(events[1]?.status, "ok");
157
+ assert.equal(getLegacyTelemetry()["legacy.uokFallbackUsed"], 1);
158
+ } finally {
159
+ clearUnifiedAuditOverrideForTests();
160
+ resetLegacyTelemetry();
161
+ rmSync(basePath, { recursive: true, force: true });
162
+ }
163
+ });
164
+
123
165
  test("runAutoLoopWithUok uses legacy path when explicit legacy fallback is enabled", async () => {
124
166
  const basePath = makeBasePath();
125
167
  try {
@@ -176,3 +218,185 @@ test("runAutoLoopWithUok respects GSD_UOK_FORCE_LEGACY emergency switch", async
176
218
  rmSync(basePath, { recursive: true, force: true });
177
219
  }
178
220
  });
221
+
222
+ test("runAutoLoopWithUok records error exit and restores previous audit override when kernel fails", async () => {
223
+ const basePath = makeBasePath();
224
+ clearUnifiedAuditOverrideForTests();
225
+ setUnifiedAuditEnabled(false);
226
+ try {
227
+ resetLegacyTelemetry();
228
+ const args = makeArgs(basePath, {
229
+ uok: {
230
+ enabled: true,
231
+ audit_unified: { enabled: true },
232
+ gitops: { enabled: false },
233
+ },
234
+ });
235
+ args.runKernelLoop = async (_ctx, _pi, _s, loopDeps): Promise<void> => {
236
+ args.calls.kernel += 1;
237
+ args.calls.kernelDeps = loopDeps;
238
+ throw new Error("kernel exploded");
239
+ };
240
+
241
+ await assert.rejects(
242
+ () => runAutoLoopWithUok(args),
243
+ /kernel exploded/,
244
+ );
245
+
246
+ assert.equal(args.calls.kernel, 1);
247
+ assert.equal(args.calls.legacy, 0);
248
+ assert.equal(isUnifiedAuditEnabled(), false);
249
+
250
+ const events = readParityEvents(basePath);
251
+ assert.equal(events.length, 2);
252
+ assert.equal(events[0]?.phase, "enter");
253
+ assert.equal(events[1]?.phase, "exit");
254
+ assert.equal(events[1]?.status, "error");
255
+ assert.match(String(events[1]?.error), /kernel exploded/);
256
+ } finally {
257
+ clearUnifiedAuditOverrideForTests();
258
+ resetLegacyTelemetry();
259
+ rmSync(basePath, { recursive: true, force: true });
260
+ }
261
+ });
262
+
263
+ test("runAutoLoopWithUok treats kernel-enter audit failures as telemetry-only", async (t) => {
264
+ const basePath = makeBasePath();
265
+ const dbPath = join(basePath, ".gsd", "gsd.db");
266
+ clearUnifiedAuditOverrideForTests();
267
+ _resetLogs();
268
+ mkdirSync(join(basePath, ".gsd"), { recursive: true });
269
+ assert.equal(openDatabase(dbPath), true, "DB must open for this scenario");
270
+ t.after(() => {
271
+ clearUnifiedAuditOverrideForTests();
272
+ closeDatabase();
273
+ _resetLogs();
274
+ rmSync(basePath, { recursive: true, force: true });
275
+ });
276
+
277
+ _getAdapter()!.exec("DROP TABLE audit_events");
278
+
279
+ const args = makeArgs(basePath, {
280
+ uok: {
281
+ enabled: true,
282
+ audit_unified: { enabled: true },
283
+ gitops: { enabled: false },
284
+ },
285
+ });
286
+ args.runKernelLoop = async (_ctx, _pi, _s, loopDeps): Promise<void> => {
287
+ args.calls.kernel += 1;
288
+ args.calls.kernelDeps = loopDeps;
289
+ const observer = loopDeps.uokObserver;
290
+ assert.ok(observer, "kernel path must still install a turn observer");
291
+ observer.onTurnStart({
292
+ basePath,
293
+ traceId: "trace-audit-down",
294
+ turnId: "turn-audit-down",
295
+ iteration: 1,
296
+ unitType: "execute-task",
297
+ unitId: "M001/S01/T01",
298
+ startedAt: new Date().toISOString(),
299
+ });
300
+ observer.onTurnResult({
301
+ traceId: "trace-audit-down",
302
+ turnId: "turn-audit-down",
303
+ iteration: 1,
304
+ unitType: "execute-task",
305
+ unitId: "M001/S01/T01",
306
+ status: "completed",
307
+ failureClass: "none",
308
+ phaseResults: [],
309
+ startedAt: new Date().toISOString(),
310
+ finishedAt: new Date().toISOString(),
311
+ });
312
+
313
+ const gateRunner = new UokGateRunner();
314
+ gateRunner.register({
315
+ id: "test-gate",
316
+ type: "policy",
317
+ execute: async () => ({
318
+ outcome: "pass",
319
+ failureClass: "none",
320
+ rationale: "ok",
321
+ }),
322
+ });
323
+ await gateRunner.run("test-gate", {
324
+ basePath,
325
+ traceId: "trace-audit-down",
326
+ turnId: "turn-audit-down",
327
+ unitType: "execute-task",
328
+ unitId: "M001/S01/T01",
329
+ });
330
+
331
+ const filtered = applyModelPolicyFilter(
332
+ [{ id: "model-a", provider: "openai", api: "responses" }],
333
+ {
334
+ basePath,
335
+ traceId: "trace-audit-down",
336
+ turnId: "turn-audit-down",
337
+ unitType: "execute-task",
338
+ },
339
+ );
340
+ assert.equal(filtered.eligible.length, 1);
341
+
342
+ mkdirSync(join(basePath, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), {
343
+ recursive: true,
344
+ });
345
+ writeEscalationArtifact(basePath, {
346
+ version: 1,
347
+ taskId: "T01",
348
+ sliceId: "S01",
349
+ milestoneId: "M001",
350
+ question: "Choose a path",
351
+ options: [
352
+ { id: "a", label: "A", tradeoffs: "First path" },
353
+ { id: "b", label: "B", tradeoffs: "Second path" },
354
+ ],
355
+ recommendation: "a",
356
+ recommendationRationale: "Test recommendation",
357
+ continueWithDefault: false,
358
+ createdAt: new Date().toISOString(),
359
+ });
360
+ };
361
+
362
+ await runAutoLoopWithUok(args);
363
+
364
+ assert.equal(args.calls.kernel, 1);
365
+ assert.equal(args.calls.legacy, 0);
366
+
367
+ const events = readParityEvents(basePath);
368
+ assert.equal(events.length, 3);
369
+ assert.equal(events[0]?.phase, "enter");
370
+ assert.equal(events[1]?.phase, "telemetry-error");
371
+ assert.equal(events[1]?.telemetry, "uok-kernel-enter");
372
+ assert.equal(events[2]?.phase, "exit");
373
+ assert.equal(events[2]?.status, "ok");
374
+ assert.equal(isUnifiedAuditEnabled(), true);
375
+ assert.equal(isUnifiedAuditEnabled(basePath), false);
376
+ assert.ok(
377
+ peekLogs().some(entry =>
378
+ entry.severity === "warn" &&
379
+ entry.component === "db" &&
380
+ entry.message.includes("uok-kernel-enter audit emit failed"),
381
+ ),
382
+ "degraded audit path should emit a non-fatal warning",
383
+ );
384
+
385
+ assert.doesNotThrow(() => {
386
+ writeEscalationArtifact(basePath, {
387
+ version: 1,
388
+ taskId: "T02",
389
+ sliceId: "S01",
390
+ milestoneId: "M001",
391
+ question: "Continue?",
392
+ options: [
393
+ { id: "yes", label: "Yes", tradeoffs: "Continue" },
394
+ { id: "no", label: "No", tradeoffs: "Stop" },
395
+ ],
396
+ recommendation: "yes",
397
+ recommendationRationale: "Audit stayed disabled after degraded enter",
398
+ continueWithDefault: true,
399
+ createdAt: new Date().toISOString(),
400
+ });
401
+ });
402
+ });
@@ -12,8 +12,13 @@ import {
12
12
  registerToolCompatibility,
13
13
  resetToolCompatibilityRegistry,
14
14
  } from "@gsd/pi-coding-agent";
15
+ import {
16
+ clearUnifiedAuditOverrideForTests,
17
+ setUnifiedAuditEnabled,
18
+ } from "../uok/audit-toggle.ts";
15
19
 
16
20
  test.afterEach(() => {
21
+ clearUnifiedAuditOverrideForTests();
17
22
  resetToolCompatibilityRegistry();
18
23
  });
19
24
 
@@ -32,6 +37,7 @@ test("uok model policy builds requirement vectors from unit metadata", () => {
32
37
  test("uok model policy enforces provider/api/tool constraints and emits decision audit events", () => {
33
38
  const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-model-policy-"));
34
39
  try {
40
+ setUnifiedAuditEnabled(true);
35
41
  mkdirSync(join(basePath, ".gsd"), { recursive: true });
36
42
  registerToolCompatibility("screenshot", { producesImages: true });
37
43
 
@@ -6,10 +6,10 @@ import assert from 'node:assert/strict';
6
6
  import { existsSync, mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { tmpdir } from 'node:os';
9
- import { regenerateIfMissing, renderPlanContent, renderStateProjection } from '../workflow-projections.ts';
9
+ import { regenerateIfMissing, renderPlanContent, renderStateProjection, renderSummaryProjection } from '../workflow-projections.ts';
10
10
  import type { SliceRow, TaskRow } from '../gsd-db.ts';
11
11
  import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from '../gsd-db.ts';
12
- import { clearPathCache, _clearGsdRootCache } from '../paths.ts';
12
+ import { clearPathCache, _clearGsdRootCache, normalizeRealPath, resolveMilestoneFile, resolveTaskFile } from '../paths.ts';
13
13
  import { invalidateStateCache } from '../state.ts';
14
14
  import { clearParseCache } from '../files.ts';
15
15
 
@@ -243,6 +243,217 @@ test('workflow-projections: regenerateIfMissing PLAN restores slice plan and tas
243
243
  }
244
244
  });
245
245
 
246
+ test('workflow-projections: regenerateIfMissing ROADMAP is idempotent for flat-phase roadmap projections', async () => {
247
+ const base = mkdtempSync(join(tmpdir(), 'gsd-projections-flat-roadmap-'));
248
+ const dbPath = join(base, '.gsd', 'gsd.db');
249
+ const phaseDir = join(base, '.gsd', 'phases', '01-milestone');
250
+ mkdirSync(phaseDir, { recursive: true });
251
+ openDatabase(dbPath);
252
+ clearParseCache();
253
+ clearPathCache();
254
+ _clearGsdRootCache();
255
+ invalidateStateCache();
256
+
257
+ try {
258
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active', planning: { vision: 'Ship a layout-aware roadmap.' } });
259
+ insertSlice({
260
+ id: 'S01',
261
+ milestoneId: 'M001',
262
+ title: 'Flat slice',
263
+ status: 'pending',
264
+ demo: 'Roadmap exists at the flat-phase path.',
265
+ planning: { goal: 'Keep ROADMAP regeneration idempotent.' },
266
+ });
267
+
268
+ const roadmapPath = join(phaseDir, '01-ROADMAP.md');
269
+ writeFileSync(roadmapPath, '# existing flat roadmap\n');
270
+
271
+ const regenerated = await regenerateIfMissing(base, 'M001', 'S01', 'ROADMAP');
272
+
273
+ assert.equal(regenerated, false, 'existing flat-phase ROADMAP is detected without rewriting');
274
+ assert.equal(readFileSync(roadmapPath, 'utf-8'), '# existing flat roadmap\n');
275
+ assert.equal(normalizeRealPath(resolveMilestoneFile(base, 'M001', 'ROADMAP') ?? ''), normalizeRealPath(roadmapPath));
276
+ } finally {
277
+ closeDatabase();
278
+ rmSync(base, { recursive: true, force: true });
279
+ }
280
+ });
281
+
282
+ test('workflow-projections: regenerateIfMissing ROADMAP regenerates missing flat-phase roadmap projections', async () => {
283
+ const base = mkdtempSync(join(tmpdir(), 'gsd-projections-missing-roadmap-'));
284
+ const dbPath = join(base, '.gsd', 'gsd.db');
285
+ const phaseDir = join(base, '.gsd', 'phases', '01-milestone');
286
+ mkdirSync(phaseDir, { recursive: true });
287
+ openDatabase(dbPath);
288
+ clearParseCache();
289
+ clearPathCache();
290
+ _clearGsdRootCache();
291
+ invalidateStateCache();
292
+
293
+ try {
294
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active', planning: { vision: 'Ship a layout-aware roadmap.' } });
295
+ insertSlice({
296
+ id: 'S01',
297
+ milestoneId: 'M001',
298
+ title: 'Flat slice',
299
+ status: 'pending',
300
+ demo: 'Roadmap regenerates at the flat-phase path.',
301
+ planning: { goal: 'Recover ROADMAP from DB.' },
302
+ });
303
+
304
+ const roadmapPath = join(phaseDir, '01-ROADMAP.md');
305
+ const legacyRoadmapPath = join(base, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md');
306
+ assert.ok(!existsSync(roadmapPath), 'precondition: flat-phase ROADMAP is absent');
307
+
308
+ const regenerated = await regenerateIfMissing(base, 'M001', 'S01', 'ROADMAP');
309
+
310
+ assert.equal(regenerated, true, 'missing flat-phase ROADMAP is regenerated');
311
+ assert.match(readFileSync(roadmapPath, 'utf-8'), /Ship a layout-aware roadmap/);
312
+ assert.equal(existsSync(legacyRoadmapPath), false, 'regeneration does not create a legacy ROADMAP path');
313
+ assert.equal(normalizeRealPath(resolveMilestoneFile(base, 'M001', 'ROADMAP') ?? ''), normalizeRealPath(roadmapPath));
314
+ } finally {
315
+ closeDatabase();
316
+ rmSync(base, { recursive: true, force: true });
317
+ }
318
+ });
319
+
320
+ test('workflow-projections: regenerateIfMissing SUMMARY is idempotent for flat-phase task summaries', async () => {
321
+ const base = mkdtempSync(join(tmpdir(), 'gsd-projections-flat-summary-'));
322
+ const dbPath = join(base, '.gsd', 'gsd.db');
323
+ const phaseDir = join(base, '.gsd', 'phases', '01-milestone');
324
+ mkdirSync(phaseDir, { recursive: true });
325
+ openDatabase(dbPath);
326
+ clearParseCache();
327
+ clearPathCache();
328
+ _clearGsdRootCache();
329
+ invalidateStateCache();
330
+
331
+ try {
332
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
333
+ insertSlice({
334
+ id: 'S01',
335
+ milestoneId: 'M001',
336
+ title: 'Flat slice',
337
+ status: 'complete',
338
+ demo: 'Summary regenerates once.',
339
+ planning: { goal: 'Recover flat-phase summaries from DB.' },
340
+ });
341
+ insertTask({
342
+ id: 'T01',
343
+ sliceId: 'S01',
344
+ milestoneId: 'M001',
345
+ title: 'Completed task',
346
+ status: 'complete',
347
+ oneLiner: 'Completed the flat task.',
348
+ narrative: 'The task was completed through the DB-backed projection path.',
349
+ verificationResult: 'passed',
350
+ duration: '5m',
351
+ keyFiles: ['src/example.ts'],
352
+ keyDecisions: ['Use centralized task summary paths.'],
353
+ });
354
+
355
+ const summaryPath = join(phaseDir, 'T01-SUMMARY.md');
356
+ assert.ok(!existsSync(summaryPath), 'precondition: flat-phase task summary absent');
357
+
358
+ const first = await regenerateIfMissing(base, 'M001', 'S01', 'SUMMARY');
359
+ const second = await regenerateIfMissing(base, 'M001', 'S01', 'SUMMARY');
360
+
361
+ assert.equal(first, true, 'first call regenerates the missing task summary');
362
+ assert.equal(second, false, 'second call sees the flat-phase task summary and does not rewrite it');
363
+ assert.equal(normalizeRealPath(resolveTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY') ?? ''), normalizeRealPath(summaryPath));
364
+ assert.match(readFileSync(summaryPath, 'utf-8'), /# T01: Completed the flat task\./);
365
+ } finally {
366
+ closeDatabase();
367
+ rmSync(base, { recursive: true, force: true });
368
+ }
369
+ });
370
+
371
+ test('workflow-projections: flat-phase SUMMARY regeneration ignores stale nested task summaries', async () => {
372
+ const base = mkdtempSync(join(tmpdir(), 'gsd-projections-flat-stale-nested-'));
373
+ const dbPath = join(base, '.gsd', 'gsd.db');
374
+ const phaseDir = join(base, '.gsd', 'phases', '01-milestone');
375
+ const nestedTasksDir = join(phaseDir, 'tasks');
376
+ mkdirSync(nestedTasksDir, { recursive: true });
377
+ writeFileSync(join(nestedTasksDir, 'T01-SUMMARY.md'), '# stale nested summary\n');
378
+ openDatabase(dbPath);
379
+ clearParseCache();
380
+ clearPathCache();
381
+ _clearGsdRootCache();
382
+ invalidateStateCache();
383
+
384
+ try {
385
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
386
+ insertSlice({
387
+ id: 'S01',
388
+ milestoneId: 'M001',
389
+ title: 'Flat slice',
390
+ status: 'complete',
391
+ demo: 'Summary regenerates at phase root.',
392
+ planning: { goal: 'Recover canonical flat-phase summaries from DB.' },
393
+ });
394
+ insertTask({
395
+ id: 'T01',
396
+ sliceId: 'S01',
397
+ milestoneId: 'M001',
398
+ title: 'Completed task',
399
+ status: 'complete',
400
+ });
401
+
402
+ const rootSummaryPath = join(phaseDir, 'T01-SUMMARY.md');
403
+ assert.ok(!existsSync(rootSummaryPath), 'precondition: canonical flat-phase task summary absent');
404
+
405
+ const regenerated = await regenerateIfMissing(base, 'M001', 'S01', 'SUMMARY');
406
+
407
+ assert.equal(regenerated, true, 'missing phase-root summary is regenerated despite stale nested summary');
408
+ assert.equal(normalizeRealPath(resolveTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY') ?? ''), normalizeRealPath(rootSummaryPath));
409
+ assert.match(readFileSync(rootSummaryPath, 'utf-8'), /# T01: Completed task/);
410
+ assert.equal(readFileSync(join(nestedTasksDir, 'T01-SUMMARY.md'), 'utf-8'), '# stale nested summary\n');
411
+ } finally {
412
+ closeDatabase();
413
+ rmSync(base, { recursive: true, force: true });
414
+ }
415
+ });
416
+
417
+ test('workflow-projections: renderSummaryProjection uses milestone title when creating fresh flat-phase dirs', async () => {
418
+ const base = mkdtempSync(join(tmpdir(), 'gsd-projections-fresh-summary-'));
419
+ const dbPath = join(base, '.gsd', 'gsd.db');
420
+ mkdirSync(join(base, '.gsd'), { recursive: true });
421
+ openDatabase(dbPath);
422
+ clearParseCache();
423
+ clearPathCache();
424
+ _clearGsdRootCache();
425
+ invalidateStateCache();
426
+
427
+ try {
428
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
429
+ insertSlice({
430
+ id: 'S01',
431
+ milestoneId: 'M001',
432
+ title: 'Flat slice',
433
+ status: 'complete',
434
+ });
435
+ insertTask({
436
+ id: 'T01',
437
+ sliceId: 'S01',
438
+ milestoneId: 'M001',
439
+ title: 'Completed task',
440
+ status: 'complete',
441
+ });
442
+
443
+ const titleSummaryPath = join(base, '.gsd', 'phases', '01-milestone', 'T01-SUMMARY.md');
444
+ const idSummaryPath = join(base, '.gsd', 'phases', '01-m001', 'T01-SUMMARY.md');
445
+
446
+ renderSummaryProjection(base, 'M001', 'S01', 'T01');
447
+
448
+ assert.ok(existsSync(titleSummaryPath), 'fresh summary projection uses the milestone title slug');
449
+ assert.equal(existsSync(idSummaryPath), false, 'fresh summary projection does not create an id-slug orphan dir');
450
+ assert.equal(normalizeRealPath(resolveTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY') ?? ''), normalizeRealPath(titleSummaryPath));
451
+ } finally {
452
+ closeDatabase();
453
+ rmSync(base, { recursive: true, force: true });
454
+ }
455
+ });
456
+
246
457
  test('workflow-projections: renderStateProjection does not clobber non-empty STATE.md when manifest has milestones', async () => {
247
458
  const base = mkdtempSync(join(tmpdir(), 'gsd-projection-stale-'));
248
459
  const gsdDir = join(base, '.gsd');
@@ -10,7 +10,6 @@
10
10
  * for recovery, and invalidates caches.
11
11
  */
12
12
 
13
- import { join } from "node:path";
14
13
  import { existsSync } from "node:fs";
15
14
 
16
15
  import {
@@ -21,7 +20,7 @@ import {
21
20
  getLatestAssessmentByScope,
22
21
  updateMilestoneStatus,
23
22
  } from "../gsd-db.js";
24
- import { gsdProjectionRoot, clearPathCache, relMilestoneFile } from "../paths.js";
23
+ import { clearPathCache, resolveMilestoneFile, targetMilestoneFile } from "../paths.js";
25
24
  import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
26
25
  import { isClosedStatus, isDeferredStatus } from "../status-guards.js";
27
26
  import { saveFile, clearParseCache } from "../files.js";
@@ -210,9 +209,14 @@ export async function handleCompleteMilestone(
210
209
  // ── Filesystem operations (outside transaction) ─────────────────────────
211
210
  const summaryMd = renderMilestoneSummaryMarkdown(params, completedAt);
212
211
 
213
- // Layout-aware: flat-phase → phases/NN-slug/NN-SUMMARY.md;
214
- // legacy milestones/MID/MID-SUMMARY.md.
215
- const summaryPath = join(artifactBasePath, relMilestoneFile(artifactBasePath, params.milestoneId, "SUMMARY"));
212
+ const summaryPath =
213
+ resolveMilestoneFile(artifactBasePath, params.milestoneId, "SUMMARY") ??
214
+ targetMilestoneFile(
215
+ artifactBasePath,
216
+ params.milestoneId,
217
+ "SUMMARY",
218
+ getMilestone(params.milestoneId)?.title,
219
+ );
216
220
 
217
221
  // Guard (#4598): if SUMMARY.md already exists on disk, do not overwrite it.
218
222
  // This handles re-dispatch scenarios (DB/disk state divergence) where a prior
@@ -1,6 +1,6 @@
1
- import { join } from "node:path";
2
1
  import { existsSync, unlinkSync } from "node:fs";
3
- import { relSliceFile, relMilestoneFile } from "../paths.js";
2
+ import { join } from "node:path";
3
+ import { relSliceFile, resolveMilestoneFile, resolveMilestonePath, targetMilestoneFile } from "../paths.js";
4
4
  import { clearParseCache } from "../files.js";
5
5
  import { isClosedStatus } from "../status-guards.js";
6
6
  import { isNonEmptyString } from "../validation.js";
@@ -290,12 +290,23 @@ export async function handleReassessRoadmap(
290
290
  params.sliceChanges.removed.length > 0;
291
291
 
292
292
  if (hasStructuralChanges) {
293
- // Layout-aware: flat-phase phases/NN-slug/NN-VALIDATION.md; legacy → milestones/MID/MID-VALIDATION.md
294
- const validationFile = join(basePath, relMilestoneFile(basePath, params.milestoneId, "VALIDATION"));
295
- try {
296
- if (existsSync(validationFile)) unlinkSync(validationFile);
297
- } catch (e) {
298
- logWarning("tool", `validation file cleanup failed: ${(e as Error).message}`);
293
+ const milestoneDir = resolveMilestonePath(basePath, params.milestoneId);
294
+ const validationFiles = new Set([
295
+ resolveMilestoneFile(basePath, params.milestoneId, "VALIDATION"),
296
+ targetMilestoneFile(
297
+ basePath,
298
+ params.milestoneId,
299
+ "VALIDATION",
300
+ getMilestone(params.milestoneId)?.title,
301
+ ),
302
+ milestoneDir ? join(milestoneDir, `${params.milestoneId}-VALIDATION.md`) : null,
303
+ ].filter((file): file is string => Boolean(file)));
304
+ for (const validationFile of validationFiles) {
305
+ try {
306
+ if (existsSync(validationFile)) unlinkSync(validationFile);
307
+ } catch (e) {
308
+ logWarning("tool", `validation file cleanup failed: ${(e as Error).message}`);
309
+ }
299
310
  }
300
311
  }
301
312
 
@@ -12,15 +12,13 @@
12
12
  * despite passing validation.
13
13
  */
14
14
 
15
- import { join } from "node:path";
16
-
17
15
  import {
18
16
  transaction,
19
17
  insertAssessment,
20
18
  getMilestoneSlices,
21
19
  getMilestone,
22
20
  } from "../gsd-db.js";
23
- import { gsdProjectionRoot, clearPathCache, relMilestoneFile } from "../paths.js";
21
+ import { clearPathCache, targetMilestoneFile } from "../paths.js";
24
22
  import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
25
23
  import { resolveWorktreeProjectRoot } from "../worktree-root.js";
26
24
  import { saveFile, clearParseCache } from "../files.js";
@@ -160,11 +158,12 @@ export async function handleValidateMilestone(
160
158
  // worktree exists for this milestone, validation reads/writes the
161
159
  // worktree's artifacts instead of stale project-root state.
162
160
  const validationMd = renderValidationMarkdown(effectiveParams);
163
- // Layout-aware: flat-phase → phases/NN-slug/NN-VALIDATION.md;
164
- // legacy → milestones/MID/MID-VALIDATION.md.
165
- // join(artifactBasePath, relMilestoneFile) gives the absolute path; relMilestoneFile
166
- // returns ".gsd/..."-relative so joining with basePath (project/worktree root) is correct.
167
- const validationPath = join(artifactBasePath, relMilestoneFile(artifactBasePath, effectiveParams.milestoneId, "VALIDATION"));
161
+ const validationPath = targetMilestoneFile(
162
+ artifactBasePath,
163
+ effectiveParams.milestoneId,
164
+ "VALIDATION",
165
+ getMilestone(effectiveParams.milestoneId)?.title,
166
+ );
168
167
 
169
168
  // ── DB write first — matches complete-task/complete-slice pattern ───
170
169
  // Write DB before disk so a crash between the two leaves a recoverable
@@ -204,7 +203,12 @@ export async function handleValidateMilestone(
204
203
  const projectRoot = resolveWorktreeProjectRoot(basePath);
205
204
  if (projectRoot !== artifactBasePath) {
206
205
  // Mirror to project root using the project root's layout (same logic as above).
207
- const projectValidationPath = join(projectRoot, relMilestoneFile(projectRoot, effectiveParams.milestoneId, "VALIDATION"));
206
+ const projectValidationPath = targetMilestoneFile(
207
+ projectRoot,
208
+ effectiveParams.milestoneId,
209
+ "VALIDATION",
210
+ getMilestone(effectiveParams.milestoneId)?.title,
211
+ );
208
212
  try {
209
213
  await saveFile(projectValidationPath, validationMd);
210
214
  } catch (mirrorErr) {