@opengsd/gsd-pi 1.1.1-dev.a5a2de8 → 1.1.1-dev.b2556262

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 (325) hide show
  1. package/dist/headless-recover.js +56 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  4. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  5. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  6. package/dist/resources/extensions/browser-tools/index.js +68 -24
  7. package/dist/resources/extensions/browser-tools/state.js +12 -0
  8. package/dist/resources/extensions/browser-tools/tools/session.js +3 -2
  9. package/dist/resources/extensions/browser-tools/utils.js +3 -3
  10. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  11. package/dist/resources/extensions/gsd/auto/loop.js +4 -2
  12. package/dist/resources/extensions/gsd/auto/phases.js +87 -12
  13. package/dist/resources/extensions/gsd/auto/session.js +22 -1
  14. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +1 -0
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +81 -13
  16. package/dist/resources/extensions/gsd/auto-model-selection.js +154 -9
  17. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -2
  18. package/dist/resources/extensions/gsd/auto-prompts.js +26 -21
  19. package/dist/resources/extensions/gsd/auto-recovery.js +4 -2
  20. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  21. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  22. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  23. package/dist/resources/extensions/gsd/auto.js +40 -15
  24. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +192 -77
  26. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  27. package/dist/resources/extensions/gsd/closeout-wizard.js +32 -9
  28. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  29. package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -9
  30. package/dist/resources/extensions/gsd/commands-maintenance.js +93 -15
  31. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  32. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +2 -2
  33. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  34. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  35. package/dist/resources/extensions/gsd/db-writer.js +35 -0
  36. package/dist/resources/extensions/gsd/docs/preferences-reference.md +50 -1
  37. package/dist/resources/extensions/gsd/gsd-db.js +480 -172
  38. package/dist/resources/extensions/gsd/guided-flow.js +4 -1
  39. package/dist/resources/extensions/gsd/markdown-renderer.js +37 -53
  40. package/dist/resources/extensions/gsd/md-importer.js +38 -3
  41. package/dist/resources/extensions/gsd/migration-auto-check.js +126 -31
  42. package/dist/resources/extensions/gsd/parsers-legacy.js +23 -0
  43. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  44. package/dist/resources/extensions/gsd/planning-path-scope.js +22 -4
  45. package/dist/resources/extensions/gsd/pre-execution-checks.js +10 -2
  46. package/dist/resources/extensions/gsd/preferences-models.js +111 -43
  47. package/dist/resources/extensions/gsd/preferences-types.js +13 -0
  48. package/dist/resources/extensions/gsd/preferences-validation.js +68 -3
  49. package/dist/resources/extensions/gsd/preferences.js +4 -1
  50. package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
  51. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  52. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  54. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
  55. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  56. package/dist/resources/extensions/gsd/roadmap-slices.js +5 -1
  57. package/dist/resources/extensions/gsd/safety/content-validator.js +6 -4
  58. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  59. package/dist/resources/extensions/gsd/source-observations.js +306 -0
  60. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +15 -8
  61. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +33 -5
  62. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +34 -13
  63. package/dist/resources/extensions/gsd/state-reconciliation/index.js +39 -14
  64. package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +4 -4
  65. package/dist/resources/extensions/gsd/state.js +7 -3
  66. package/dist/resources/extensions/gsd/tool-contract.js +15 -1
  67. package/dist/resources/extensions/gsd/tool-presentation-plan.js +24 -2
  68. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -0
  69. package/dist/resources/extensions/gsd/tools/plan-slice.js +42 -11
  70. package/dist/resources/extensions/gsd/tools/plan-task.js +7 -1
  71. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +62 -406
  72. package/dist/resources/extensions/gsd/uat-policy.js +130 -0
  73. package/dist/resources/extensions/gsd/uat-run.js +414 -0
  74. package/dist/resources/extensions/gsd/unit-context-manifest.js +3 -4
  75. package/dist/resources/extensions/gsd/unit-tool-contracts.js +38 -14
  76. package/dist/resources/extensions/gsd/verdict-parser.js +3 -8
  77. package/dist/resources/extensions/gsd/workflow-manifest.js +132 -5
  78. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -3
  79. package/dist/resources/extensions/gsd/workflow-projections.js +8 -0
  80. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  81. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  82. package/dist/resources/extensions/gsd/worktree-state-projection.js +18 -17
  83. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  84. package/dist/resources/extensions/subagent/agents.js +1 -0
  85. package/dist/resources/extensions/subagent/index.js +27 -12
  86. package/dist/resources/extensions/subagent/launch.js +7 -2
  87. package/dist/web/standalone/.next/BUILD_ID +1 -1
  88. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  89. package/dist/web/standalone/.next/build-manifest.json +2 -2
  90. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  91. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  92. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  100. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app/index.html +1 -1
  108. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  109. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  110. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  111. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  112. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  113. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  114. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  115. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  116. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  118. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  119. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  120. package/dist/web/standalone/node_modules/@gsd/native/dist/native.js +22 -0
  121. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  122. package/package.json +4 -4
  123. package/packages/cloud-mcp-gateway/package.json +2 -2
  124. package/packages/contracts/package.json +1 -1
  125. package/packages/daemon/package.json +4 -4
  126. package/packages/gsd-agent-core/package.json +5 -5
  127. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  128. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js +21 -23
  129. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js.map +1 -1
  130. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  131. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  132. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +25 -0
  133. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  134. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +1 -0
  135. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  136. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +66 -12
  137. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  138. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  139. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +18 -11
  140. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  141. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  142. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +16 -0
  143. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  144. package/packages/gsd-agent-modes/package.json +7 -7
  145. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  146. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  147. package/packages/mcp-server/package.json +3 -3
  148. package/packages/native/dist/native.js +22 -0
  149. package/packages/native/package.json +1 -1
  150. package/packages/pi-agent-core/package.json +1 -1
  151. package/packages/pi-ai/dist/image-models.generated.d.ts +30 -0
  152. package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  153. package/packages/pi-ai/dist/image-models.generated.js +30 -0
  154. package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
  155. package/packages/pi-ai/dist/models.generated.d.ts +174 -29
  156. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  157. package/packages/pi-ai/dist/models.generated.js +178 -54
  158. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  159. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  160. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  161. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  162. package/packages/pi-ai/package.json +1 -1
  163. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  164. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  165. package/packages/pi-coding-agent/dist/theme/themes.js +1 -1
  166. package/packages/pi-coding-agent/dist/theme/themes.js.map +1 -1
  167. package/packages/pi-coding-agent/package.json +7 -7
  168. package/packages/pi-tui/dist/utils.d.ts +11 -0
  169. package/packages/pi-tui/dist/utils.d.ts.map +1 -1
  170. package/packages/pi-tui/dist/utils.js +119 -6
  171. package/packages/pi-tui/dist/utils.js.map +1 -1
  172. package/packages/pi-tui/package.json +2 -1
  173. package/packages/rpc-client/package.json +2 -2
  174. package/pkg/dist/theme/themes.js +1 -1
  175. package/pkg/dist/theme/themes.js.map +1 -1
  176. package/pkg/package.json +1 -1
  177. package/scripts/install/handoff.js +16 -3
  178. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  179. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  180. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  181. package/src/resources/extensions/browser-tools/index.ts +75 -27
  182. package/src/resources/extensions/browser-tools/state.ts +13 -0
  183. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  184. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +57 -0
  185. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  186. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  187. package/src/resources/extensions/browser-tools/tools/session.ts +4 -2
  188. package/src/resources/extensions/browser-tools/utils.ts +3 -3
  189. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  190. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  191. package/src/resources/extensions/gsd/auto/loop.ts +4 -2
  192. package/src/resources/extensions/gsd/auto/phases.ts +89 -15
  193. package/src/resources/extensions/gsd/auto/session.ts +24 -1
  194. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +1 -0
  195. package/src/resources/extensions/gsd/auto-dispatch.ts +117 -12
  196. package/src/resources/extensions/gsd/auto-model-selection.ts +190 -12
  197. package/src/resources/extensions/gsd/auto-post-unit.ts +20 -2
  198. package/src/resources/extensions/gsd/auto-prompts.ts +25 -22
  199. package/src/resources/extensions/gsd/auto-recovery.ts +22 -3
  200. package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
  201. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  202. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  203. package/src/resources/extensions/gsd/auto.ts +41 -14
  204. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  205. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +250 -78
  206. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  207. package/src/resources/extensions/gsd/closeout-wizard.ts +47 -13
  208. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  209. package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -17
  210. package/src/resources/extensions/gsd/commands-maintenance.ts +124 -13
  211. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  212. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -2
  213. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  214. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  215. package/src/resources/extensions/gsd/db-writer.ts +38 -0
  216. package/src/resources/extensions/gsd/docs/preferences-reference.md +50 -1
  217. package/src/resources/extensions/gsd/gsd-db.ts +564 -186
  218. package/src/resources/extensions/gsd/guided-flow.ts +4 -1
  219. package/src/resources/extensions/gsd/markdown-renderer.ts +44 -66
  220. package/src/resources/extensions/gsd/md-importer.ts +49 -2
  221. package/src/resources/extensions/gsd/migration-auto-check.ts +154 -34
  222. package/src/resources/extensions/gsd/parsers-legacy.ts +20 -0
  223. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  224. package/src/resources/extensions/gsd/planning-path-scope.ts +22 -4
  225. package/src/resources/extensions/gsd/pre-execution-checks.ts +9 -2
  226. package/src/resources/extensions/gsd/preferences-models.ts +113 -43
  227. package/src/resources/extensions/gsd/preferences-types.ts +47 -0
  228. package/src/resources/extensions/gsd/preferences-validation.ts +76 -2
  229. package/src/resources/extensions/gsd/preferences.ts +5 -0
  230. package/src/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
  231. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  232. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  233. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  234. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
  235. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  236. package/src/resources/extensions/gsd/roadmap-slices.ts +6 -1
  237. package/src/resources/extensions/gsd/safety/content-validator.ts +8 -5
  238. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  239. package/src/resources/extensions/gsd/source-observations.ts +402 -0
  240. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +20 -8
  241. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +44 -5
  242. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +39 -11
  243. package/src/resources/extensions/gsd/state-reconciliation/index.ts +45 -15
  244. package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +4 -4
  245. package/src/resources/extensions/gsd/state.ts +7 -4
  246. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +114 -0
  247. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  248. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +299 -1
  249. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +32 -0
  250. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +75 -3
  251. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +22 -1
  252. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  253. package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +145 -0
  254. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  255. package/src/resources/extensions/gsd/tests/closeout-wizard.test.ts +44 -0
  256. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +26 -1
  257. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  258. package/src/resources/extensions/gsd/tests/content-validator.test.ts +74 -0
  259. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  260. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +17 -2
  261. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  262. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +1 -11
  263. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +64 -0
  264. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +15 -0
  265. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +62 -1
  266. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +4 -1
  267. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  268. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -0
  269. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +42 -0
  270. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  271. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  272. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +99 -0
  273. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +99 -2
  274. package/src/resources/extensions/gsd/tests/plan-task.test.ts +19 -0
  275. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  276. package/src/resources/extensions/gsd/tests/preferences.test.ts +14 -0
  277. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +1 -0
  278. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +133 -0
  279. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  280. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +101 -1
  281. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +2 -2
  282. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +28 -0
  283. package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +5 -3
  284. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +162 -18
  285. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  286. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +8 -0
  287. package/src/resources/extensions/gsd/tests/source-observations.test.ts +275 -0
  288. package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +43 -0
  289. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -21
  290. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +203 -0
  291. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +170 -0
  292. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +7 -1
  293. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  294. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +306 -1
  295. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  296. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +260 -5
  297. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +511 -1
  298. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  299. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +44 -0
  300. package/src/resources/extensions/gsd/tool-contract.ts +29 -1
  301. package/src/resources/extensions/gsd/tool-presentation-plan.ts +41 -6
  302. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -0
  303. package/src/resources/extensions/gsd/tools/plan-slice.ts +54 -12
  304. package/src/resources/extensions/gsd/tools/plan-task.ts +8 -1
  305. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +71 -489
  306. package/src/resources/extensions/gsd/types.ts +1 -0
  307. package/src/resources/extensions/gsd/uat-policy.ts +191 -0
  308. package/src/resources/extensions/gsd/uat-run.ts +550 -0
  309. package/src/resources/extensions/gsd/unit-context-manifest.ts +3 -4
  310. package/src/resources/extensions/gsd/unit-tool-contracts.ts +38 -14
  311. package/src/resources/extensions/gsd/verdict-parser.ts +3 -10
  312. package/src/resources/extensions/gsd/workflow-manifest.ts +193 -7
  313. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -3
  314. package/src/resources/extensions/gsd/workflow-projections.ts +9 -0
  315. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  316. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  317. package/src/resources/extensions/gsd/worktree-state-projection.ts +22 -22
  318. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  319. package/src/resources/extensions/shared/tests/format-utils.test.ts +8 -3
  320. package/src/resources/extensions/subagent/agents.ts +4 -0
  321. package/src/resources/extensions/subagent/index.ts +28 -3
  322. package/src/resources/extensions/subagent/launch.ts +8 -0
  323. package/src/resources/extensions/subagent/tests/model-override.test.ts +31 -0
  324. /package/dist/web/standalone/.next/static/{9y3LeeR2uGr2yRj9RjY3D → tJOKQbQRO-9MiFDO8DIDS}/_buildManifest.js +0 -0
  325. /package/dist/web/standalone/.next/static/{9y3LeeR2uGr2yRj9RjY3D → tJOKQbQRO-9MiFDO8DIDS}/_ssgManifest.js +0 -0
@@ -0,0 +1,306 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Active-unit source observations for provider payload context.
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { isAbsolute, relative, resolve, sep } from "node:path";
5
+ import { extractPlanningPathReference, normalizeFilePath } from "./pre-execution-checks.js";
6
+ export const WHOLE_FILE_OBSERVATION_MAX_BYTES = 50 * 1024;
7
+ export const WHOLE_FILE_OBSERVATION_MAX_LINES = 2000;
8
+ const SOURCE_CONTEXT_TITLE = "## Source Context Block";
9
+ const SOURCE_OBSERVATION_UNIT_TYPE = "execute-task";
10
+ export function supportsSourceObservationsForUnit(unitType) {
11
+ return unitType === SOURCE_OBSERVATION_UNIT_TYPE;
12
+ }
13
+ export class SourceObservationStore {
14
+ active = null;
15
+ beginUnit(unit) {
16
+ if (!supportsSourceObservationsForUnit(unit.unitType)) {
17
+ this.clear();
18
+ return;
19
+ }
20
+ if (this.matches(unit))
21
+ return;
22
+ this.active = { unit: { ...unit }, observations: new Map() };
23
+ }
24
+ clear() {
25
+ this.active = null;
26
+ }
27
+ degradeUnit(unit) {
28
+ if (!this.active)
29
+ return;
30
+ const current = this.active.unit;
31
+ if (current.unitType === unit.unitType &&
32
+ current.unitId === unit.unitId &&
33
+ current.startedAt === unit.startedAt) {
34
+ this.active = null;
35
+ }
36
+ }
37
+ observePlanTask(task) {
38
+ if (!this.active)
39
+ return;
40
+ for (const entry of planDeclaredSourceEntries(task)) {
41
+ this.observePath(entry.path, "plan");
42
+ }
43
+ }
44
+ observeRead(input) {
45
+ if (!this.active)
46
+ return;
47
+ const rawPath = readPathFromInput(input);
48
+ if (!rawPath.trim())
49
+ return;
50
+ this.observePath(rawPath, "read");
51
+ }
52
+ observeMutation(input) {
53
+ if (!this.active)
54
+ return;
55
+ const rawPath = readPathFromInput(input);
56
+ if (!rawPath.trim())
57
+ return;
58
+ this.observePath(rawPath, "mutation", { replaceExisting: true });
59
+ }
60
+ renderActiveBlock() {
61
+ if (!this.active || this.active.observations.size === 0)
62
+ return null;
63
+ if (!supportsSourceObservationsForUnit(this.active.unit.unitType))
64
+ return null;
65
+ const observations = [...this.active.observations.values()]
66
+ .sort((a, b) => a.path.localeCompare(b.path));
67
+ const lines = [
68
+ SOURCE_CONTEXT_TITLE,
69
+ `Active Unit: ${this.active.unit.unitType} ${this.active.unit.unitId}`,
70
+ "",
71
+ "The files below are protected active-Unit source context. " +
72
+ "Use this block instead of rereading small files just to recover line windows.",
73
+ ];
74
+ const wholeFiles = observations.filter((observation) => observation.status === "whole");
75
+ const unavailable = observations.filter((observation) => observation.status !== "whole");
76
+ if (wholeFiles.length > 0) {
77
+ lines.push("", "### Whole-File Observations");
78
+ for (const observation of wholeFiles) {
79
+ lines.push("", `#### ${observation.path}`, `Status: whole-file (${observation.lines ?? 0} lines, ${formatSize(observation.bytes ?? 0)})`, fencedSource(observation.text ?? ""));
80
+ }
81
+ }
82
+ if (unavailable.length > 0) {
83
+ lines.push("", "### Unavailable Source Observations");
84
+ for (const observation of unavailable) {
85
+ const detail = observation.reason ? ` - ${observation.reason}` : "";
86
+ lines.push(`- ${observation.path}: ${observation.status}${detail}`);
87
+ }
88
+ }
89
+ return lines.join("\n");
90
+ }
91
+ matches(unit) {
92
+ if (!this.active)
93
+ return false;
94
+ const current = this.active.unit;
95
+ return current.unitType === unit.unitType &&
96
+ current.unitId === unit.unitId &&
97
+ current.startedAt === unit.startedAt &&
98
+ current.basePath === unit.basePath;
99
+ }
100
+ observePath(rawPath, source, options = {}) {
101
+ if (!this.active)
102
+ return;
103
+ const observation = observeSourcePath(this.active.unit.basePath, rawPath, source);
104
+ const key = observation.absolutePath ?? `${observation.status}:${observation.path}`;
105
+ const existing = this.active.observations.get(key);
106
+ if (options.replaceExisting || !existing || observation.status === "whole" || existing.status !== "whole") {
107
+ this.active.observations.set(key, observation);
108
+ }
109
+ }
110
+ }
111
+ export function planDeclaredSourceEntries(task) {
112
+ const entries = [];
113
+ for (const file of task.files) {
114
+ const path = extractPlanningPathReference(file) ?? file.trim();
115
+ if (path)
116
+ entries.push({ path, field: "files" });
117
+ }
118
+ for (const input of task.inputs) {
119
+ const path = extractPlanningPathReference(input);
120
+ if (path)
121
+ entries.push({ path, field: "inputs" });
122
+ }
123
+ return entries;
124
+ }
125
+ export function observeSourcePath(basePath, rawPath, source) {
126
+ const normalizedRaw = normalizeFilePath(rawPath.trim());
127
+ const displayPath = normalizedRaw || rawPath.trim();
128
+ if (!normalizedRaw) {
129
+ return unavailable(displayPath || "<empty>", null, "unresolved selector", source);
130
+ }
131
+ if (containsGlobPattern(normalizedRaw)) {
132
+ return unavailable(displayPath, null, "glob", source, "glob selectors are not whole files");
133
+ }
134
+ if (rawPath.trim().endsWith("/")) {
135
+ return unavailable(displayPath, null, "directory", source, "directory selectors are not whole files");
136
+ }
137
+ const rootPath = resolve(basePath);
138
+ const absolutePath = isAbsolute(normalizedRaw) ? resolve(normalizedRaw) : resolve(rootPath, normalizedRaw);
139
+ const path = formatObservationPath(rootPath, absolutePath, displayPath);
140
+ if (!isPathInsideRoot(rootPath, absolutePath)) {
141
+ return unavailable(displayPath, absolutePath, "unresolved selector", source, "path is outside active Unit root");
142
+ }
143
+ if (!existsSync(absolutePath)) {
144
+ return unavailable(path, absolutePath, "missing", source, "file does not exist in the active Unit root");
145
+ }
146
+ let stat;
147
+ try {
148
+ stat = statSync(absolutePath);
149
+ }
150
+ catch {
151
+ return unavailable(path, absolutePath, "missing", source, "file could not be inspected");
152
+ }
153
+ if (stat.isDirectory()) {
154
+ return unavailable(path, absolutePath, "directory", source, "directory selectors are not whole files");
155
+ }
156
+ if (!stat.isFile()) {
157
+ return unavailable(path, absolutePath, "unresolved selector", source, "path is not a regular file");
158
+ }
159
+ if (stat.size > WHOLE_FILE_OBSERVATION_MAX_BYTES) {
160
+ return unavailable(path, absolutePath, "over-threshold", source, `${formatSize(stat.size)} exceeds ${formatSize(WHOLE_FILE_OBSERVATION_MAX_BYTES)}`);
161
+ }
162
+ let buffer;
163
+ try {
164
+ buffer = readFileSync(absolutePath);
165
+ }
166
+ catch {
167
+ return unavailable(path, absolutePath, "missing", source, "file could not be read");
168
+ }
169
+ if (isBinaryOrImage(buffer)) {
170
+ return unavailable(path, absolutePath, "binary/image", source, "binary or image files are not inlined as source text");
171
+ }
172
+ const text = buffer.toString("utf8");
173
+ const lines = countLines(text);
174
+ if (lines > WHOLE_FILE_OBSERVATION_MAX_LINES) {
175
+ return unavailable(path, absolutePath, "over-threshold", source, `${lines} lines exceeds ${WHOLE_FILE_OBSERVATION_MAX_LINES}`);
176
+ }
177
+ return {
178
+ path,
179
+ absolutePath,
180
+ status: "whole",
181
+ source,
182
+ text,
183
+ bytes: buffer.byteLength,
184
+ lines,
185
+ };
186
+ }
187
+ export function injectSourceContextBlockIntoPayload(payload, block) {
188
+ const messages = payload.messages;
189
+ if (Array.isArray(messages)) {
190
+ return {
191
+ ...payload,
192
+ messages: [
193
+ ...withoutExistingSourceContextMessages(messages),
194
+ { role: "user", content: [{ type: "text", text: block }] },
195
+ ],
196
+ };
197
+ }
198
+ const input = payload.input;
199
+ if (Array.isArray(input)) {
200
+ return {
201
+ ...payload,
202
+ input: [
203
+ ...withoutExistingSourceContextItems(input),
204
+ { role: "user", content: [{ type: "input_text", text: block }] },
205
+ ],
206
+ };
207
+ }
208
+ return payload;
209
+ }
210
+ function unavailable(path, absolutePath, status, source, reason) {
211
+ return { path, absolutePath, status, source, reason };
212
+ }
213
+ function formatObservationPath(basePath, absolutePath, fallback) {
214
+ const rel = relative(basePath, absolutePath);
215
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) {
216
+ return rel.split(sep).join("/");
217
+ }
218
+ return fallback;
219
+ }
220
+ function isPathInsideRoot(rootPath, absolutePath) {
221
+ const rel = relative(rootPath, absolutePath);
222
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
223
+ }
224
+ function containsGlobPattern(candidate) {
225
+ return ["*", "?", "[", "]", "{", "}"].some((char) => candidate.includes(char));
226
+ }
227
+ function readPathFromInput(input) {
228
+ if (typeof input.path === "string")
229
+ return input.path;
230
+ if (typeof input.file_path === "string")
231
+ return input.file_path;
232
+ return "";
233
+ }
234
+ function formatSize(bytes) {
235
+ if (bytes < 1024)
236
+ return `${bytes}B`;
237
+ if (bytes < 1024 * 1024)
238
+ return `${(bytes / 1024).toFixed(1)}KB`;
239
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
240
+ }
241
+ function isBinaryOrImage(buffer) {
242
+ if (buffer.includes(0))
243
+ return true;
244
+ if (startsWith(buffer, [0xff, 0xd8, 0xff]))
245
+ return true;
246
+ if (startsWith(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
247
+ return true;
248
+ if (startsWithAscii(buffer, "GIF"))
249
+ return true;
250
+ if (startsWithAscii(buffer, "RIFF") &&
251
+ buffer.length >= 12 &&
252
+ buffer.subarray(8, 12).toString("ascii") === "WEBP")
253
+ return true;
254
+ return false;
255
+ }
256
+ function startsWith(buffer, signature) {
257
+ if (buffer.length < signature.length)
258
+ return false;
259
+ return signature.every((byte, index) => buffer[index] === byte);
260
+ }
261
+ function startsWithAscii(buffer, text) {
262
+ return buffer.length >= text.length && buffer.subarray(0, text.length).toString("ascii") === text;
263
+ }
264
+ function countLines(text) {
265
+ if (text.length === 0)
266
+ return 0;
267
+ return text.endsWith("\n") ? text.split("\n").length - 1 : text.split("\n").length;
268
+ }
269
+ function fencedSource(text) {
270
+ const longest = longestBacktickRun(text);
271
+ const fence = longest >= 3 ? "`".repeat(longest + 1) : "```";
272
+ return `${fence}\n${text}\n${fence}`;
273
+ }
274
+ function longestBacktickRun(text) {
275
+ let longest = 0;
276
+ let current = 0;
277
+ for (const char of text) {
278
+ if (char === "`") {
279
+ current += 1;
280
+ longest = Math.max(longest, current);
281
+ }
282
+ else {
283
+ current = 0;
284
+ }
285
+ }
286
+ return longest;
287
+ }
288
+ function firstText(content) {
289
+ if (typeof content === "string")
290
+ return content;
291
+ if (!Array.isArray(content))
292
+ return null;
293
+ const block = content.find((entry) => entry && typeof entry === "object" && "text" in entry);
294
+ return typeof block?.text === "string" ? block.text : null;
295
+ }
296
+ function isSourceContextMessage(message) {
297
+ if (!message || typeof message !== "object")
298
+ return false;
299
+ return firstText(message.content)?.startsWith(SOURCE_CONTEXT_TITLE) === true;
300
+ }
301
+ function withoutExistingSourceContextMessages(messages) {
302
+ return messages.filter((message) => !isSourceContextMessage(message));
303
+ }
304
+ function withoutExistingSourceContextItems(items) {
305
+ return items.filter((item) => !isSourceContextMessage(item));
306
+ }
@@ -6,7 +6,7 @@
6
6
  // SUMMARY.md mtime — deterministic and idempotent (re-running yields the
7
7
  // same value).
8
8
  import { existsSync, statSync } from "node:fs";
9
- import { getMilestone, getMilestoneSlices, getSliceTasks, isDbAvailable, updateMilestoneStatus, updateSliceStatus, updateTaskStatus, } from "../../gsd-db.js";
9
+ import { getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, isDbAvailable, updateMilestoneStatus, updateSliceStatus, updateTaskStatus, } from "../../gsd-db.js";
10
10
  import { resolveMilestoneFile, resolveSliceFile, resolveTaskFile, } from "../../paths.js";
11
11
  const COMPLETE_STATUSES = new Set(["complete", "done"]);
12
12
  function summaryMtimeIso(path) {
@@ -17,16 +17,24 @@ function summaryMtimeIso(path) {
17
17
  return null;
18
18
  }
19
19
  }
20
- export function detectMissingCompletionTimestampDrift(state, ctx) {
20
+ export function detectMissingCompletionTimestampDrift(_state, ctx) {
21
21
  if (!isDbAvailable())
22
22
  return [];
23
- const mid = state.activeMilestone?.id;
24
- if (!mid)
25
- return [];
23
+ // Scan every milestone, not just the active one. Markdown artifacts exist on
24
+ // disk for all milestones, so a user can manually complete a queued/parked
25
+ // milestone (edit the roadmap + drop a SUMMARY) and leave completed_at=null
26
+ // in the DB. Gating on the active milestone left that drift unrepaired until
27
+ // the milestone happened to become active.
28
+ const drifts = [];
29
+ for (const { id: mid } of getAllMilestones()) {
30
+ collectMilestoneCompletionDrift(mid, ctx, drifts);
31
+ }
32
+ return drifts;
33
+ }
34
+ function collectMilestoneCompletionDrift(mid, ctx, drifts) {
26
35
  const milestone = getMilestone(mid);
27
36
  if (!milestone)
28
- return [];
29
- const drifts = [];
37
+ return;
30
38
  // Milestone-level
31
39
  if (COMPLETE_STATUSES.has(milestone.status) &&
32
40
  milestone.completed_at === null) {
@@ -69,7 +77,6 @@ export function detectMissingCompletionTimestampDrift(state, ctx) {
69
77
  }
70
78
  }
71
79
  }
72
- return drifts;
73
80
  }
74
81
  export function repairMissingCompletionTimestamp(record, ctx) {
75
82
  const composite = record.ids[0];
@@ -40,6 +40,18 @@ function isRepairableStaleRenderReason(reason) {
40
40
  (reason.includes("SUMMARY.md missing") && /^S\d+/.test(reason)) ||
41
41
  reason.includes("UAT.md missing"));
42
42
  }
43
+ function canonicalizeMilestoneId(dirSegment) {
44
+ if (getMilestone(dirSegment))
45
+ return dirSegment;
46
+ const suffixId = dirSegment.match(/^(M\d+-[a-z0-9]{6})(?:$|-)/)?.[1];
47
+ if (suffixId && getMilestone(suffixId))
48
+ return suffixId;
49
+ // Descriptor layout: e.g. M001-DESCRIPTOR → M001
50
+ const baseId = dirSegment.match(/^(M\d+)(?:$|-)/i)?.[1];
51
+ if (baseId && getMilestone(baseId))
52
+ return baseId;
53
+ return suffixId ?? baseId ?? dirSegment;
54
+ }
43
55
  function resolveRoadmapMilestoneIdFromPath(normPath) {
44
56
  const milestoneMatch = normPath.match(/milestones\/([^/]+)\//);
45
57
  if (!milestoneMatch) {
@@ -70,7 +82,12 @@ async function repairStaleRenderFromBasePath(record, basePath) {
70
82
  if (!pathMatch) {
71
83
  throw new Error(`stale-render drift: plan path missing milestone/slice segments: ${record.renderPath}`);
72
84
  }
73
- await renderPlanCheckboxes(basePath, pathMatch[1], pathMatch[2]);
85
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
86
+ const wrote = await renderPlanCheckboxes(basePath, milestoneId, pathMatch[2], record.renderPath);
87
+ if (!wrote) {
88
+ throw new Error(`stale-render drift: plan re-render wrote nothing for ${milestoneId}/${pathMatch[2]} ` +
89
+ `(${record.renderPath}); slice has no tasks or its path is unresolvable`);
90
+ }
74
91
  return;
75
92
  }
76
93
  if (reason.includes("SUMMARY.md missing") && /^T\d+/.test(reason)) {
@@ -79,7 +96,13 @@ async function repairStaleRenderFromBasePath(record, basePath) {
79
96
  if (!pathMatch || !taskMatch) {
80
97
  throw new Error(`stale-render drift: task summary path/reason malformed: ${record.renderPath} reason=${reason}`);
81
98
  }
82
- await renderTaskSummary(basePath, pathMatch[1], pathMatch[2], taskMatch[1]);
99
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
100
+ const wrote = await renderTaskSummary(basePath, milestoneId, pathMatch[2], taskMatch[1]);
101
+ if (!wrote) {
102
+ throw new Error(`stale-render drift: task summary re-render wrote nothing for ` +
103
+ `${milestoneId}/${pathMatch[2]}/${taskMatch[1]} (${record.renderPath}); ` +
104
+ `task has no summary in DB or its slice path is unresolvable`);
105
+ }
83
106
  return;
84
107
  }
85
108
  if (reason.includes("SUMMARY.md missing") && /^S\d+/.test(reason)) {
@@ -87,7 +110,7 @@ async function repairStaleRenderFromBasePath(record, basePath) {
87
110
  if (!pathMatch) {
88
111
  throw new Error(`stale-render drift: slice summary path missing milestone/slice segments: ${record.renderPath}`);
89
112
  }
90
- const milestoneId = pathMatch[1];
113
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
91
114
  const sliceId = pathMatch[2];
92
115
  const slice = getSlice(milestoneId, sliceId);
93
116
  const uatPath = join(dirname(record.renderPath), buildSliceFileName(sliceId, "UAT"));
@@ -95,7 +118,12 @@ async function repairStaleRenderFromBasePath(record, basePath) {
95
118
  if (slice?.full_uat_md && !existsSync(uatPath)) {
96
119
  setSliceSummaryMd(milestoneId, sliceId, slice.full_summary_md ?? "", "");
97
120
  }
98
- await renderSliceSummary(basePath, milestoneId, sliceId);
121
+ const wrote = await renderSliceSummary(basePath, milestoneId, sliceId);
122
+ if (!wrote) {
123
+ throw new Error(`stale-render drift: slice summary re-render wrote nothing for ` +
124
+ `${milestoneId}/${sliceId} (${record.renderPath}); slice has no summary/UAT ` +
125
+ `in DB or its path is unresolvable`);
126
+ }
99
127
  return;
100
128
  }
101
129
  if (reason.includes("UAT.md missing")) {
@@ -105,7 +133,7 @@ async function repairStaleRenderFromBasePath(record, basePath) {
105
133
  }
106
134
  // When UAT.md is removed from disk, mirror that intent by clearing stale
107
135
  // persisted UAT content instead of rehydrating it back onto disk.
108
- const milestoneId = pathMatch[1];
136
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
109
137
  const sliceId = pathMatch[2];
110
138
  const slice = getSlice(milestoneId, sliceId);
111
139
  if (!slice) {
@@ -4,26 +4,47 @@
4
4
  // laptop sleep where the heartbeat wasn't released cleanly), and clears them
5
5
  // before the next dispatch attempts to acquire the lock.
6
6
  import { effectiveLockFile, isSessionLockProcessAlive, readSessionLockData, removeStaleSessionLock, } from "../../session-lock.js";
7
+ import { clearStaleWorkerLock } from "../../crash-recovery.js";
8
+ import { findStaleWorkerForProject } from "../../db/auto-workers.js";
9
+ import { isDbAvailable } from "../../gsd-db.js";
10
+ import { normalizeRealPath } from "../../paths.js";
11
+ import { logWarning } from "../../workflow-logger.js";
7
12
  export function detectStaleWorkerDrift(_state, ctx) {
8
13
  const data = readSessionLockData(ctx.basePath);
9
- if (!data)
10
- return [];
11
- if (typeof data.pid !== "number")
12
- return [];
13
- if (isSessionLockProcessAlive(data))
14
- return [];
15
- return [
16
- {
17
- kind: "stale-worker",
18
- lockPath: effectiveLockFile(),
19
- pid: data.pid,
20
- },
21
- ];
14
+ if (data && typeof data.pid === "number") {
15
+ return isSessionLockProcessAlive(data)
16
+ ? []
17
+ : [{ kind: "stale-worker", lockPath: effectiveLockFile(), pid: data.pid }];
18
+ }
19
+ // The lock file is missing or unparseable. It is not the only source of
20
+ // truth: a crashed worker can leave a workers row 'active' with held leases
21
+ // and in-flight dispatches even when its lock file is gone. Fall back to the
22
+ // DB worker registry so that state is still detected and repaired.
23
+ if (isDbAvailable()) {
24
+ try {
25
+ const stale = findStaleWorkerForProject(normalizeRealPath(ctx.basePath));
26
+ if (stale && typeof stale.pid === "number") {
27
+ return [{ kind: "stale-worker", lockPath: effectiveLockFile(), pid: stale.pid }];
28
+ }
29
+ }
30
+ catch (err) {
31
+ // Best-effort: detection must never throw and abort the reconcile cycle.
32
+ logWarning("reconcile", `stale-worker DB fallback detection failed: ${err.message}`);
33
+ }
34
+ }
35
+ return [];
22
36
  }
23
37
  export function repairStaleWorker(_record, ctx) {
24
38
  // removeStaleSessionLock is idempotent: it re-reads lock state and is a
25
39
  // no-op when the lock is held by an alive process. Safe under cap=2 retry.
26
40
  removeStaleSessionLock(ctx.basePath);
41
+ // Removing the lock file alone leaves the DB-side worker state dangling: the
42
+ // dead worker's milestone_leases stay 'held' and its unit_dispatches stay
43
+ // 'running'/'claimed', blocking new claims until the lease TTL expires.
44
+ // clearStaleWorkerLock cancels those dispatches, releases the leases, and
45
+ // marks the worker stopping — the same cleanup the startup crash-recovery
46
+ // path performs. It is DB-gated, idempotent, and best-effort.
47
+ clearStaleWorkerLock(ctx.basePath);
27
48
  }
28
49
  export const staleWorkerHandler = {
29
50
  kind: "stale-worker",
@@ -3,6 +3,8 @@
3
3
  // reconcileBeforeDispatch runs before every Dispatch decision and worker spawn.
4
4
  import { deriveState as defaultDeriveState, invalidateStateCache as defaultInvalidate, } from "../state.js";
5
5
  import { clearParseCache as defaultClearParseCache } from "../files.js";
6
+ import { clearPathCache } from "../paths.js";
7
+ import { logWarning } from "../workflow-logger.js";
6
8
  import { ReconciliationFailedError, } from "./errors.js";
7
9
  import { DRIFT_REGISTRY } from "./registry.js";
8
10
  export { ReconciliationFailedError } from "./errors.js";
@@ -35,17 +37,23 @@ export async function reconcileBeforeDispatch(basePath, deps = defaultDeps) {
35
37
  deps.invalidateStateCache();
36
38
  const stateSnapshot = await deps.deriveState(basePath, deps.deriveStateOptions);
37
39
  const ctx = { basePath, state: stateSnapshot };
38
- const drift = await detectAllDrift(stateSnapshot, ctx, registry, pass);
40
+ const detection = await detectAllDrift(stateSnapshot, ctx, registry);
41
+ const drift = detection.records;
39
42
  if (drift.length === 0) {
40
43
  return {
41
44
  ok: true,
42
45
  stateSnapshot,
43
46
  repaired,
44
- blockers: stateSnapshot.blockers ?? [],
47
+ blockers: [
48
+ ...new Set([
49
+ ...(stateSnapshot.blockers ?? []),
50
+ ...detection.detectBlockers,
51
+ ]),
52
+ ],
45
53
  };
46
54
  }
47
55
  const failures = [];
48
- const blockers = [];
56
+ const blockers = [...detection.detectBlockers];
49
57
  let repairedThisPass = false;
50
58
  for (const record of drift) {
51
59
  const handler = registry.find((h) => h.kind === record.kind);
@@ -71,7 +79,11 @@ export async function reconcileBeforeDispatch(basePath, deps = defaultDeps) {
71
79
  }
72
80
  }
73
81
  if (repairedThisPass) {
82
+ // A repair may have mutated on-disk structure (e.g. quarantined a slice
83
+ // dir). Clear both the parse cache and the path/dir cache centrally so
84
+ // later passes and any subsequent repair see fresh filesystem state.
74
85
  clearParseCache();
86
+ clearPathCache();
75
87
  }
76
88
  if (blockers.length > 0) {
77
89
  let blockerState = stateSnapshot;
@@ -95,9 +107,10 @@ export async function reconcileBeforeDispatch(basePath, deps = defaultDeps) {
95
107
  deps.invalidateStateCache();
96
108
  const finalState = await deps.deriveState(basePath, deps.deriveStateOptions);
97
109
  const finalCtx = { basePath, state: finalState };
98
- const persistent = await detectAllDrift(finalState, finalCtx, registry);
110
+ const finalDetection = await detectAllDrift(finalState, finalCtx, registry);
111
+ const persistent = finalDetection.records;
99
112
  if (persistent.length > 0) {
100
- const blockers = [];
113
+ const blockers = [...finalDetection.detectBlockers];
101
114
  const unblockedPersistent = [];
102
115
  for (const record of persistent) {
103
116
  const handler = registry.find((h) => h.kind === record.kind);
@@ -123,24 +136,36 @@ export async function reconcileBeforeDispatch(basePath, deps = defaultDeps) {
123
136
  ok: true,
124
137
  stateSnapshot: finalState,
125
138
  repaired,
126
- blockers: finalState.blockers ?? [],
139
+ blockers: [
140
+ ...new Set([
141
+ ...(finalState.blockers ?? []),
142
+ ...finalDetection.detectBlockers,
143
+ ]),
144
+ ],
127
145
  };
128
146
  }
147
+ /**
148
+ * Run every detector. A single detector throwing (e.g. a transient file read
149
+ * error) must NOT abort the whole cycle and hide every later handler's drift —
150
+ * it is collected as a blocker so dispatch is still gated, while the remaining
151
+ * detectors run and their drift gets repaired (graceful degradation, ADR-017).
152
+ */
129
153
  async function detectAllDrift(state, ctx,
130
154
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
- registry, pass) {
132
- const collected = [];
155
+ registry) {
156
+ const records = [];
157
+ const detectBlockers = [];
133
158
  for (const handler of registry) {
134
159
  try {
135
160
  const detected = await handler.detect(state, ctx);
136
- collected.push(...detected);
161
+ records.push(...detected);
137
162
  }
138
163
  catch (cause) {
139
- throw new ReconciliationFailedError({
140
- failures: [{ drift: { kind: handler.kind }, cause }],
141
- pass,
142
- });
164
+ const message = cause instanceof Error ? cause.message : String(cause);
165
+ const blocker = `Drift detection failed for "${handler.kind}": ${message}`;
166
+ logWarning("reconcile", blocker);
167
+ detectBlockers.push(blocker);
143
168
  }
144
169
  }
145
- return collected;
170
+ return { records, detectBlockers };
146
171
  }
@@ -7,10 +7,10 @@
7
7
  // startSliceParallel) share one contract.
8
8
  import { reconcileBeforeDispatch, ReconciliationFailedError, } from "./index.js";
9
9
  /**
10
- * Run reconciliation before spawning workers. Returns ok=true when the run
11
- * completed without throwing (blockers ride along but don't fail the gate —
12
- * spawn callers can choose how to handle them). On
13
- * ReconciliationFailedError, returns ok=false with the error message so the
10
+ * Run reconciliation before spawning workers. Returns ok=true only when the run
11
+ * completed without throwing AND surfaced no blockers; any blocker fails the
12
+ * gate (ok=false, reason carries the first blocker) so callers must not spawn.
13
+ * On ReconciliationFailedError, returns ok=false with the error message so the
14
14
  * caller can surface it to the user without re-throwing.
15
15
  *
16
16
  * Other unexpected errors propagate; they are not part of the drift
@@ -394,6 +394,8 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
394
394
  }
395
395
  const allSlicesDone = slices.length > 0 && slices.every(s => isStatusDone(s.status));
396
396
  const title = stripMilestonePrefix(m.title) || m.id;
397
+ const hasContext = !!resolveMilestoneFile(basePath, m.id, "CONTEXT");
398
+ const hasDraftContext = !hasContext && !!resolveMilestoneFile(basePath, m.id, "CONTEXT-DRAFT");
397
399
  if (!activeMilestoneFound) {
398
400
  const deps = m.depends_on;
399
401
  const depsUnmet = deps.some(dep => !completeMilestoneIds.has(dep));
@@ -401,9 +403,9 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
401
403
  registry.push({ id: m.id, title, status: 'pending', dependsOn: deps });
402
404
  continue;
403
405
  }
404
- if (m.status === 'queued' && slices.length === 0) {
406
+ if (m.status === 'queued' && slices.length === 0 && !hasContext) {
405
407
  if (!firstDeferredQueuedShell) {
406
- firstDeferredQueuedShell = { id: m.id, title, deps };
408
+ firstDeferredQueuedShell = { id: m.id, title, deps, hasDraftContext };
407
409
  }
408
410
  registry.push({ id: m.id, title, status: 'pending', ...(deps.length > 0 ? { dependsOn: deps } : {}) });
409
411
  continue;
@@ -415,7 +417,7 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
415
417
  registry.push({ id: m.id, title, status: 'active', ...(deps.length > 0 ? { dependsOn: deps } : {}) });
416
418
  continue;
417
419
  }
418
- if (m.status === 'needs-discussion')
420
+ if ((m.status === 'needs-discussion' && !hasContext) || hasDraftContext)
419
421
  activeMilestoneHasDraft = true;
420
422
  activeMilestone = { id: m.id, title };
421
423
  activeMilestoneSlices = slices;
@@ -432,6 +434,8 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
432
434
  activeMilestone = { id: shell.id, title: shell.title };
433
435
  activeMilestoneSlices = [];
434
436
  activeMilestoneFound = true;
437
+ if (shell.hasDraftContext)
438
+ activeMilestoneHasDraft = true;
435
439
  const entry = registry.find(e => e.id === shell.id);
436
440
  if (entry)
437
441
  entry.status = 'active';