@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
@@ -13,13 +13,12 @@ import { logWarning } from "./workflow-logger.js";
13
13
  import { isClosedStatus } from "./status-guards.js";
14
14
  import { dirname, join, relative } from "node:path";
15
15
  import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTasksBySliceIds, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, isDbAvailable, } from "./gsd-db.js";
16
- import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, gsdProjectionRoot, gsdRoot, buildMilestoneFileName, buildTaskFileName, } from "./paths.js";
16
+ import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, targetMilestoneFile, targetSliceFile, gsdProjectionRoot, gsdRoot, buildTaskFileName, } from "./paths.js";
17
17
  import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.js";
18
18
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
19
19
  import { invalidateStateCache } from "./state.js";
20
20
  import { clearPathCache, milestonesDir, legacyMilestonesDir, isLegacyMilestonesLayout, resolveMilestonePath, relSliceFile, canonicalPhaseDirName } from "./paths.js";
21
21
  import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "./compat/compat-marker.js";
22
- import { planFileName, milestoneIdToPhaseNum, sliceIdToPlanNum, } from "./layout-policy.js";
23
22
  // ─── Compat marker invalidation ───────────────────────────────────────────
24
23
  // Every successful projection write pushes its (basePath, projectionPath,
25
24
  // entities) here; invalidateCaches() drains it and refreshes .gsd/.compat.json
@@ -134,19 +133,6 @@ function sanitizeInlineRoadmapText(value) {
134
133
  .replace(/\s+/g, " ")
135
134
  .trim();
136
135
  }
137
- function resolveRoadmapProjectionPath(basePath, milestoneId) {
138
- const phaseNum = milestoneIdToPhaseNum(milestoneId);
139
- const existing = resolveMilestonePath(basePath, milestoneId);
140
- const legacyBase = legacyMilestonesDir(basePath);
141
- const isLegacyLayout = existing
142
- ? existing.startsWith(legacyBase + "/") || existing.startsWith(legacyBase + "\\")
143
- : isLegacyMilestonesLayout(basePath);
144
- const phaseDir = existing ?? join(isLegacyLayout ? legacyBase : milestonesDir(basePath), isLegacyLayout ? milestoneId : canonicalPhaseDirName(milestoneId, getMilestone(milestoneId)?.title));
145
- const roadmapFileName = isLegacyLayout
146
- ? `${milestoneId}-ROADMAP.md`
147
- : `${String(phaseNum).padStart(2, "0")}-ROADMAP.md`;
148
- return join(phaseDir, roadmapFileName);
149
- }
150
136
  function isMilestoneFlatPhaseLayout(basePath, milestoneId) {
151
137
  const existing = resolveMilestonePath(basePath, milestoneId);
152
138
  const legacyBase = legacyMilestonesDir(basePath);
@@ -492,7 +478,7 @@ export async function renderRoadmapFromDb(basePath, milestoneId) {
492
478
  // clear "write the ROADMAP" failure) instead of a misleading stub.
493
479
  if (slices.length === 0 && !milestone.vision.trim()) {
494
480
  logWarning("projection", `renderRoadmapFromDb skipped unplanned milestone ${milestoneId} (zero slices, empty vision) — refusing to write a stub ROADMAP`);
495
- const absPath = resolveRoadmapProjectionPath(basePath, milestoneId);
481
+ const absPath = targetMilestoneFile(basePath, milestoneId, "ROADMAP", milestone.title);
496
482
  if (existsSync(absPath)) {
497
483
  const artifactPath = toArtifactPath(absPath, basePath);
498
484
  unlinkSync(absPath);
@@ -506,7 +492,7 @@ export async function renderRoadmapFromDb(basePath, milestoneId) {
506
492
  }
507
493
  return { skipped: "unplanned-milestone" };
508
494
  }
509
- const absPath = resolveRoadmapProjectionPath(basePath, milestoneId);
495
+ const absPath = targetMilestoneFile(basePath, milestoneId, "ROADMAP", milestone.title);
510
496
  const artifactPath = toArtifactPath(absPath, basePath);
511
497
  const content = renderRoadmapMarkdown(milestone, slices);
512
498
  await writeAndStore(absPath, artifactPath, content, {
@@ -536,21 +522,15 @@ export async function renderMilestoneArtifactsFromDb(basePath, milestoneId) {
536
522
  const artifacts = getMilestoneScopedArtifacts(milestoneId);
537
523
  if (artifacts.length === 0)
538
524
  return false;
539
- const phaseDir = resolveMilestonePath(basePath, milestoneId) ??
540
- join(milestonesDir(basePath), canonicalPhaseDirName(milestoneId, getMilestone(milestoneId)?.title));
541
- mkdirSync(phaseDir, { recursive: true });
542
- const legacyBase = legacyMilestonesDir(basePath);
543
- const isLegacy = phaseDir.startsWith(legacyBase + "/") || phaseDir.startsWith(legacyBase + "\\");
525
+ const milestone = getMilestone(milestoneId);
544
526
  let wrote = false;
545
527
  for (const artifact of artifacts) {
546
528
  if (artifact.artifact_type === "ROADMAP")
547
529
  continue;
548
530
  if (!artifact.full_content.trim())
549
531
  continue;
550
- const fileName = isLegacy
551
- ? `${milestoneId}-${artifact.artifact_type}.md`
552
- : buildMilestoneFileName(milestoneId, artifact.artifact_type);
553
- const absPath = join(phaseDir, fileName);
532
+ const absPath = targetMilestoneFile(basePath, milestoneId, artifact.artifact_type, milestone?.title);
533
+ mkdirSync(dirname(absPath), { recursive: true });
554
534
  const artifactPath = toArtifactPath(absPath, basePath);
555
535
  await writeAndStore(absPath, artifactPath, artifact.full_content, {
556
536
  artifact_type: artifact.artifact_type,
@@ -664,20 +644,11 @@ export async function renderSliceSummary(basePath, milestoneId, sliceId) {
664
644
  if (!slice) {
665
645
  return false; // No slice data — skip silently
666
646
  }
667
- // Flat-phase: resolve or create the phase dir, since resolveSlicePath only
668
- // finds existing dirs. In flat-phase the slice "path" is the phase dir.
669
- let slicePath = resolveSlicePath(basePath, milestoneId, sliceId);
670
- if (!slicePath) {
671
- const mDir = milestonesDir(basePath);
672
- const dirName = canonicalPhaseDirName(milestoneId, getMilestone(milestoneId)?.title);
673
- slicePath = join(mDir, dirName);
674
- mkdirSync(slicePath, { recursive: true });
675
- }
647
+ const milestoneTitle = getMilestone(milestoneId)?.title;
676
648
  let wrote = false;
677
- // Write SUMMARY — flat-phase: NN-MM-SUMMARY.md inside phase dir
678
649
  if (slice.full_summary_md) {
679
- const summaryName = planFileName(milestoneIdToPhaseNum(milestoneId), sliceIdToPlanNum(sliceId), "SUMMARY");
680
- const summaryAbs = join(slicePath, summaryName);
650
+ const summaryAbs = targetSliceFile(basePath, milestoneId, sliceId, "SUMMARY", milestoneTitle);
651
+ mkdirSync(dirname(summaryAbs), { recursive: true });
681
652
  const summaryArtifact = toArtifactPath(summaryAbs, basePath);
682
653
  await writeAndStore(summaryAbs, summaryArtifact, slice.full_summary_md, {
683
654
  artifact_type: "SUMMARY",
@@ -686,10 +657,9 @@ export async function renderSliceSummary(basePath, milestoneId, sliceId) {
686
657
  }, basePath);
687
658
  wrote = true;
688
659
  }
689
- // Write UAT — flat-phase: NN-MM-UAT.md inside phase dir
690
660
  if (slice.full_uat_md) {
691
- const uatName = planFileName(milestoneIdToPhaseNum(milestoneId), sliceIdToPlanNum(sliceId), "UAT");
692
- const uatAbs = join(slicePath, uatName);
661
+ const uatAbs = targetSliceFile(basePath, milestoneId, sliceId, "UAT", milestoneTitle);
662
+ mkdirSync(dirname(uatAbs), { recursive: true });
693
663
  const uatArtifact = toArtifactPath(uatAbs, basePath);
694
664
  await writeAndStore(uatAbs, uatArtifact, slice.full_uat_md, {
695
665
  artifact_type: "UAT",
@@ -874,7 +844,7 @@ function detectStaleRendersImpl(basePath) {
874
844
  // TODO(flat-phase): roadmap checkbox parsing may not match flat-phase
875
845
  // roadmap format, causing false-positive drift loops. Skip during transition.
876
846
  /*
877
- const roadmapPath = resolveRoadmapProjectionPath(basePath, milestone.id);
847
+ const roadmapPath = targetMilestoneFile(basePath, milestone.id, "ROADMAP", milestone.title);
878
848
  if (existsSync(roadmapPath)) {
879
849
  try {
880
850
  const parsed = parseProjectionByIdentity(roadmapPath, parseRoadmap) as ReturnType<typeof parseRoadmap>;
@@ -961,34 +931,27 @@ function detectStaleRendersImpl(basePath) {
961
931
  }
962
932
  }
963
933
  }
964
- // Check missing slice summary/UAT files
934
+ // Check missing slice summary/UAT files. Use the same target helper as
935
+ // renderSliceSummary so detection and repair agree on flat-phase vs legacy
936
+ // output locations.
965
937
  const sliceRow = getSlice(milestone.id, slice.id);
966
938
  if (sliceRow && sliceRow.status === "complete") {
967
- // Use the SAME path construction as renderSliceSummary (planFileName format)
968
- // so the detector and repair always agree on the file location.
969
- const slicePath = resolveSlicePath(basePath, milestone.id, slice.id);
970
- if (slicePath) {
971
- const phaseNum = milestoneIdToPhaseNum(milestone.id);
972
- const planNum = sliceIdToPlanNum(slice.id);
973
- if (sliceRow.full_summary_md) {
974
- const summaryName = planFileName(phaseNum, planNum, "SUMMARY");
975
- const summaryAbsPath = join(slicePath, summaryName);
976
- if (!existsSync(summaryAbsPath)) {
977
- stale.push({
978
- path: summaryAbsPath,
979
- reason: `${slice.id} is complete with summary in DB but SUMMARY.md missing on disk`,
980
- });
981
- }
939
+ if (sliceRow.full_summary_md) {
940
+ const summaryAbsPath = targetSliceFile(basePath, milestone.id, slice.id, "SUMMARY", milestone.title);
941
+ if (!existsSync(summaryAbsPath)) {
942
+ stale.push({
943
+ path: summaryAbsPath,
944
+ reason: `${slice.id} is complete with summary in DB but SUMMARY.md missing on disk`,
945
+ });
982
946
  }
983
- if (sliceRow.full_uat_md) {
984
- const uatName = planFileName(phaseNum, planNum, "UAT");
985
- const uatAbsPath = join(slicePath, uatName);
986
- if (!existsSync(uatAbsPath)) {
987
- stale.push({
988
- path: uatAbsPath,
989
- reason: `${slice.id} is complete with UAT in DB but UAT.md missing on disk`,
990
- });
991
- }
947
+ }
948
+ if (sliceRow.full_uat_md) {
949
+ const uatAbsPath = targetSliceFile(basePath, milestone.id, slice.id, "UAT", milestone.title);
950
+ if (!existsSync(uatAbsPath)) {
951
+ stale.push({
952
+ path: uatAbsPath,
953
+ reason: `${slice.id} is complete with UAT in DB but UAT.md missing on disk`,
954
+ });
992
955
  }
993
956
  }
994
957
  }
@@ -1013,12 +976,8 @@ export function roadmapRenderMarksSliceDone(roadmapContent, sliceId) {
1013
976
  return parseRoadmap(roadmapContent).slices.some((slice) => slice.id === sliceId && slice.done);
1014
977
  }
1015
978
  export async function renderReplanFromDb(basePath, milestoneId, sliceId, replanData) {
1016
- // Flat-phase: replan file lives in the phase dir
1017
- const slicePath = resolveSlicePath(basePath, milestoneId, sliceId)
1018
- ?? join(milestonesDir(basePath), canonicalPhaseDirName(milestoneId, getMilestone(milestoneId)?.title));
1019
- const phaseNum = milestoneIdToPhaseNum(milestoneId);
1020
- const planNum = sliceIdToPlanNum(sliceId);
1021
- const absPath = join(slicePath, planFileName(phaseNum, planNum, "REPLAN"));
979
+ const absPath = targetSliceFile(basePath, milestoneId, sliceId, "REPLAN", getMilestone(milestoneId)?.title);
980
+ mkdirSync(dirname(absPath), { recursive: true });
1022
981
  const artifactPath = toArtifactPath(absPath, basePath);
1023
982
  const lines = [];
1024
983
  lines.push(`# ${sliceId} Replan`);
@@ -1045,24 +1004,8 @@ export async function renderReplanFromDb(basePath, milestoneId, sliceId, replanD
1045
1004
  return { replanPath: absPath, content };
1046
1005
  }
1047
1006
  export async function renderAssessmentFromDb(basePath, milestoneId, sliceId, assessmentData) {
1048
- // Flat-phase: the assessment file lives in the phase dir, NOT in a slices/SID/
1049
- // subdir. resolveSlicePath() detects a slices/SID/ dir unconditionally, so a
1050
- // stray slices/SID/ created by an earlier planning step would send this write
1051
- // to a hybrid path (wrong dir, flat filename) that verification cannot find.
1052
- // Guard with the same legacy-base check relSlicePath() uses so flat-phase
1053
- // milestones always target the phase dir.
1054
- const mDir = resolveMilestonePath(basePath, milestoneId);
1055
- const legacyBase = legacyMilestonesDir(basePath);
1056
- const isLegacyLayout = mDir
1057
- ? mDir.startsWith(legacyBase + "/") || mDir.startsWith(legacyBase + "\\")
1058
- : false;
1059
- const fallbackDir = join(milestonesDir(basePath), canonicalPhaseDirName(milestoneId, getMilestone(milestoneId)?.title));
1060
- const slicePath = isLegacyLayout
1061
- ? (resolveSlicePath(basePath, milestoneId, sliceId) ?? fallbackDir)
1062
- : (mDir ?? fallbackDir);
1063
- const phaseNum = milestoneIdToPhaseNum(milestoneId);
1064
- const planNum = sliceIdToPlanNum(sliceId);
1065
- const absPath = join(slicePath, planFileName(phaseNum, planNum, "ASSESSMENT"));
1007
+ const absPath = targetSliceFile(basePath, milestoneId, sliceId, "ASSESSMENT", getMilestone(milestoneId)?.title);
1008
+ mkdirSync(dirname(absPath), { recursive: true });
1066
1009
  const artifactPath = toArtifactPath(absPath, basePath);
1067
1010
  const lines = [];
1068
1011
  lines.push(`# ${sliceId} Assessment`);
@@ -172,7 +172,7 @@ export function snapshotUnitMetrics(ctx, unitType, unitId, startedAt, model, opt
172
172
  ledger.units.push(unit);
173
173
  }
174
174
  saveLedger(basePath, ledger);
175
- if (isUnifiedAuditEnabled()) {
175
+ if (isUnifiedAuditEnabled(basePath)) {
176
176
  emitUokAuditEvent(basePath, buildAuditEnvelope({
177
177
  traceId: opts?.traceId ?? `metrics:${unitType}:${unitId}`,
178
178
  turnId: opts?.turnId,
@@ -324,7 +324,7 @@ export function snapshotUnitMetricsByScope(scope, ctx, unitType, unitId, started
324
324
  scopedLedger.units.push(unit);
325
325
  }
326
326
  saveLedger(base, scopedLedger);
327
- if (isUnifiedAuditEnabled()) {
327
+ if (isUnifiedAuditEnabled(base)) {
328
328
  emitUokAuditEvent(base, buildAuditEnvelope({
329
329
  traceId: opts?.traceId ?? `metrics:${unitType}:${unitId}`,
330
330
  turnId: opts?.turnId,
@@ -10,7 +10,7 @@
10
10
  * via prefix matching, so existing projects work without migration.
11
11
  */
12
12
  import { readdirSync, existsSync, realpathSync, statSync, Dirent } from "node:fs";
13
- import { join, dirname, normalize, resolve } from "node:path";
13
+ import { join, dirname, normalize, relative, resolve } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { spawnSync } from "node:child_process";
16
16
  import { nativeScanGsdTree } from "./native-parser-bridge.js";
@@ -331,21 +331,26 @@ export function resolveGsdPathContract(workRoot, originalProjectRoot) {
331
331
  const resolvedWorkRoot = resolve(workRoot || process.cwd());
332
332
  const isWorktree = isGsdWorktreePath(resolvedWorkRoot);
333
333
  if (isWorktree && !originalProjectRoot?.trim()) {
334
- const projectGsd = resolveExternalStateProjectGsdFromWorktreePath(resolvedWorkRoot);
335
- if (projectGsd) {
334
+ const rawProjectGsd = resolveExternalStateProjectGsdFromWorktreePath(resolvedWorkRoot);
335
+ if (rawProjectGsd) {
336
+ // Canonicalize the `.gsd` root so the DB path matches what every other
337
+ // accessor (gsdRoot/gsdProjectionRoot) returns — otherwise an unresolved
338
+ // symlink (e.g. `/mnt/c/.../.gsd` → native ext4 on WSL) selects the wrong
339
+ // journal mode and yields a fragile, move-prone handle. See issue #1239.
340
+ const projectGsd = normalizeRealPath(rawProjectGsd);
336
341
  return {
337
342
  projectRoot: dirname(dirname(projectGsd)),
338
343
  workRoot: resolvedWorkRoot,
339
344
  projectGsd,
340
- worktreeGsd: join(resolvedWorkRoot, ".gsd"),
345
+ worktreeGsd: normalizeRealPath(join(resolvedWorkRoot, ".gsd")),
341
346
  projectDb: join(projectGsd, "gsd.db"),
342
347
  isWorktree,
343
348
  };
344
349
  }
345
350
  }
346
351
  const projectRoot = resolve(resolveWorktreeProjectRoot(resolvedWorkRoot, originalProjectRoot));
347
- const projectGsd = join(projectRoot, ".gsd");
348
- const worktreeGsd = isWorktree ? join(resolvedWorkRoot, ".gsd") : null;
352
+ const projectGsd = normalizeRealPath(join(projectRoot, ".gsd"));
353
+ const worktreeGsd = isWorktree ? normalizeRealPath(join(resolvedWorkRoot, ".gsd")) : null;
349
354
  return {
350
355
  projectRoot,
351
356
  workRoot: resolvedWorkRoot,
@@ -879,11 +884,22 @@ export function resolveTasksDir(basePath, milestoneId, sliceId) {
879
884
  * Resolve a specific task file.
880
885
  */
881
886
  export function resolveTaskFile(basePath, milestoneId, sliceId, taskId, suffix) {
882
- const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
883
- if (!tDir)
887
+ const phaseDir = resolveMilestonePath(basePath, milestoneId);
888
+ if (!phaseDir)
884
889
  return null;
885
- const file = resolveFile(tDir, taskId, suffix);
886
- return file ? join(tDir, file) : null;
890
+ const legacyBase = legacyMilestonesDir(basePath);
891
+ const isLegacy = phaseDir.startsWith(legacyBase + "/") || phaseDir.startsWith(legacyBase + "\\");
892
+ if (suffix !== "PLAN" && !isLegacy) {
893
+ const flatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
894
+ return existsSync(flatPath) ? flatPath : null;
895
+ }
896
+ const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
897
+ if (tDir) {
898
+ const file = resolveFile(tDir, taskId, suffix);
899
+ if (file)
900
+ return join(tDir, file);
901
+ }
902
+ return null;
887
903
  }
888
904
  // ─── Relative Path Builders (for prompts — .gsd/milestones/...) ────────────
889
905
  /**
@@ -915,29 +931,30 @@ export function relMilestonePath(basePath, milestoneId, title) {
915
931
  return `.gsd/${LAYOUT_SEGMENTS.level1}/${canonicalPhaseDirName(milestoneId, title)}`;
916
932
  }
917
933
  /**
918
- * Build relative .gsd/ path to a milestone file.
919
- * Layout-aware: uses resolveMilestoneFile to find NN-SUFFIX.md (flat-phase)
920
- * or M001-SUFFIX.md (legacy). Falls back to a layout-appropriate canonical
921
- * name when the file doesn't exist yet.
922
- */
923
- export function relMilestoneFile(basePath, milestoneId, suffix) {
924
- const mRel = relMilestonePath(basePath, milestoneId);
925
- // resolveMilestoneFile checks NN-SUFFIX.md (flat-phase) and MID-SUFFIX.md (legacy).
926
- const absFile = resolveMilestoneFile(basePath, milestoneId, suffix);
927
- if (absFile) {
928
- const fileName = absFile.split(/[/\\]/).pop();
929
- return `${mRel}/${fileName}`;
930
- }
931
- // File doesn't exist yet — use layout-aware fallback filename.
934
+ * Build the canonical absolute write target for a milestone file.
935
+ * Existing compatibility filenames are intentionally ignored; readers that need
936
+ * to preserve an existing file should call resolveMilestoneFile first.
937
+ */
938
+ export function targetMilestoneFile(basePath, milestoneId, suffix, title) {
932
939
  const mDir = resolveMilestonePath(basePath, milestoneId);
933
940
  const legacyBase = legacyMilestonesDir(basePath);
934
941
  const isLegacy = mDir
935
942
  ? mDir.startsWith(legacyBase + "/") || mDir.startsWith(legacyBase + "\\")
936
- : false;
943
+ : isLegacyMilestonesLayout(basePath);
944
+ const dir = mDir ?? join(isLegacy ? legacyBase : milestonesDir(basePath), isLegacy ? milestoneId : canonicalPhaseDirName(milestoneId, title));
937
945
  const fileName = isLegacy
938
946
  ? `${milestoneId}-${suffix}.md`
939
947
  : `${String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0")}-${suffix}.md`;
940
- return `${mRel}/${fileName}`;
948
+ return join(dir, fileName);
949
+ }
950
+ /**
951
+ * Build relative .gsd/ path to a milestone file.
952
+ * Preserves an existing compatibility filename when present; otherwise falls
953
+ * back to the canonical write target.
954
+ */
955
+ export function relMilestoneFile(basePath, milestoneId, suffix, title) {
956
+ const rel = relative(gsdProjectionRoot(basePath), resolveMilestoneFile(basePath, milestoneId, suffix) ?? targetMilestoneFile(basePath, milestoneId, suffix, title)).replace(/\\/g, "/");
957
+ return `.gsd/${rel}`;
941
958
  }
942
959
  /**
943
960
  * Build relative .gsd/ path to a slice directory.
@@ -965,32 +982,36 @@ export function relSlicePath(basePath, milestoneId, sliceId, milestoneTitle) {
965
982
  return relMilestonePath(basePath, milestoneId, milestoneTitle);
966
983
  }
967
984
  /**
968
- * Build relative .gsd/ path to a slice file.
969
- * Layout-aware: legacy uses S01-SUFFIX.md; flat-phase uses NN-MM-SUFFIX.md.
970
- *
971
- * @param milestoneTitle - Optional milestone title forwarded to relSlicePath /
972
- * relMilestonePath for title-aware flat-phase fallback dir naming. Only used
973
- * when the phase directory does not yet exist on disk.
985
+ * Build the canonical absolute write target for a slice file.
986
+ * Existing compatibility filenames are intentionally ignored; readers that need
987
+ * to preserve an existing file should call resolveSliceFile first.
974
988
  */
975
- export function relSliceFile(basePath, milestoneId, sliceId, suffix, milestoneTitle) {
976
- const sRel = relSlicePath(basePath, milestoneId, sliceId, milestoneTitle);
977
- const absPath = resolveSliceFile(basePath, milestoneId, sliceId, suffix);
978
- if (absPath) {
979
- const fileName = absPath.split(/[/\\]/).pop();
980
- return `${sRel}/${fileName}`;
981
- }
982
- // Fallback when file doesn't exist yet — use layout-aware filename.
989
+ export function targetSliceFile(basePath, milestoneId, sliceId, suffix, milestoneTitle) {
983
990
  const mDir = resolveMilestonePath(basePath, milestoneId);
984
991
  const legacyBase = legacyMilestonesDir(basePath);
985
992
  const isLegacy = mDir
986
993
  ? mDir.startsWith(legacyBase + "/") || mDir.startsWith(legacyBase + "\\")
987
- : false;
988
- if (isLegacy) {
989
- return `${sRel}/${sliceId}-${suffix}.md`;
990
- }
991
- const phaseNum = milestoneIdToPhaseNum(milestoneId);
992
- const planNum = sliceIdToPlanNum(sliceId);
993
- return `${sRel}/${planFileName(phaseNum, planNum, suffix)}`;
994
+ : isLegacyMilestonesLayout(basePath);
995
+ const milestoneDir = mDir
996
+ ?? dirname(targetMilestoneFile(basePath, milestoneId, "ROADMAP", milestoneTitle));
997
+ const slicesDir = join(milestoneDir, "slices");
998
+ const dir = isLegacy
999
+ ? join(slicesDir, resolveDir(slicesDir, sliceId) ?? sliceId)
1000
+ : milestoneDir;
1001
+ const fileName = isLegacy
1002
+ ? `${sliceId}-${suffix}.md`
1003
+ : planFileName(milestoneIdToPhaseNum(milestoneId), sliceIdToPlanNum(sliceId), suffix);
1004
+ return join(dir, fileName);
1005
+ }
1006
+ /**
1007
+ * Build relative .gsd/ path to a slice file.
1008
+ * Preserves an existing compatibility filename when present; otherwise falls
1009
+ * back to the canonical write target.
1010
+ */
1011
+ export function relSliceFile(basePath, milestoneId, sliceId, suffix, milestoneTitle) {
1012
+ const rel = relative(gsdProjectionRoot(basePath), resolveSliceFile(basePath, milestoneId, sliceId, suffix)
1013
+ ?? targetSliceFile(basePath, milestoneId, sliceId, suffix, milestoneTitle)).replace(/\\/g, "/");
1014
+ return `.gsd/${rel}`;
994
1015
  }
995
1016
  /**
996
1017
  * Build relative .gsd/ path to a task file.
@@ -999,7 +1020,7 @@ export function relSliceFile(basePath, milestoneId, sliceId, suffix, milestoneTi
999
1020
  * Flat-phase: PLAN → slice plan path (tasks as checkboxes); other suffixes
1000
1021
  * (e.g. SUMMARY) → phase dir / TID-SUFFIX.md
1001
1022
  */
1002
- export function relTaskFile(basePath, milestoneId, sliceId, taskId, suffix) {
1023
+ export function relTaskFile(basePath, milestoneId, sliceId, taskId, suffix, milestoneTitle) {
1003
1024
  const sDir = resolveSlicePath(basePath, milestoneId, sliceId);
1004
1025
  const phaseDir = resolveMilestonePath(basePath, milestoneId);
1005
1026
  // Legacy: slice path is a slices/SID/ subdir inside the milestone dir
@@ -1011,6 +1032,6 @@ export function relTaskFile(basePath, milestoneId, sliceId, taskId, suffix) {
1011
1032
  if (suffix === "PLAN") {
1012
1033
  return relSliceFile(basePath, milestoneId, sliceId, "PLAN");
1013
1034
  }
1014
- const relS = relSlicePath(basePath, milestoneId, sliceId);
1035
+ const relS = relSlicePath(basePath, milestoneId, sliceId, milestoneTitle);
1015
1036
  return `${relS}/${buildTaskFileName(taskId, suffix)}`;
1016
1037
  }
@@ -42,6 +42,33 @@ export function persistHookState(basePath) {
42
42
  export function restoreHookState(basePath) {
43
43
  getOrCreateRegistry().restoreState(basePath);
44
44
  }
45
+ /**
46
+ * Reconcile a restored `activeHook` against the session's sidecar queue.
47
+ *
48
+ * The registry persists `activeHook` to disk but the pending hook *dispatch*
49
+ * lives only on the non-persisted `s.sidecarQueue`. After a pause/resume or
50
+ * crash-recovery, `restoreHookState` re-hydrates `activeHook` while the dispatch
51
+ * is gone, so the registry believes a hook is in-flight but nothing will ever
52
+ * complete it — and the next unrelated unit's close-out is falsely charged
53
+ * against the stale hook and blocked. Re-enqueue the missing dispatch so the
54
+ * hook actually runs, restoring the invariant that a persisted `activeHook`
55
+ * always has a live dispatch (#1246).
56
+ */
57
+ export function reconcileRestoredHookDispatch(basePath, sidecarQueue) {
58
+ const dispatch = getOrCreateRegistry().getPendingHookDispatch(basePath);
59
+ if (!dispatch)
60
+ return;
61
+ const alreadyQueued = sidecarQueue.some(item => item.kind === "hook" && item.unitType === dispatch.unitType);
62
+ if (alreadyQueued)
63
+ return;
64
+ sidecarQueue.push({
65
+ kind: "hook",
66
+ unitType: dispatch.unitType,
67
+ unitId: dispatch.unitId,
68
+ prompt: dispatch.prompt,
69
+ model: dispatch.model,
70
+ });
71
+ }
45
72
  export function clearPersistedHookState(basePath) {
46
73
  getOrCreateRegistry().clearPersistedState(basePath);
47
74
  }
@@ -66,6 +66,39 @@ function resolveWinningPhase(models, chain) {
66
66
  }
67
67
  return undefined;
68
68
  }
69
+ /**
70
+ * Normalize a single model field into a {@link ResolvedModelConfig}.
71
+ *
72
+ * Accepts either the legacy bare-string form or the extended object form
73
+ * (`{ model, provider?, fallbacks? }`). This is the shared normalization used
74
+ * for phase buckets so sibling single-model fields — `post_unit_hooks[].model`,
75
+ * `auto_supervisor.model`, and `reactive_execution.subagent_model` — honor
76
+ * `fallbacks[]` identically rather than accepting a bare string only (#1229).
77
+ *
78
+ * Returns undefined for an unset, blank, or model-less field.
79
+ */
80
+ export function normalizeModelFieldConfig(field) {
81
+ if (!field)
82
+ return undefined;
83
+ if (typeof field === "string") {
84
+ return field.trim() ? { primary: field, fallbacks: [] } : undefined;
85
+ }
86
+ if (!field.model)
87
+ return undefined;
88
+ // When provider is explicitly set, prepend it to the model ID so the
89
+ // resolution code in auto.ts can do an explicit provider match.
90
+ const primary = field.provider && !field.model.includes("/")
91
+ ? `${field.provider}/${field.model}`
92
+ : field.model;
93
+ // Carry the per-field `thinking` sub-field through so the dispatch site can
94
+ // prefer it over the session/floor level, mirroring phase-inline thinking
95
+ // (#1269). `undefined` is preserved as absent so behavior is unchanged.
96
+ return {
97
+ primary,
98
+ fallbacks: field.fallbacks ?? [],
99
+ ...(field.thinking ? { thinking: field.thinking } : {}),
100
+ };
101
+ }
69
102
  /**
70
103
  * Resolve model and fallbacks for a given auto-mode unit type.
71
104
  * Returns the primary model and ordered fallbacks, or undefined if not configured.
@@ -73,6 +106,15 @@ function resolveWinningPhase(models, chain) {
73
106
  * Supports both legacy string format and extended object format:
74
107
  * - Legacy: `planning: claude-opus-4-6`
75
108
  * - Extended: `planning: { model: claude-opus-4-6, fallbacks: [glm-5, minimax-m2.5] }`
109
+ *
110
+ * Hook unit types (`hook/<name>`) resolve against the matching
111
+ * `post_unit_hooks[]` entry's `model` field so hook sessions honor the same
112
+ * `fallbacks[]` chain as phase buckets — the recovery path can then switch to
113
+ * a fallback provider instead of hard-failing (and potentially pausing) the
114
+ * run on a transient provider trip (#1229).
115
+ *
116
+ * The synthetic `supervisor` unit type resolves against `auto_supervisor.model`
117
+ * for supervisor interventions (wrap-up warnings, timeout recovery).
76
118
  */
77
119
  export function resolveModelWithFallbacksForUnit(unitType, basePath, availableModelIds, preferredModelId, skipProfileDefaults = false) {
78
120
  const loadOpts = {
@@ -81,26 +123,25 @@ export function resolveModelWithFallbacksForUnit(unitType, basePath, availableMo
81
123
  ...(skipProfileDefaults ? { skipProfileDefaults: true } : {}),
82
124
  };
83
125
  const prefs = loadEffectiveGSDPreferences(basePath, loadOpts);
126
+ // Supervisor interventions resolve against `auto_supervisor.model` (#1229).
127
+ if (unitType === "supervisor") {
128
+ return normalizeModelFieldConfig(prefs?.preferences?.auto_supervisor?.model);
129
+ }
130
+ // Hook sessions resolve against their own `post_unit_hooks[].model` field,
131
+ // which now shares the phase-bucket object form (#1229).
132
+ if (unitType.startsWith("hook/")) {
133
+ const hookName = unitType.slice("hook/".length);
134
+ const hooks = prefs?.preferences?.post_unit_hooks;
135
+ const hook = Array.isArray(hooks) ? hooks.find((h) => h.name === hookName) : undefined;
136
+ return normalizeModelFieldConfig(hook?.model);
137
+ }
84
138
  const chain = phaseChainForUnit(unitType);
85
139
  if (!chain)
86
140
  return undefined;
87
141
  const winner = resolveWinningPhase(prefs?.preferences?.models, chain);
88
142
  if (!winner)
89
143
  return undefined;
90
- const phaseConfig = winner.config;
91
- // Normalize: string -> { model, fallbacks: [] }
92
- if (typeof phaseConfig === "string") {
93
- return { primary: phaseConfig, fallbacks: [] };
94
- }
95
- // When provider is explicitly set, prepend it to the model ID so the
96
- // resolution code in auto.ts can do an explicit provider match.
97
- const primary = phaseConfig.provider && !phaseConfig.model.includes("/")
98
- ? `${phaseConfig.provider}/${phaseConfig.model}`
99
- : phaseConfig.model;
100
- return {
101
- primary,
102
- fallbacks: phaseConfig.fallbacks ?? [],
103
- };
144
+ return normalizeModelFieldConfig(winner.config);
104
145
  }
105
146
  /**
106
147
  * Resolve the explicitly configured reasoning effort for a unit type (ADR-026).
@@ -125,6 +166,13 @@ export function resolveModelWithFallbacksForUnit(unitType, basePath, availableMo
125
166
  * applied here.
126
167
  */
127
168
  export function resolveThinkingLevelForUnit(unitType, basePath, availableModelIds) {
169
+ // Single-field configs — `auto_supervisor.model` and `post_unit_hooks[].model`
170
+ // — carry their own inline `thinking` on the object model form and have no
171
+ // phase-bucket chain, so the walk below never covers them. Resolve their
172
+ // per-field level directly (#1269).
173
+ if (unitType === "supervisor" || unitType.startsWith("hook/")) {
174
+ return resolveModelWithFallbacksForUnit(unitType, basePath, availableModelIds)?.thinking;
175
+ }
128
176
  const loadOpts = availableModelIds !== undefined ? { availableModelIds } : undefined;
129
177
  const prefs = loadEffectiveGSDPreferences(basePath, loadOpts)?.preferences;
130
178
  if (!prefs)
@@ -383,12 +431,15 @@ export function resolveDynamicRoutingConfig() {
383
431
  export function resolveAutoSupervisorConfig() {
384
432
  const prefs = loadEffectiveGSDPreferences();
385
433
  const configured = prefs?.preferences.auto_supervisor ?? {};
434
+ const modelConfig = normalizeModelFieldConfig(configured.model);
386
435
  return {
387
436
  soft_timeout_minutes: configured.soft_timeout_minutes ?? 20,
388
437
  idle_timeout_minutes: configured.idle_timeout_minutes ?? 10,
389
438
  hard_timeout_minutes: configured.hard_timeout_minutes ?? 30,
390
439
  stalled_tool_timeout_minutes: configured.stalled_tool_timeout_minutes ?? 5,
391
- ...(configured.model ? { model: configured.model } : {}),
440
+ ...(modelConfig
441
+ ? { model: modelConfig.primary, modelFallbacks: modelConfig.fallbacks }
442
+ : {}),
392
443
  };
393
444
  }
394
445
  // ─── Token Profile Resolution ─────────────────────────────────────────────