spexcode 0.6.1 → 0.6.5

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