spexcode 0.6.2 → 0.6.6

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 (495) hide show
  1. package/README.md +3 -1
  2. package/bin/spex.mjs +4 -0
  3. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +23 -0
  4. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +195 -0
  5. package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
  6. package/node_modules/@spexcode/session-core/dist/index.js +5 -0
  7. package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
  8. package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
  9. package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
  10. package/node_modules/@spexcode/session-core/dist/message.js +52 -0
  11. package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
  12. package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
  13. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +50 -0
  14. package/node_modules/@spexcode/session-core/dist/runtime-session.js +286 -0
  15. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +14 -0
  16. package/node_modules/@spexcode/session-core/dist/session-cursors.js +82 -0
  17. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +46 -0
  18. package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
  19. package/node_modules/@spexcode/session-core/package.json +33 -0
  20. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +77 -0
  21. package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
  22. package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
  23. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +36 -0
  24. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +316 -0
  25. package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
  26. package/node_modules/@spexcode/spec-cli/dist/cli.js +1908 -0
  27. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +112 -0
  28. package/node_modules/@spexcode/spec-cli/dist/client.js +481 -0
  29. package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
  30. package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
  31. package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
  32. package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
  33. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +58 -0
  34. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +798 -0
  35. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
  36. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
  37. package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
  38. package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
  39. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
  40. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
  41. package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
  42. package/node_modules/@spexcode/spec-cli/dist/doctor.js +553 -0
  43. package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
  44. package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
  45. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
  46. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
  47. package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
  48. package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
  49. package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
  50. package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
  51. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
  52. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
  53. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
  54. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +363 -0
  55. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +38 -0
  56. package/node_modules/@spexcode/spec-cli/dist/gateway.js +527 -0
  57. package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
  58. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +717 -0
  59. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
  60. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +36 -0
  61. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
  62. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
  63. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
  64. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
  65. package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
  66. package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +36 -32
  67. package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
  68. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
  69. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +338 -0
  70. package/node_modules/@spexcode/spec-cli/dist/harness.js +3146 -0
  71. package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
  72. package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
  73. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +15 -0
  74. package/node_modules/@spexcode/spec-cli/dist/help.js +594 -0
  75. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
  76. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
  77. package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
  78. package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
  79. package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
  80. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
  81. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
  82. package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
  83. package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
  84. package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
  85. package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
  86. package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
  87. package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
  88. package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
  89. package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
  90. package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
  91. package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
  92. package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
  93. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +8 -0
  94. package/node_modules/@spexcode/spec-cli/dist/listen.js +24 -0
  95. package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
  96. package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
  97. package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
  98. package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
  99. package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
  100. package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
  101. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
  102. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
  103. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
  104. package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
  105. package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
  106. package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
  107. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +5 -0
  108. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +215 -0
  109. package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
  110. package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
  111. package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
  112. package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
  113. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
  114. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
  115. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
  116. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
  117. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
  118. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
  119. package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
  120. package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
  121. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
  122. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
  123. package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
  124. package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
  125. package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
  126. package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
  127. package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
  128. package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
  129. package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
  130. package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
  131. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
  132. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
  133. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
  134. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
  135. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.d.ts +1 -0
  136. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -0
  137. package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
  138. package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
  139. package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
  140. package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
  141. package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
  142. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
  143. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
  144. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
  145. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
  146. package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
  147. package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
  148. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
  149. package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
  150. package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
  151. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +11 -0
  152. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +20 -0
  153. package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
  154. package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
  155. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +433 -0
  156. package/node_modules/@spexcode/spec-cli/dist/sessions.js +4667 -0
  157. package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
  158. package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
  159. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
  160. package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
  161. package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
  162. package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
  163. package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
  164. package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
  165. package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
  166. package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
  167. package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
  168. package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
  169. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
  170. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
  171. package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
  172. package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
  173. package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
  174. package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
  175. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
  176. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
  177. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
  178. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +1 -1
  179. package/node_modules/@spexcode/spec-cli/package.json +49 -0
  180. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  181. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  182. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  183. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  184. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  185. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  186. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  187. package/node_modules/@spexcode/spec-core/dist/git.d.ts +170 -0
  188. package/node_modules/@spexcode/spec-core/dist/git.js +2768 -0
  189. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  190. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  191. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  192. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  193. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  194. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  195. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  196. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  197. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  198. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  199. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  200. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  201. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +187 -0
  202. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  203. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  204. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  205. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  206. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  207. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  208. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  209. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  210. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  211. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  212. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  213. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  214. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  215. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  216. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  217. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  218. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  219. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  220. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  221. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  222. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  223. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  224. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  225. package/node_modules/@spexcode/spec-core/package.json +28 -0
  226. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  227. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  228. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  229. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  230. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  231. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  232. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  233. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  234. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  235. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  236. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  237. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  238. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  239. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  240. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  241. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  242. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  243. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  244. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  245. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  246. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  247. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  248. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  249. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  250. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  251. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  252. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  253. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  254. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  255. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  256. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  257. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  258. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  259. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  260. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  261. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  262. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  263. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  264. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  265. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  266. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  267. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  268. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  269. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  270. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  271. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  272. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  273. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  274. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  275. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  276. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  277. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  278. package/package.json +33 -19
  279. package/spec-cli/bin/spex.mjs +0 -69
  280. package/spec-cli/src/anchors.ts +0 -728
  281. package/spec-cli/src/attach.ts +0 -43
  282. package/spec-cli/src/claude-headless.ts +0 -321
  283. package/spec-cli/src/cli.ts +0 -1258
  284. package/spec-cli/src/client.ts +0 -414
  285. package/spec-cli/src/cockpit.ts +0 -43
  286. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  287. package/spec-cli/src/commit-surgery.ts +0 -68
  288. package/spec-cli/src/contract-filter.ts +0 -202
  289. package/spec-cli/src/delivery-queue.ts +0 -165
  290. package/spec-cli/src/doctor.ts +0 -516
  291. package/spec-cli/src/execution-trace.ts +0 -444
  292. package/spec-cli/src/file-write.ts +0 -22
  293. package/spec-cli/src/gateway-auth.ts +0 -181
  294. package/spec-cli/src/gateway-hub.ts +0 -358
  295. package/spec-cli/src/gateway.ts +0 -461
  296. package/spec-cli/src/git.ts +0 -2556
  297. package/spec-cli/src/graph.ts +0 -271
  298. package/spec-cli/src/graphCache.ts +0 -710
  299. package/spec-cli/src/graphDelta.ts +0 -77
  300. package/spec-cli/src/graphStream.ts +0 -1019
  301. package/spec-cli/src/harness-select.ts +0 -77
  302. package/spec-cli/src/harness.ts +0 -3025
  303. package/spec-cli/src/headless-controller.ts +0 -63
  304. package/spec-cli/src/help.ts +0 -492
  305. package/spec-cli/src/host-resources.ts +0 -762
  306. package/spec-cli/src/host.ts +0 -611
  307. package/spec-cli/src/identity-presets.d.ts +0 -13
  308. package/spec-cli/src/identity-presets.js +0 -138
  309. package/spec-cli/src/index.ts +0 -818
  310. package/spec-cli/src/init.ts +0 -258
  311. package/spec-cli/src/issues-cli.ts +0 -276
  312. package/spec-cli/src/issues.ts +0 -237
  313. package/spec-cli/src/layout.ts +0 -640
  314. package/spec-cli/src/lint.ts +0 -488
  315. package/spec-cli/src/listen.ts +0 -28
  316. package/spec-cli/src/localIssues.ts +0 -538
  317. package/spec-cli/src/materialize.ts +0 -417
  318. package/spec-cli/src/mentions.ts +0 -81
  319. package/spec-cli/src/opencode-headless.ts +0 -205
  320. package/spec-cli/src/pi-headless.ts +0 -153
  321. package/spec-cli/src/plugin-harness.ts +0 -157
  322. package/spec-cli/src/process-identity.ts +0 -207
  323. package/spec-cli/src/project-identity.ts +0 -73
  324. package/spec-cli/src/project-store.ts +0 -17
  325. package/spec-cli/src/pty-bridge.ts +0 -396
  326. package/spec-cli/src/pty-helper.mjs +0 -96
  327. package/spec-cli/src/pty-native-helper.mjs +0 -22
  328. package/spec-cli/src/ranker.ts +0 -191
  329. package/spec-cli/src/reaper.ts +0 -125
  330. package/spec-cli/src/resilience.ts +0 -41
  331. package/spec-cli/src/reviewSnapshot.ts +0 -28
  332. package/spec-cli/src/reviews.ts +0 -434
  333. package/spec-cli/src/runtime-guard.ts +0 -35
  334. package/spec-cli/src/runtime-ownership.ts +0 -88
  335. package/spec-cli/src/search.bench.mjs +0 -80
  336. package/spec-cli/src/search.ts +0 -63
  337. package/spec-cli/src/session-cursors.ts +0 -91
  338. package/spec-cli/src/session-execution.ts +0 -68
  339. package/spec-cli/src/session-files.ts +0 -121
  340. package/spec-cli/src/session-follow.ts +0 -182
  341. package/spec-cli/src/session-reparent.ts +0 -33
  342. package/spec-cli/src/session-timeline.ts +0 -247
  343. package/spec-cli/src/session-web.ts +0 -135
  344. package/spec-cli/src/sessions.ts +0 -3883
  345. package/spec-cli/src/sh.ts +0 -3
  346. package/spec-cli/src/slash-commands.ts +0 -318
  347. package/spec-cli/src/source-files.ts +0 -104
  348. package/spec-cli/src/specs.ts +0 -498
  349. package/spec-cli/src/supervise.ts +0 -245
  350. package/spec-cli/src/tree.ts +0 -118
  351. package/spec-cli/src/tsx-bin.ts +0 -18
  352. package/spec-cli/src/uninstall.ts +0 -171
  353. package/spec-cli/src/uploads.ts +0 -253
  354. package/spec-cli/src/worktree-sources.ts +0 -42
  355. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  356. package/spec-dashboard/dist/assets/App-B6IbFbGu.js +0 -2
  357. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  358. package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +0 -27
  359. package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +0 -2
  360. package/spec-dashboard/dist/assets/IssuesPage-BG9rNOgV.js +0 -1
  361. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  404. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  405. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  406. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  407. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  408. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  409. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  410. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  411. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  412. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  413. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  414. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  415. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  416. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  417. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  418. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  419. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  420. package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +0 -2
  421. package/spec-dashboard/dist/assets/Modal-B3EcgeA3.js +0 -1
  422. package/spec-dashboard/dist/assets/PageScroll-CNZOugWV.js +0 -1
  423. package/spec-dashboard/dist/assets/ProjectsPage-C2CRHmvs.js +0 -1
  424. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  425. package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +0 -40
  426. package/spec-dashboard/dist/assets/SessionWindow-1nYgZ4CL.js +0 -1
  427. package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +0 -1
  428. package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +0 -13
  429. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  430. package/spec-dashboard/dist/assets/data-SlvxNA2r.js +0 -1
  431. package/spec-dashboard/dist/assets/index-DLVeOHL-.js +0 -41
  432. package/spec-dashboard/dist/assets/index-DYLGfPjx.css +0 -1
  433. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  434. package/spec-dashboard/dist/assets/launch-yxCYr64x.js +0 -276
  435. package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +0 -1
  436. package/spec-dashboard/dist/index.html +0 -29
  437. package/spec-dashboard/src/reviewFilters.js +0 -322
  438. package/spec-dashboard/src/reviewQuery.js +0 -174
  439. package/spec-dashboard/src/session.js +0 -199
  440. package/spec-eval/src/cache.ts +0 -57
  441. package/spec-eval/src/cli.ts +0 -894
  442. package/spec-eval/src/evaltab.ts +0 -264
  443. package/spec-eval/src/filing.ts +0 -39
  444. package/spec-eval/src/freshness.ts +0 -836
  445. package/spec-eval/src/humanok.ts +0 -28
  446. package/spec-eval/src/scenariofresh.ts +0 -244
  447. package/spec-eval/src/scenarios.ts +0 -672
  448. package/spec-eval/src/sessioneval.ts +0 -2220
  449. package/spec-eval/src/sidecar.ts +0 -93
  450. package/spec-eval/src/timeline.ts +0 -54
  451. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  452. package/spec-forge/src/cache.ts +0 -66
  453. package/spec-forge/src/cli.ts +0 -99
  454. package/spec-forge/src/drivers/github.ts +0 -132
  455. package/spec-forge/src/drivers/gitlab.ts +0 -153
  456. package/spec-forge/src/drivers.ts +0 -73
  457. package/spec-forge/src/links.ts +0 -72
  458. package/spec-forge/src/needs-eval.ts +0 -29
  459. package/spec-forge/src/port.ts +0 -36
  460. package/spec-forge/src/resident.ts +0 -46
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  480. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  481. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  482. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  483. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  484. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec.md +0 -0
  485. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -0
  486. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -0
  487. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -0
  488. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -0
  489. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  490. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  491. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  492. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  493. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  494. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  495. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -0,0 +1,717 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { rebasePublishedSessions } from '@spexcode/spec-core';
5
+ import { buildBoard, spliceSessions } from './graphSnapshot.js';
6
+ import { headSha, repoRoot, requireGitWorkspace, withGitAbortSignal } from '@spexcode/spec-core';
7
+ import { listSessionIds, mainBranch, mainCheckout, readPublicRecordEntry, sessionArtifactPath, sessionRecordPath } from '@spexcode/spec-core';
8
+ import { boardThreads } from './issues.js';
9
+ import { resolveForgeHost } from '@spexcode/spec-forge/drivers';
10
+ import { residentForgeState } from '@spexcode/spec-forge/resident';
11
+ import { resolveProjectIdentity } from '@spexcode/spec-core';
12
+ import { readReviewSnapshot } from '@spexcode/spec-core';
13
+ import { sessionEvalProjection } from '@spexcode/spec-eval/sessioneval';
14
+ const DEBUG = process.env.SPEXCODE_BOARD_DEBUG === '1';
15
+ function textOrNull(path) {
16
+ try {
17
+ return readFileSync(path, 'utf8');
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return null;
22
+ throw error;
23
+ }
24
+ }
25
+ function gitDirOf(root) {
26
+ const dotgit = join(root, '.git');
27
+ try {
28
+ if (statSync(dotgit).isDirectory())
29
+ return dotgit;
30
+ }
31
+ catch (error) {
32
+ if (error.code === 'ENOENT')
33
+ return '';
34
+ throw error;
35
+ }
36
+ const dotgitText = textOrNull(dotgit);
37
+ if (dotgitText === null)
38
+ return '';
39
+ const match = dotgitText.match(/^gitdir:\s*(.+)$/m);
40
+ if (!match)
41
+ throw new Error(`unparseable gitdir pointer at ${dotgit}`);
42
+ const dir = match[1].trim();
43
+ return isAbsolute(dir) ? dir : resolve(root, dir);
44
+ }
45
+ function commonDirOf(root) {
46
+ const gitDir = gitDirOf(root);
47
+ if (!gitDir)
48
+ return '';
49
+ const common = textOrNull(join(gitDir, 'commondir'));
50
+ if (common === null)
51
+ return gitDir;
52
+ const dir = common.trim();
53
+ if (!dir)
54
+ throw new Error(`empty commondir pointer at ${join(gitDir, 'commondir')}`);
55
+ return isAbsolute(dir) ? dir : resolve(gitDir, dir);
56
+ }
57
+ function refSha(root, ref) {
58
+ if (/^[0-9a-f]{40}$/.test(ref))
59
+ return ref;
60
+ const common = commonDirOf(root);
61
+ if (!common)
62
+ return '<missing>';
63
+ const names = ref.startsWith('refs/') ? [ref] : [`refs/heads/${ref}`, `refs/remotes/${ref}`];
64
+ for (const name of names) {
65
+ const loose = textOrNull(join(common, name));
66
+ if (loose !== null)
67
+ return loose.trim() || '<missing>';
68
+ }
69
+ const packed = textOrNull(join(common, 'packed-refs'));
70
+ if (packed !== null) {
71
+ for (const line of packed.split('\n')) {
72
+ if (!line || line[0] === '#' || line[0] === '^')
73
+ continue;
74
+ const space = line.indexOf(' ');
75
+ if (space > 0 && names.includes(line.slice(space + 1).trim()))
76
+ return line.slice(0, space).trim();
77
+ }
78
+ }
79
+ return '<missing>';
80
+ }
81
+ function headRevision(root) {
82
+ if (!gitDirOf(root))
83
+ return '<missing>';
84
+ return headSha(root);
85
+ }
86
+ // Patrol validation needs a stronger contract than the overlay cache's best-effort mtime key: ctime catches
87
+ // a same-size edit whose mtime was restored, and every non-ENOENT read failure is loud instead of collapsing an
88
+ // unreadable subtree into a stable "unchanged" revision. A path that vanishes during the sample is represented
89
+ // by its absence and is rechecked by the producer fence.
90
+ function strictSpecTreeRevision(wtPath) {
91
+ const root = join(wtPath, '.spec');
92
+ try {
93
+ statSync(root);
94
+ }
95
+ catch (error) {
96
+ if (error.code === 'ENOENT')
97
+ return '';
98
+ throw error;
99
+ }
100
+ const parts = [];
101
+ const stack = [root];
102
+ while (stack.length) {
103
+ const dir = stack.pop();
104
+ let entries;
105
+ try {
106
+ entries = readdirSync(dir, { withFileTypes: true });
107
+ }
108
+ catch (error) {
109
+ if (error.code !== 'ENOENT')
110
+ throw error;
111
+ continue;
112
+ }
113
+ for (const entry of entries) {
114
+ const path = join(dir, entry.name);
115
+ if (entry.isDirectory()) {
116
+ stack.push(path);
117
+ continue;
118
+ }
119
+ try {
120
+ const stat = statSync(path);
121
+ parts.push(`${path}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`);
122
+ }
123
+ catch (error) {
124
+ if (error.code !== 'ENOENT')
125
+ throw error;
126
+ }
127
+ }
128
+ }
129
+ return parts.sort().join('\n');
130
+ }
131
+ function worktreeRevision(root) {
132
+ return {
133
+ root,
134
+ head: headRevision(root),
135
+ spec: strictSpecTreeRevision(root),
136
+ };
137
+ }
138
+ function digest(value) {
139
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
140
+ }
141
+ // The patrol's cheap authority check. These are exactly the mutable inputs a board assembly reads, folded
142
+ // without doing the assembly: HEAD/config + `.spec`, governed records and active governed worktree state,
143
+ // the issue-store carrier, and the already-resident session-eval projections. It intentionally does not call
144
+ // listSessions or sessionEvalProjections: verification must neither poll tmux again nor mint/schedule eval work.
145
+ // The hot/warm liveness signatures and eval generations remain graph-stream's canonical event-owned axes.
146
+ function sessionInputRevision() {
147
+ const ids = listSessionIds().sort();
148
+ // listSessions projects the structured record plus a one-line preview of the separately-stored originating
149
+ // prompt into each board row. Fold both exact artifacts so a missed store event cannot leave a stale
150
+ // label/preview forever — the prompt file is folded because the preview is derived from it.
151
+ const sessionInputs = ids.map((id) => [
152
+ id,
153
+ textOrNull(sessionRecordPath(id)),
154
+ textOrNull(sessionArtifactPath(id, 'prompt')),
155
+ ]);
156
+ const projections = digest(ids.map((id) => [id, sessionEvalProjection(id)]));
157
+ const activeRoots = [...new Set(ids.flatMap((id) => {
158
+ const entry = readPublicRecordEntry(id);
159
+ return entry.kind === 'ok' && entry.raw.governed && !entry.raw.archived ? [entry.raw.worktree_path] : [];
160
+ }))].sort();
161
+ return { sessions: digest(sessionInputs), projections, projectionIds: ids, activeRoots };
162
+ }
163
+ function boardInputRevision(board) {
164
+ const root = repoRoot();
165
+ requireGitWorkspace(root);
166
+ const session = sessionInputRevision();
167
+ // Durable active records are the current root set; a cached ordinary row may be stale and must not replace
168
+ // them. The one projection-only addition is explicit: listSessions republishes an archived-runtime hazard
169
+ // into the working set, and that marked row keeps its root monitored until cold state is repaired.
170
+ const activeRoots = [...new Set([
171
+ ...session.activeRoots,
172
+ ...(board?.sessions.flatMap((row) => row.archiveHazard ? [row.path] : []) ?? []),
173
+ ])].sort();
174
+ const main = mainCheckout();
175
+ const base = mainBranch();
176
+ const mainTip = refSha(main, base);
177
+ const nodeIds = (board?.nodes ?? []).map((node) => node.id);
178
+ const issuesStamp = boardThreads({ host: resolveForgeHost(), state: residentForgeState() }, nodeIds).stamp;
179
+ const worktreeParts = Object.fromEntries(activeRoots.map((worktree) => [worktree, digest(worktreeRevision(worktree))]));
180
+ const fullInputs = {
181
+ root: worktreeRevision(root),
182
+ config: [
183
+ [join(root, 'spexcode.json'), textOrNull(join(root, 'spexcode.json'))],
184
+ [join(root, 'spexcode.local.json'), textOrNull(join(root, 'spexcode.local.json'))],
185
+ [join(main, 'spexcode.json'), textOrNull(join(main, 'spexcode.json'))],
186
+ [join(main, 'spexcode.local.json'), textOrNull(join(main, 'spexcode.local.json'))],
187
+ ],
188
+ main: { root: main, branch: base, tip: mainTip },
189
+ worktrees: worktreeParts,
190
+ issuesStamp,
191
+ identity: resolveProjectIdentity(root, root),
192
+ };
193
+ const fullParts = Object.fromEntries(Object.entries(fullInputs).map(([key, value]) => [key, digest(value)]));
194
+ const full = digest(fullParts);
195
+ return {
196
+ full,
197
+ ...session,
198
+ combined: digest([full, session.sessions, session.projections]),
199
+ fullParts,
200
+ worktreeParts,
201
+ };
202
+ }
203
+ // Bind an input sample to what the completed board actually carries. The sample supplies the graph/session
204
+ // revision observed immediately after the producer; issue and projection values come from the returned board
205
+ // itself. Re-reading all inputs here would create a certification race: a blind change after the producer's
206
+ // after-sample could otherwise be mistaken for an input the already-built board contains.
207
+ function revisionCarriedByBoard(sample, board) {
208
+ const fullParts = { ...sample.fullParts, issuesStamp: digest(board.issuesStamp) };
209
+ const full = digest(fullParts);
210
+ const boardProjections = new Map(board.sessions.map((session) => [session.id, session.evalSummary ?? null]));
211
+ const projections = digest(sample.projectionIds.map((id) => [id, boardProjections.get(id) ?? null]));
212
+ return {
213
+ full,
214
+ sessions: sample.sessions,
215
+ projections,
216
+ combined: digest([full, sample.sessions, projections]),
217
+ fullParts,
218
+ worktreeParts: sample.worktreeParts,
219
+ projectionIds: sample.projectionIds,
220
+ };
221
+ }
222
+ // A sessions splice reuses its base topology. It may sample fresh record/projection inputs, but it must never
223
+ // certify that those old nodes carry a full revision sampled after the base was built.
224
+ function revisionCarriedBySessionSplice(base, sample, board, stable) {
225
+ const boardProjections = new Map(board.sessions.map((session) => [session.id, session.evalSummary ?? null]));
226
+ const projections = stable
227
+ ? digest(sample.projectionIds.map((id) => [id, boardProjections.get(id) ?? null]))
228
+ : sample.projections;
229
+ return {
230
+ full: base.full,
231
+ sessions: sample.sessions,
232
+ projections,
233
+ combined: digest([base.full, sample.sessions, projections]),
234
+ fullParts: base.fullParts,
235
+ worktreeParts: base.worktreeParts,
236
+ projectionIds: sample.projectionIds,
237
+ };
238
+ }
239
+ // The full producer's topology may be newer than the last published session projection. When completion
240
+ // re-bases that already-visible projection onto the new topology, keep the full carrier from the producer and
241
+ // only the sessions/projections carrier from the published rows. Do not sample current inputs here: that would
242
+ // certify a write that neither producer actually carried.
243
+ function revisionCarriedByPublishedSessionRebase(full, published) {
244
+ return {
245
+ full: full.full,
246
+ sessions: published.sessions,
247
+ projections: published.projections,
248
+ combined: digest([full.full, published.sessions, published.projections]),
249
+ fullParts: full.fullParts,
250
+ worktreeParts: full.worktreeParts,
251
+ projectionIds: published.projectionIds,
252
+ };
253
+ }
254
+ function carrySubtractiveWorktrees(base, activeRoots) {
255
+ const active = new Set(activeRoots);
256
+ const known = Object.keys(base.worktreeParts);
257
+ if (activeRoots.some((root) => !(root in base.worktreeParts)))
258
+ return { revision: base, added: true };
259
+ if (known.every((root) => active.has(root)))
260
+ return { revision: base, added: false };
261
+ const worktreeParts = Object.fromEntries(Object.entries(base.worktreeParts).filter(([root]) => active.has(root)));
262
+ const fullParts = { ...base.fullParts, worktrees: digest(worktreeParts) };
263
+ const full = digest(fullParts);
264
+ return {
265
+ revision: {
266
+ ...base,
267
+ full,
268
+ fullParts,
269
+ worktreeParts,
270
+ combined: digest([full, base.sessions, base.projections]),
271
+ },
272
+ added: false,
273
+ };
274
+ }
275
+ function isPureSubtractiveWorktreeMove(base, sample) {
276
+ const moved = Object.keys(sample.fullParts).filter((key) => sample.fullParts[key] !== base.fullParts[key]);
277
+ if (moved.length !== 1 || moved[0] !== 'worktrees')
278
+ return false;
279
+ const current = Object.keys(sample.worktreeParts);
280
+ const previous = Object.keys(base.worktreeParts);
281
+ return current.length < previous.length
282
+ && current.every((root) => sample.worktreeParts[root] === base.worktreeParts[root]);
283
+ }
284
+ // a build slower than this is LOGGED, never silently tolerated — the fail-loud regression alarm. Sized
285
+ // above a warm build (~sub-second once the fs walks yield) but below the cold two-walk first build, so a
286
+ // genuinely-degraded hot path shouts while an ordinary cold start stays quiet-ish.
287
+ const BUDGET_MS = Number(process.env.SPEXCODE_BOARD_BUDGET_MS || 1500);
288
+ // a build that NEVER settles is a different animal from a slow one: `inflight` clears only in the finally
289
+ // below, so a never-settling buildBoard() would pin the single-flight forever — every later read (even of a
290
+ // perfectly good cached board) short-circuits into the pinned promise before `valid` is consulted,
291
+ // invalidation can't help, no log ever fires, and only a restart cures it (the live wedge: hung git
292
+ // children → /api/graph 503 forever, silently). So the build races a generous watchdog that REJECTS loudly;
293
+ // the rejection flows through the SAME finally → inflight clears → the next read retries fresh. Sitting at
294
+ // the single-flight boundary, this one wall bounds every never-settle cause — including ones with no child
295
+ // process at all (fs/promises under libuv threadpool starvation); git.ts's per-child timeouts merely make
296
+ // the common cause die sooner. Generous: well above the slowest legitimate cold build, so it only ever
297
+ // fires on a genuine wedge.
298
+ const BUILD_TIMEOUT_MS = Number(process.env.SPEXCODE_BOARD_BUILD_TIMEOUT_MS || 120000);
299
+ const RETRY_BACKOFF_MS = Number(process.env.SPEXCODE_BOARD_RETRY_BACKOFF_MS || 1000);
300
+ const BACKGROUND_START_DELAY_MS = Number(process.env.SPEXCODE_BOARD_BACKGROUND_START_DELAY_MS || 300);
301
+ let cached = null; // last completed build; served while `dirty === 'none'`
302
+ let cachedJson = null; // JSON.stringify(cached), serialized ONCE per build (see getBoardJson)
303
+ let cachedRevision = null; // input revision represented by `cached`
304
+ let dirty = 'full'; // no cached board yet → the first read builds fully
305
+ let inflight = null;
306
+ let sessionFlight = null;
307
+ let sessionOwed = false;
308
+ let sessionGeneration = 0;
309
+ let sessionProjectionPublication = 0;
310
+ let topologyGeneration = 0;
311
+ let gen = 0; // bumped on invalidation so patrol validation cannot certify across an event
312
+ let retryAt = 0;
313
+ let lastFailure = null;
314
+ // mark the cache stale at a SCOPE. Called by every board-stream freshness source (see
315
+ // boardStream.fireChanged), so a real change forces the next getBoard() to rebuild while a quiet poll storm
316
+ // keeps hitting the cache. `dirty` is the structural producer's scope; `sessionOwed` is intentionally not
317
+ // folded into it, because full+sessions owes both a full convergence and a cheap first projection.
318
+ function mergeDirty(scope) {
319
+ if (scope === 'full' || dirty === 'full')
320
+ dirty = 'full';
321
+ else
322
+ dirty = 'sessions';
323
+ }
324
+ export function invalidateBoard(scope = 'full') {
325
+ gen++;
326
+ if (scope === 'sessions') {
327
+ sessionOwed = true;
328
+ sessionGeneration++;
329
+ }
330
+ mergeDirty(scope);
331
+ retryAt = 0;
332
+ lastFailure = null;
333
+ }
334
+ function traceCacheCommit(scope, startedAt) {
335
+ if (!DEBUG)
336
+ return;
337
+ const at = Date.now();
338
+ console.warn(`spec-cli: graph cache ${JSON.stringify({ at, stage: 'cache-commit', scope, buildMs: at - startedAt })}`);
339
+ }
340
+ function startSessionSplice() {
341
+ if (!cached || !cachedRevision || !sessionOwed)
342
+ return null;
343
+ if (sessionFlight)
344
+ return sessionFlight;
345
+ sessionOwed = false;
346
+ if (dirty === 'sessions')
347
+ dirty = 'none';
348
+ let timedOut = false;
349
+ const startedAt = Date.now();
350
+ let watchdog;
351
+ const timeoutError = () => new Error(`graph session splice did not settle within ${BUILD_TIMEOUT_MS}ms`);
352
+ const producer = Promise.resolve().then(async () => {
353
+ // A full completion may replace the topology while listSessions is in flight. Rebase before publishing;
354
+ // the splice is a projection over whatever last-good structure is current, never a whole-board write race.
355
+ while (true) {
356
+ const base = cached, revision = cachedRevision, generation = topologyGeneration;
357
+ if (!base || !revision)
358
+ throw new Error('graph session splice lost its cached topology');
359
+ const before = sessionInputRevision();
360
+ const board = await spliceSessions(base);
361
+ const after = sessionInputRevision();
362
+ if (base !== cached || revision !== cachedRevision || generation !== topologyGeneration)
363
+ continue;
364
+ const stable = before.sessions === after.sessions && before.projections === after.projections &&
365
+ JSON.stringify(before.activeRoots) === JSON.stringify(after.activeRoots);
366
+ const projectedRoots = [...new Set(board.sessions.map((row) => row.path))].sort();
367
+ const rootTransition = carrySubtractiveWorktrees(revision, projectedRoots);
368
+ const carried = stable ? rootTransition.revision : revision;
369
+ cached = board;
370
+ cachedJson = null;
371
+ cachedRevision = revisionCarriedBySessionSplice(carried, before, board, stable);
372
+ sessionProjectionPublication++;
373
+ if (rootTransition.added)
374
+ mergeDirty('full');
375
+ if (!stable) {
376
+ sessionOwed = true;
377
+ sessionGeneration++;
378
+ mergeDirty('sessions');
379
+ }
380
+ return board;
381
+ }
382
+ })
383
+ .then((board) => {
384
+ if (timedOut)
385
+ throw timeoutError();
386
+ traceCacheCommit('sessions', startedAt);
387
+ return board;
388
+ })
389
+ .catch((error) => {
390
+ sessionOwed = true;
391
+ mergeDirty('sessions');
392
+ throw error;
393
+ });
394
+ let settle;
395
+ settle = producer.finally(() => {
396
+ clearTimeout(watchdog);
397
+ if (sessionFlight?.settle === settle)
398
+ sessionFlight = null;
399
+ });
400
+ const wait = new Promise((resolve, reject) => {
401
+ watchdog = setTimeout(() => {
402
+ timedOut = true;
403
+ console.warn(`spec-cli: graph session splice did not settle within ${BUILD_TIMEOUT_MS}ms`);
404
+ reject(timeoutError());
405
+ }, BUILD_TIMEOUT_MS);
406
+ watchdog.unref?.();
407
+ settle.then((board) => { if (!timedOut)
408
+ resolve(board); }, (error) => { if (!timedOut)
409
+ reject(error); });
410
+ });
411
+ const flight = { wait, settle };
412
+ sessionFlight = flight;
413
+ void wait.catch(() => { });
414
+ void settle.catch(() => { });
415
+ return flight;
416
+ }
417
+ function startBuild(mode = 'dirty') {
418
+ if (inflight)
419
+ return inflight;
420
+ if (Date.now() < retryAt)
421
+ return null;
422
+ const controller = new AbortController();
423
+ let watchdog;
424
+ let timedOut = false;
425
+ let built = false;
426
+ let buildStartedAt = 0;
427
+ let buildScope = 'full';
428
+ let buildFullStable = true;
429
+ let buildSessionsStable = true;
430
+ let buildSessionGeneration = sessionGeneration;
431
+ let buildSessionProjectionPublication = sessionProjectionPublication;
432
+ let buildStartedWithSessionOwed = false;
433
+ let buildAddedSessionRoot = false;
434
+ let completedRevision = null;
435
+ // Do not invoke the producer inline. buildBoard() has an asynchronous signature but performs a sizeable
436
+ // synchronous setup before its first await (Promise.all evaluates its arguments immediately). A stale HTTP
437
+ // caller must be able to return its last-good bytes before that setup runs; first-cold/fresh callers still
438
+ // await the same deferred promise below.
439
+ let resolveBuild;
440
+ let rejectBuild;
441
+ const build = new Promise((resolve, reject) => {
442
+ resolveBuild = resolve;
443
+ rejectBuild = reject;
444
+ });
445
+ // Give a stale HTTP response a turn to flush before the producer's synchronous setup occupies the event
446
+ // loop. Fresh callers simply absorb this small scheduling window while waiting on the same flight.
447
+ setTimeout(() => {
448
+ if (controller.signal.aborted) {
449
+ rejectBuild(Object.assign(new Error('graph build aborted before start'), { name: 'AbortError' }));
450
+ return;
451
+ }
452
+ try {
453
+ Promise.resolve(withGitAbortSignal(controller.signal, async () => {
454
+ const prev = cached;
455
+ const anchor = cachedRevision;
456
+ const sampledGen = gen;
457
+ const before = await boardInputRevision(prev);
458
+ if (controller.signal.aborted)
459
+ throw Object.assign(new Error('graph build aborted before producer start'), { name: 'AbortError' });
460
+ // ONE rule for every refresh: the domain a producer runs is DERIVED from the inputs that actually
461
+ // moved, never assigned by whoever signalled. A watcher names the leaf it saw, not what the board
462
+ // reads — a generated harness artifact rewritten inside a live worktree, or a linked worktree no
463
+ // governed record names, moves no board input at all, and used to buy a whole structural assembly
464
+ // anyway. The revision sampled here is the complete full-domain contract, so an unmoved full
465
+ // obligation is DISCHARGED without assembly. The session projection is deliberately NOT
466
+ // dischargeable: liveness lives on graph-stream's poller axis, outside this revision, so a claimed
467
+ // sessions obligation always survives and takes its splice. A sample taken after the write is what
468
+ // the watcher event was about; an invalidation arriving after the sample keeps its own dirty window.
469
+ if (prev && anchor && gen === sampledGen) {
470
+ const fullMoved = before.full !== anchor.full;
471
+ const subtractiveWorktreeMove = fullMoved && isPureSubtractiveWorktreeMove(anchor, before);
472
+ const structuralMoved = fullMoved && !subtractiveWorktreeMove;
473
+ const projectionMoved = before.sessions !== anchor.sessions || before.projections !== anchor.projections;
474
+ // A projection move nobody signalled is this validation doing the patrol's repair job.
475
+ if ((projectionMoved || subtractiveWorktreeMove) && !sessionOwed && dirty !== 'sessions') {
476
+ gen++;
477
+ sessionOwed = true;
478
+ sessionGeneration++;
479
+ }
480
+ const owed = structuralMoved ? 'full' : (sessionOwed || dirty === 'sessions') ? 'sessions' : 'none';
481
+ if (DEBUG && owed !== 'none') {
482
+ const moved = Object.keys(before.fullParts).filter((key) => before.fullParts[key] !== anchor.fullParts[key]);
483
+ console.warn(`spec-cli: graph refresh revision moved — signalled=${dirty} scope=${owed} inputs=[${moved.join(', ')}]`);
484
+ }
485
+ else if (DEBUG && dirty !== 'none') {
486
+ console.warn(`spec-cli: graph refresh discharged a ${dirty} signal — no graph input moved`);
487
+ }
488
+ if (structuralMoved)
489
+ dirty = 'full';
490
+ else if (sessionOwed || dirty === 'sessions')
491
+ dirty = 'sessions';
492
+ // Discharging CONSUMES the claim. Leaving it standing would make every later read re-enter this
493
+ // validation, hold the board permanently stale/refreshing, and never converge.
494
+ else {
495
+ dirty = 'none';
496
+ return prev;
497
+ }
498
+ }
499
+ const sessionsOnly = dirty === 'sessions' && prev !== null;
500
+ buildScope = sessionsOnly ? 'sessions' : 'full';
501
+ // Consume the scope this producer is satisfying. Invalidation after this point starts a NEW dirty
502
+ // window with its own domain: a session completion during a long full build owes one splice, not
503
+ // another full build. The occupied `inflight` slot keeps fresh/stale readers joined while dirty is clean.
504
+ dirty = 'none';
505
+ if (sessionsOnly)
506
+ sessionOwed = false;
507
+ built = true;
508
+ buildSessionGeneration = sessionGeneration;
509
+ buildSessionProjectionPublication = sessionProjectionPublication;
510
+ buildStartedWithSessionOwed = sessionOwed;
511
+ buildStartedAt = Date.now();
512
+ const board = await (sessionsOnly ? spliceSessions(prev) : buildBoard());
513
+ const after = await boardInputRevision(prev);
514
+ const movedFull = Object.keys(after.fullParts)
515
+ .filter((key) => after.fullParts[key] !== before.fullParts[key]);
516
+ // The first build can initialize the resident forge carrier that it then returns. That movement is
517
+ // stable only when the finished board carries the after-sample's exact issue stamp. Every other cold
518
+ // movement remains dirty just like a warm one; cold start is not a blanket race exemption.
519
+ const coldIssueInitialization = prev === null
520
+ && movedFull.every((key) => key === 'issuesStamp')
521
+ && after.fullParts.issuesStamp === digest(board.issuesStamp);
522
+ buildFullStable = before.full === after.full || coldIssueInitialization;
523
+ buildSessionsStable = before.sessions === after.sessions;
524
+ if (DEBUG && (!buildFullStable || !buildSessionsStable)) {
525
+ console.warn(`spec-cli: graph inputs moved during ${buildScope} producer — next=${buildFullStable ? 'sessions' : 'full'} inputs=[${movedFull.join(', ')}] sessions=${buildSessionsStable ? 'stable' : 'moved'}`);
526
+ }
527
+ // The anchor must name the sample this board is KNOWN to have read, because validation now discharges
528
+ // an unmoved obligation for every refresh, not only the patrol's. An input that moved DURING the
529
+ // producer leaves the board built from the pre-move value while the after-sample already names the
530
+ // post-move one; anchoring on `after` would let the next validation discharge the re-owed dirty scope
531
+ // against a revision this board never carried, and the cache would converge on nothing. So a moved
532
+ // half anchors on `before` — the conservative direction, which costs at most one extra rebuild and can
533
+ // never certify unread bytes.
534
+ if (sessionsOnly && anchor) {
535
+ const projectedRoots = [...new Set(board.sessions.map((row) => row.path))].sort();
536
+ const rootTransition = carrySubtractiveWorktrees(anchor, projectedRoots);
537
+ buildAddedSessionRoot = rootTransition.added;
538
+ completedRevision = revisionCarriedBySessionSplice(rootTransition.revision, before, board, buildFullStable && buildSessionsStable && !rootTransition.added);
539
+ }
540
+ else {
541
+ completedRevision = revisionCarriedByBoard(buildFullStable && buildSessionsStable ? after : before, board);
542
+ }
543
+ return board;
544
+ }))
545
+ .then(resolveBuild, rejectBuild);
546
+ }
547
+ catch (error) {
548
+ rejectBuild(error);
549
+ }
550
+ }, mode === 'patrol' ? 0 : BACKGROUND_START_DELAY_MS).unref?.();
551
+ const timeoutError = () => new Error(`graph build did not settle within ${BUILD_TIMEOUT_MS}ms`);
552
+ // `settle` owns the real builder. The watchdog only rejects `wait`; the slot remains occupied until this
553
+ // promise settles, so a next read can never overlap an abandoned git/fs build.
554
+ let settle;
555
+ settle = build.then(async (board) => {
556
+ if (timedOut)
557
+ throw timeoutError();
558
+ if (built) {
559
+ // A full build's session snapshot may predate a session projection that clients already saw. Rebase those
560
+ // published rows in memory on the new topology. This completion path must stay bounded: a later/unpublished
561
+ // session generation remains owed to the independent cheap splice rather than making full wait for quiet.
562
+ const publishedDuringBuild = sessionProjectionPublication !== buildSessionProjectionPublication;
563
+ if (buildScope === 'full' && publishedDuringBuild && cached && cachedRevision) {
564
+ board = rebasePublishedSessions(board, cached);
565
+ completedRevision = revisionCarriedByPublishedSessionRebase(completedRevision, cachedRevision);
566
+ }
567
+ const sessionStillOwed = sessionOwed || (!publishedDuringBuild && (buildStartedWithSessionOwed || !buildSessionsStable || buildSessionGeneration !== sessionGeneration));
568
+ if (sessionStillOwed) {
569
+ sessionOwed = true;
570
+ mergeDirty('sessions');
571
+ }
572
+ cached = board;
573
+ cachedJson = null;
574
+ cachedRevision = completedRevision;
575
+ if (buildScope === 'full')
576
+ topologyGeneration++;
577
+ else
578
+ sessionProjectionPublication++;
579
+ if (!buildFullStable || buildAddedSessionRoot)
580
+ mergeDirty('full');
581
+ traceCacheCommit(buildScope, buildStartedAt);
582
+ }
583
+ retryAt = 0;
584
+ lastFailure = null;
585
+ return board;
586
+ }).catch((error) => {
587
+ const failure = error instanceof Error ? error : new Error(String(error));
588
+ if (built)
589
+ mergeDirty(buildScope);
590
+ lastFailure = failure;
591
+ retryAt = Date.now() + RETRY_BACKOFF_MS;
592
+ console.warn(`spec-cli: /api/graph build failed — ${failure.message}`);
593
+ throw failure;
594
+ }).finally(() => {
595
+ clearTimeout(watchdog);
596
+ if (inflight?.settle === settle)
597
+ inflight = null;
598
+ const ms = built ? Date.now() - buildStartedAt : 0;
599
+ if (built && ms > BUDGET_MS)
600
+ console.warn(`spec-cli: /api/graph build took ${ms}ms (budget ${BUDGET_MS}ms) — ${buildScope} path is slow`);
601
+ });
602
+ const wait = new Promise((resolve, reject) => {
603
+ watchdog = setTimeout(() => {
604
+ timedOut = true;
605
+ console.warn(`spec-cli: /api/graph build did not settle within ${BUILD_TIMEOUT_MS}ms — aborting the single-flight build`);
606
+ controller.abort();
607
+ reject(timeoutError());
608
+ }, BUILD_TIMEOUT_MS);
609
+ watchdog.unref?.();
610
+ settle.then((board) => {
611
+ if (!timedOut)
612
+ resolve(board);
613
+ }, (error) => {
614
+ if (!timedOut)
615
+ reject(error);
616
+ });
617
+ });
618
+ const flight = { wait, settle };
619
+ inflight = flight;
620
+ // Background stale readers intentionally do not await these promises. Observe both rejection paths so a
621
+ // failed build is loud without becoming an unhandled rejection.
622
+ void wait.catch(() => { });
623
+ void settle.catch(() => { });
624
+ return flight;
625
+ }
626
+ export function getBoard() {
627
+ // A clean-looking cache can be under patrol validation. Fresh readers join that flight before taking the
628
+ // cache fast path, otherwise one can return stale bytes while the flight is discovering a missed change.
629
+ if (inflight)
630
+ return inflight.wait;
631
+ if (dirty === 'full') {
632
+ const flight = startBuild('dirty');
633
+ if (flight)
634
+ return flight.wait;
635
+ }
636
+ if (sessionFlight)
637
+ return sessionFlight.wait;
638
+ if (dirty === 'none' && cached)
639
+ return Promise.resolve(cached);
640
+ const flight = startBuild('dirty');
641
+ if (flight)
642
+ return flight.wait;
643
+ return Promise.reject(lastFailure ?? new Error('graph build retry is temporarily backing off'));
644
+ }
645
+ export async function waitForPublishedRevision(required, publication) {
646
+ while (publication.revision() < required) {
647
+ publication.invalidate();
648
+ await publication.wait();
649
+ }
650
+ }
651
+ // A board flight can have captured a resident forge slice before its reconcile publishes a newer revision.
652
+ // That older flight may settle, but cannot discharge this request: its invalidation stays owed and the fence
653
+ // consumes the next publication instead of letting a fresh /api/issues read return the old review snapshot.
654
+ export async function getBoardForForgeRevision(required) {
655
+ await waitForPublishedRevision(required, {
656
+ revision: () => readReviewSnapshot().forgeRevision,
657
+ invalidate: () => invalidateBoard('full'),
658
+ wait: async () => {
659
+ const current = inflight;
660
+ if (current)
661
+ await current.settle;
662
+ else
663
+ await getBoard();
664
+ },
665
+ });
666
+ return cached ?? getBoard();
667
+ }
668
+ // Delta delivery may choose the sessions obligation while a route-owned full is still in flight. Starting
669
+ // the full first preserves structural convergence; returning the splice first keeps the lifecycle surface live.
670
+ export function getBoardForSessionRefresh() {
671
+ if (cached && sessionOwed) {
672
+ if (!inflight && dirty === 'full')
673
+ startBuild('dirty');
674
+ const flight = startSessionSplice();
675
+ if (flight)
676
+ return flight.wait;
677
+ }
678
+ return getBoard();
679
+ }
680
+ // The delta-gated cold tick calls this instead of invalidating. Equal inputs resolve to the cached object;
681
+ // changed inputs repair through the same flight and full producer as a watcher-owned invalidation.
682
+ export function patrolBoard() {
683
+ if (!cached)
684
+ return getBoard();
685
+ const flight = startBuild('patrol');
686
+ if (flight)
687
+ return flight.wait;
688
+ return Promise.reject(lastFailure ?? new Error('graph patrol retry is temporarily backing off'));
689
+ }
690
+ export async function readBoard(consistency = 'fresh') {
691
+ if (consistency === 'stale-ok' && cached) {
692
+ const stale = dirty !== 'none' || inflight !== null || sessionFlight !== null || sessionOwed;
693
+ // A held session splice already owns this refresh. Returning stale bytes must not manufacture a full
694
+ // producer beside it; a real full obligation still starts its one structural producer independently.
695
+ const flight = inflight
696
+ ?? (dirty === 'full' ? startBuild('dirty') : null)
697
+ ?? sessionFlight
698
+ ?? (sessionOwed ? startSessionSplice() : null)
699
+ ?? (dirty === 'sessions' ? startBuild('dirty') : null);
700
+ return { board: cached, freshness: stale ? 'stale' : 'fresh', refreshing: !!flight, ...(lastFailure ? { error: lastFailure.message } : {}) };
701
+ }
702
+ const board = await getBoard();
703
+ return { board, freshness: 'fresh', refreshing: false };
704
+ }
705
+ // the SERIALIZED board for the /api/graph route — JSON.stringify runs ONCE per build, not once per poll,
706
+ // so a poll storm of cache hits costs zero serialization CPU (only the etag hash for the 304 path). The SSE
707
+ // path still takes the object (getBoard) because it decomposes it into delta units ([[graph-delta]]).
708
+ export async function getBoardJson(consistency = 'fresh') {
709
+ const result = await readBoard(consistency);
710
+ const board = result.board;
711
+ if (board === cached && cachedJson !== null)
712
+ return { ...result, json: cachedJson };
713
+ const json = JSON.stringify(board);
714
+ if (board === cached)
715
+ cachedJson = json; // memoize only the CURRENT build's serialization
716
+ return { ...result, json };
717
+ }