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
@@ -1,710 +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
- worktreeParts: Record<string, string>
31
- projectionIds: string[]
32
- }
33
- type SessionInputRevision = Pick<BoardInputRevision, 'sessions' | 'projections' | 'projectionIds'> & { activeRoots: string[] }
34
- const DEBUG = process.env.SPEXCODE_BOARD_DEBUG === '1'
35
-
36
- function textOrNull(path: string): string | null {
37
- try { return readFileSync(path, 'utf8') }
38
- catch (error) {
39
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
40
- throw error
41
- }
42
- }
43
-
44
- function gitDirOf(root: string): string {
45
- const dotgit = join(root, '.git')
46
- try { if (statSync(dotgit).isDirectory()) return dotgit }
47
- catch (error) {
48
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return ''
49
- throw error
50
- }
51
- const dotgitText = textOrNull(dotgit)
52
- if (dotgitText === null) return ''
53
- const match = dotgitText.match(/^gitdir:\s*(.+)$/m)
54
- if (!match) throw new Error(`unparseable gitdir pointer at ${dotgit}`)
55
- const dir = match[1].trim()
56
- return isAbsolute(dir) ? dir : resolve(root, dir)
57
- }
58
-
59
- function commonDirOf(root: string): string {
60
- const gitDir = gitDirOf(root)
61
- if (!gitDir) return ''
62
- const common = textOrNull(join(gitDir, 'commondir'))
63
- if (common === null) return gitDir
64
- const dir = common.trim()
65
- if (!dir) throw new Error(`empty commondir pointer at ${join(gitDir, 'commondir')}`)
66
- return isAbsolute(dir) ? dir : resolve(gitDir, dir)
67
- }
68
-
69
- function refSha(root: string, ref: string): string {
70
- if (/^[0-9a-f]{40}$/.test(ref)) return ref
71
- const common = commonDirOf(root)
72
- if (!common) return '<missing>'
73
- const names = ref.startsWith('refs/') ? [ref] : [`refs/heads/${ref}`, `refs/remotes/${ref}`]
74
- for (const name of names) {
75
- const loose = textOrNull(join(common, name))
76
- if (loose !== null) return loose.trim() || '<missing>'
77
- }
78
- const packed = textOrNull(join(common, 'packed-refs'))
79
- if (packed !== null) {
80
- for (const line of packed.split('\n')) {
81
- if (!line || line[0] === '#' || line[0] === '^') continue
82
- const space = line.indexOf(' ')
83
- if (space > 0 && names.includes(line.slice(space + 1).trim())) return line.slice(0, space).trim()
84
- }
85
- }
86
- return '<missing>'
87
- }
88
-
89
- function headRevision(root: string): string {
90
- if (!gitDirOf(root)) return '<missing>'
91
- return headSha(root)
92
- }
93
-
94
- // Patrol validation needs a stronger contract than the overlay cache's best-effort mtime key: ctime catches
95
- // a same-size edit whose mtime was restored, and every non-ENOENT read failure is loud instead of collapsing an
96
- // unreadable subtree into a stable "unchanged" revision. A path that vanishes during the sample is represented
97
- // by its absence and is rechecked by the producer fence.
98
- function strictSpecTreeRevision(wtPath: string): string {
99
- const root = join(wtPath, '.spec')
100
- try { statSync(root) }
101
- catch (error) {
102
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return ''
103
- throw error
104
- }
105
- const parts: string[] = []
106
- const stack = [root]
107
- while (stack.length) {
108
- const dir = stack.pop()!
109
- let entries
110
- try { entries = readdirSync(dir, { withFileTypes: true }) }
111
- catch (error) {
112
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
113
- continue
114
- }
115
- for (const entry of entries) {
116
- const path = join(dir, entry.name)
117
- if (entry.isDirectory()) { stack.push(path); continue }
118
- try {
119
- const stat = statSync(path)
120
- parts.push(`${path}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`)
121
- } catch (error) {
122
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
123
- }
124
- }
125
- }
126
- return parts.sort().join('\n')
127
- }
128
-
129
- function worktreeRevision(root: string): unknown {
130
- return {
131
- root,
132
- head: headRevision(root),
133
- spec: strictSpecTreeRevision(root),
134
- }
135
- }
136
-
137
- function digest(value: unknown): string {
138
- return createHash('sha256').update(JSON.stringify(value)).digest('hex')
139
- }
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(): 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
- ] as const)
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
-
164
- function boardInputRevision(board: Board | null): BoardInputRevision {
165
- const root = repoRoot()
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
-
204
- // Bind an input sample to what the completed board actually carries. The sample supplies the graph/session
205
- // revision observed immediately after the producer; issue and projection values come from the returned board
206
- // itself. Re-reading all inputs here would create a certification race: a blind change after the producer's
207
- // after-sample could otherwise be mistaken for an input the already-built board contains.
208
- function revisionCarriedByBoard(sample: BoardInputRevision, board: Board): BoardInputRevision {
209
- const fullParts = { ...sample.fullParts, issuesStamp: digest(board.issuesStamp) }
210
- const full = digest(fullParts)
211
- const boardProjections = new Map(board.sessions.map((session) => [session.id, session.evalSummary ?? null]))
212
- const projections = digest(sample.projectionIds.map((id) => [id, boardProjections.get(id) ?? null]))
213
- return {
214
- full,
215
- sessions: sample.sessions,
216
- projections,
217
- combined: digest([full, sample.sessions, projections]),
218
- fullParts,
219
- worktreeParts: sample.worktreeParts,
220
- projectionIds: sample.projectionIds,
221
- }
222
- }
223
-
224
- // A sessions splice reuses its base topology. It may sample fresh record/projection inputs, but it must never
225
- // certify that those old nodes carry a full revision sampled after the base was built.
226
- function revisionCarriedBySessionSplice(
227
- base: BoardInputRevision,
228
- sample: Pick<SessionInputRevision, 'sessions' | 'projections' | 'projectionIds'>,
229
- board: Board,
230
- stable: boolean,
231
- ): BoardInputRevision {
232
- const boardProjections = new Map(board.sessions.map((session) => [session.id, session.evalSummary ?? null]))
233
- const projections = stable
234
- ? digest(sample.projectionIds.map((id) => [id, boardProjections.get(id) ?? null]))
235
- : sample.projections
236
- return {
237
- full: base.full,
238
- sessions: sample.sessions,
239
- projections,
240
- combined: digest([base.full, sample.sessions, projections]),
241
- fullParts: base.fullParts,
242
- worktreeParts: base.worktreeParts,
243
- projectionIds: sample.projectionIds,
244
- }
245
- }
246
-
247
- // The full producer's topology may be newer than the last published session projection. When completion
248
- // re-bases that already-visible projection onto the new topology, keep the full carrier from the producer and
249
- // only the sessions/projections carrier from the published rows. Do not sample current inputs here: that would
250
- // certify a write that neither producer actually carried.
251
- function revisionCarriedByPublishedSessionRebase(full: BoardInputRevision, published: BoardInputRevision): BoardInputRevision {
252
- return {
253
- full: full.full,
254
- sessions: published.sessions,
255
- projections: published.projections,
256
- combined: digest([full.full, published.sessions, published.projections]),
257
- fullParts: full.fullParts,
258
- worktreeParts: full.worktreeParts,
259
- projectionIds: published.projectionIds,
260
- }
261
- }
262
-
263
- function carrySubtractiveWorktrees(base: BoardInputRevision, activeRoots: readonly string[]): { revision: BoardInputRevision; added: boolean } {
264
- const active = new Set(activeRoots)
265
- const known = Object.keys(base.worktreeParts)
266
- if (activeRoots.some((root) => !(root in base.worktreeParts))) return { revision: base, added: true }
267
- if (known.every((root) => active.has(root))) return { revision: base, added: false }
268
- const worktreeParts = Object.fromEntries(Object.entries(base.worktreeParts).filter(([root]) => active.has(root)))
269
- const fullParts = { ...base.fullParts, worktrees: digest(worktreeParts) }
270
- const full = digest(fullParts)
271
- return {
272
- revision: {
273
- ...base,
274
- full,
275
- fullParts,
276
- worktreeParts,
277
- combined: digest([full, base.sessions, base.projections]),
278
- },
279
- added: false,
280
- }
281
- }
282
-
283
- function isPureSubtractiveWorktreeMove(base: BoardInputRevision, sample: BoardInputRevision): boolean {
284
- const moved = Object.keys(sample.fullParts).filter((key) => sample.fullParts[key] !== base.fullParts[key])
285
- if (moved.length !== 1 || moved[0] !== 'worktrees') return false
286
- const current = Object.keys(sample.worktreeParts)
287
- const previous = Object.keys(base.worktreeParts)
288
- return current.length < previous.length
289
- && current.every((root) => sample.worktreeParts[root] === base.worktreeParts[root])
290
- }
291
-
292
- // a build slower than this is LOGGED, never silently tolerated — the fail-loud regression alarm. Sized
293
- // above a warm build (~sub-second once the fs walks yield) but below the cold two-walk first build, so a
294
- // genuinely-degraded hot path shouts while an ordinary cold start stays quiet-ish.
295
- const BUDGET_MS = Number(process.env.SPEXCODE_BOARD_BUDGET_MS || 1500)
296
-
297
- // a build that NEVER settles is a different animal from a slow one: `inflight` clears only in the finally
298
- // below, so a never-settling buildBoard() would pin the single-flight forever — every later read (even of a
299
- // perfectly good cached board) short-circuits into the pinned promise before `valid` is consulted,
300
- // invalidation can't help, no log ever fires, and only a restart cures it (the live wedge: hung git
301
- // children → /api/graph 503 forever, silently). So the build races a generous watchdog that REJECTS loudly;
302
- // the rejection flows through the SAME finally → inflight clears → the next read retries fresh. Sitting at
303
- // the single-flight boundary, this one wall bounds every never-settle cause — including ones with no child
304
- // process at all (fs/promises under libuv threadpool starvation); git.ts's per-child timeouts merely make
305
- // the common cause die sooner. Generous: well above the slowest legitimate cold build, so it only ever
306
- // fires on a genuine wedge.
307
- const BUILD_TIMEOUT_MS = Number(process.env.SPEXCODE_BOARD_BUILD_TIMEOUT_MS || 120000)
308
- const RETRY_BACKOFF_MS = Number(process.env.SPEXCODE_BOARD_RETRY_BACKOFF_MS || 1000)
309
- const BACKGROUND_START_DELAY_MS = Number(process.env.SPEXCODE_BOARD_BACKGROUND_START_DELAY_MS || 300)
310
-
311
- // The structural/full and sessions obligations are deliberately separate. A full build can take seconds;
312
- // a persisted session row cannot be silently converted into time behind that unrelated topology work.
313
- type Scope = 'sessions' | 'full'
314
- let cached: Board | null = null // last completed build; served while `dirty === 'none'`
315
- let cachedJson: string | null = null // JSON.stringify(cached), serialized ONCE per build (see getBoardJson)
316
- let cachedRevision: BoardInputRevision | null = null // input revision represented by `cached`
317
- let dirty: Scope | 'none' = 'full' // no cached board yet → the first read builds fully
318
- type Flight = { wait: Promise<Board>; settle: Promise<Board> }
319
- let inflight: Flight | null = null
320
- let sessionFlight: Flight | null = null
321
- let sessionOwed = false
322
- let sessionGeneration = 0
323
- let sessionProjectionPublication = 0
324
- let topologyGeneration = 0
325
- let gen = 0 // bumped on invalidation so patrol validation cannot certify across an event
326
- let retryAt = 0
327
- let lastFailure: Error | null = null
328
-
329
- // mark the cache stale at a SCOPE. Called by every board-stream freshness source (see
330
- // boardStream.fireChanged), so a real change forces the next getBoard() to rebuild while a quiet poll storm
331
- // keeps hitting the cache. `dirty` is the structural producer's scope; `sessionOwed` is intentionally not
332
- // folded into it, because full+sessions owes both a full convergence and a cheap first projection.
333
- function mergeDirty(scope: Scope): void {
334
- if (scope === 'full' || dirty === 'full') dirty = 'full'
335
- else dirty = 'sessions'
336
- }
337
-
338
- export function invalidateBoard(scope: Scope = 'full'): void {
339
- gen++
340
- if (scope === 'sessions') { sessionOwed = true; sessionGeneration++ }
341
- mergeDirty(scope)
342
- retryAt = 0
343
- lastFailure = null
344
- }
345
-
346
- function traceCacheCommit(scope: Scope, startedAt: number): void {
347
- if (!DEBUG) return
348
- const at = Date.now()
349
- console.warn(`spec-cli: graph cache ${JSON.stringify({ at, stage: 'cache-commit', scope, buildMs: at - startedAt })}`)
350
- }
351
-
352
- function startSessionSplice(): Flight | null {
353
- if (!cached || !cachedRevision || !sessionOwed) return null
354
- if (sessionFlight) return sessionFlight
355
- sessionOwed = false
356
- if (dirty === 'sessions') dirty = 'none'
357
- let timedOut = false
358
- const startedAt = Date.now()
359
- let watchdog: ReturnType<typeof setTimeout> | undefined
360
- const timeoutError = () => new Error(`graph session splice did not settle within ${BUILD_TIMEOUT_MS}ms`)
361
- const producer = Promise.resolve().then(async () => {
362
- // A full completion may replace the topology while listSessions is in flight. Rebase before publishing;
363
- // the splice is a projection over whatever last-good structure is current, never a whole-board write race.
364
- while (true) {
365
- const base = cached, revision = cachedRevision, generation = topologyGeneration
366
- if (!base || !revision) throw new Error('graph session splice lost its cached topology')
367
- const before = sessionInputRevision()
368
- const board = await spliceSessions(base)
369
- const after = sessionInputRevision()
370
- if (base !== cached || revision !== cachedRevision || generation !== topologyGeneration) continue
371
- const stable = before.sessions === after.sessions && before.projections === after.projections &&
372
- JSON.stringify(before.activeRoots) === JSON.stringify(after.activeRoots)
373
- const projectedRoots = [...new Set(board.sessions.map((row) => row.path))].sort()
374
- const rootTransition = carrySubtractiveWorktrees(revision, projectedRoots)
375
- const carried = stable ? rootTransition.revision : revision
376
- cached = board
377
- cachedJson = null
378
- cachedRevision = revisionCarriedBySessionSplice(carried, before, board, stable)
379
- sessionProjectionPublication++
380
- if (rootTransition.added) mergeDirty('full')
381
- if (!stable) {
382
- sessionOwed = true
383
- sessionGeneration++
384
- mergeDirty('sessions')
385
- }
386
- return board
387
- }
388
- })
389
- .then((board) => {
390
- if (timedOut) throw timeoutError()
391
- traceCacheCommit('sessions', startedAt)
392
- return board
393
- })
394
- .catch((error) => {
395
- sessionOwed = true
396
- mergeDirty('sessions')
397
- throw error
398
- })
399
- let settle!: Promise<Board>
400
- settle = producer.finally(() => {
401
- clearTimeout(watchdog)
402
- if (sessionFlight?.settle === settle) sessionFlight = null
403
- })
404
- const wait = new Promise<Board>((resolve, reject) => {
405
- watchdog = setTimeout(() => {
406
- timedOut = true
407
- console.warn(`spec-cli: graph session splice did not settle within ${BUILD_TIMEOUT_MS}ms`)
408
- reject(timeoutError())
409
- }, BUILD_TIMEOUT_MS)
410
- watchdog.unref?.()
411
- settle.then((board) => { if (!timedOut) resolve(board) }, (error) => { if (!timedOut) reject(error) })
412
- })
413
- const flight = { wait, settle }
414
- sessionFlight = flight
415
- void wait.catch(() => {})
416
- void settle.catch(() => {})
417
- return flight
418
- }
419
-
420
- // One flight owns BOTH validation and a producer. Every refresh — patrol or watcher-signalled — first folds
421
- // the cheap input revision; an exact match resolves to `cached` without calling either producer. A mismatch
422
- // derives the changed domain and falls through to the build path. An invalidation during validation also falls
423
- // through under this flight, so validation can never race a second producer into existence. `mode` no longer
424
- // selects who validates; it only says whether a stale HTTP response is waiting for a turn to flush first.
425
- type FlightMode = 'dirty' | 'patrol'
426
- function startBuild(mode: FlightMode = 'dirty'): Flight | null {
427
- if (inflight) return inflight
428
- if (Date.now() < retryAt) return null
429
- const controller = new AbortController()
430
- let watchdog: ReturnType<typeof setTimeout> | undefined
431
- let timedOut = false
432
- let built = false
433
- let buildStartedAt = 0
434
- let buildScope: Scope = 'full'
435
- let buildFullStable = true
436
- let buildSessionsStable = true
437
- let buildSessionGeneration = sessionGeneration
438
- let buildSessionProjectionPublication = sessionProjectionPublication
439
- let buildStartedWithSessionOwed = false
440
- let buildAddedSessionRoot = false
441
- let completedRevision: BoardInputRevision | null = null
442
- // Do not invoke the producer inline. buildBoard() has an asynchronous signature but performs a sizeable
443
- // synchronous setup before its first await (Promise.all evaluates its arguments immediately). A stale HTTP
444
- // caller must be able to return its last-good bytes before that setup runs; first-cold/fresh callers still
445
- // await the same deferred promise below.
446
- let resolveBuild!: (board: Board) => void
447
- let rejectBuild!: (error: unknown) => void
448
- const build = new Promise<Board>((resolve, reject) => {
449
- resolveBuild = resolve
450
- rejectBuild = reject
451
- })
452
- // Give a stale HTTP response a turn to flush before the producer's synchronous setup occupies the event
453
- // loop. Fresh callers simply absorb this small scheduling window while waiting on the same flight.
454
- setTimeout(() => {
455
- if (controller.signal.aborted) {
456
- rejectBuild(Object.assign(new Error('graph build aborted before start'), { name: 'AbortError' }))
457
- return
458
- }
459
- try {
460
- Promise.resolve(withGitAbortSignal(controller.signal, async () => {
461
- const prev = cached
462
- const anchor = cachedRevision
463
- const sampledGen = gen
464
- const before = await boardInputRevision(prev)
465
- if (controller.signal.aborted)
466
- throw Object.assign(new Error('graph build aborted before producer start'), { name: 'AbortError' })
467
- // ONE rule for every refresh: the domain a producer runs is DERIVED from the inputs that actually
468
- // moved, never assigned by whoever signalled. A watcher names the leaf it saw, not what the board
469
- // reads — a generated harness artifact rewritten inside a live worktree, or a linked worktree no
470
- // governed record names, moves no board input at all, and used to buy a whole structural assembly
471
- // anyway. The revision sampled here is the complete full-domain contract, so an unmoved full
472
- // obligation is DISCHARGED without assembly. The session projection is deliberately NOT
473
- // dischargeable: liveness lives on graph-stream's poller axis, outside this revision, so a claimed
474
- // sessions obligation always survives and takes its splice. A sample taken after the write is what
475
- // the watcher event was about; an invalidation arriving after the sample keeps its own dirty window.
476
- if (prev && anchor && gen === sampledGen) {
477
- const fullMoved = before.full !== anchor.full
478
- const subtractiveWorktreeMove = fullMoved && isPureSubtractiveWorktreeMove(anchor, before)
479
- const structuralMoved = fullMoved && !subtractiveWorktreeMove
480
- const projectionMoved = before.sessions !== anchor.sessions || before.projections !== anchor.projections
481
- // A projection move nobody signalled is this validation doing the patrol's repair job.
482
- if ((projectionMoved || subtractiveWorktreeMove) && !sessionOwed && dirty !== 'sessions') {
483
- gen++
484
- sessionOwed = true
485
- sessionGeneration++
486
- }
487
- const owed = structuralMoved ? 'full' : (sessionOwed || dirty === 'sessions') ? 'sessions' : 'none'
488
- if (DEBUG && owed !== 'none') {
489
- const moved = Object.keys(before.fullParts).filter((key) => before.fullParts[key] !== anchor.fullParts[key])
490
- console.warn(`spec-cli: graph refresh revision moved — signalled=${dirty} scope=${owed} inputs=[${moved.join(', ')}]`)
491
- } else if (DEBUG && dirty !== 'none') {
492
- console.warn(`spec-cli: graph refresh discharged a ${dirty} signal — no graph input moved`)
493
- }
494
- if (structuralMoved) dirty = 'full'
495
- else if (sessionOwed || dirty === 'sessions') dirty = 'sessions'
496
- // Discharging CONSUMES the claim. Leaving it standing would make every later read re-enter this
497
- // validation, hold the board permanently stale/refreshing, and never converge.
498
- else { dirty = 'none'; return prev }
499
- }
500
- const sessionsOnly = dirty === 'sessions' && prev !== null
501
- buildScope = sessionsOnly ? 'sessions' : 'full'
502
- // Consume the scope this producer is satisfying. Invalidation after this point starts a NEW dirty
503
- // window with its own domain: a session completion during a long full build owes one splice, not
504
- // another full build. The occupied `inflight` slot keeps fresh/stale readers joined while dirty is clean.
505
- dirty = 'none'
506
- if (sessionsOnly) 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(
539
- rootTransition.revision,
540
- before,
541
- board,
542
- buildFullStable && buildSessionsStable && !rootTransition.added,
543
- )
544
- } else {
545
- completedRevision = revisionCarriedByBoard(buildFullStable && buildSessionsStable ? after : before, board)
546
- }
547
- return board
548
- }))
549
- .then(resolveBuild, rejectBuild)
550
- } catch (error) {
551
- rejectBuild(error)
552
- }
553
- }, mode === 'patrol' ? 0 : BACKGROUND_START_DELAY_MS).unref?.()
554
- const timeoutError = () => new Error(`graph build did not settle within ${BUILD_TIMEOUT_MS}ms`)
555
-
556
- // `settle` owns the real builder. The watchdog only rejects `wait`; the slot remains occupied until this
557
- // promise settles, so a next read can never overlap an abandoned git/fs build.
558
- let settle!: Promise<Board>
559
- settle = build.then(async (board) => {
560
- if (timedOut) throw timeoutError()
561
- if (built) {
562
- // A full build's session snapshot may predate a session projection that clients already saw. Rebase those
563
- // published rows in memory on the new topology. This completion path must stay bounded: a later/unpublished
564
- // session generation remains owed to the independent cheap splice rather than making full wait for quiet.
565
- const publishedDuringBuild = sessionProjectionPublication !== buildSessionProjectionPublication
566
- if (buildScope === 'full' && publishedDuringBuild && cached && cachedRevision) {
567
- board = rebasePublishedSessions(board, cached)
568
- completedRevision = revisionCarriedByPublishedSessionRebase(completedRevision!, cachedRevision)
569
- }
570
- const sessionStillOwed = sessionOwed || (!publishedDuringBuild && (
571
- buildStartedWithSessionOwed || !buildSessionsStable || buildSessionGeneration !== sessionGeneration
572
- ))
573
- if (sessionStillOwed) {
574
- sessionOwed = true
575
- mergeDirty('sessions')
576
- }
577
- cached = board
578
- cachedJson = null
579
- cachedRevision = completedRevision
580
- if (buildScope === 'full') topologyGeneration++
581
- else sessionProjectionPublication++
582
- if (!buildFullStable || buildAddedSessionRoot) mergeDirty('full')
583
- traceCacheCommit(buildScope, buildStartedAt)
584
- }
585
- retryAt = 0
586
- lastFailure = null
587
- return board
588
- }).catch((error) => {
589
- const failure = error instanceof Error ? error : new Error(String(error))
590
- if (built) mergeDirty(buildScope)
591
- lastFailure = failure
592
- retryAt = Date.now() + RETRY_BACKOFF_MS
593
- console.warn(`spec-cli: /api/graph build failed — ${failure.message}`)
594
- throw failure
595
- }).finally(() => {
596
- clearTimeout(watchdog)
597
- if (inflight?.settle === settle) 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
-
603
- const wait = new Promise<Board>((resolve, reject) => {
604
- watchdog = setTimeout(() => {
605
- timedOut = true
606
- console.warn(`spec-cli: /api/graph build did not settle within ${BUILD_TIMEOUT_MS}ms — aborting the single-flight build`)
607
- controller.abort()
608
- reject(timeoutError())
609
- }, BUILD_TIMEOUT_MS)
610
- watchdog.unref?.()
611
- settle.then((board) => {
612
- if (!timedOut) resolve(board)
613
- }, (error) => {
614
- if (!timedOut) reject(error)
615
- })
616
- })
617
- const flight = { wait, settle }
618
- inflight = flight
619
- // Background stale readers intentionally do not await these promises. Observe both rejection paths so a
620
- // failed build is loud without becoming an unhandled rejection.
621
- void wait.catch(() => {})
622
- void settle.catch(() => {})
623
- return flight
624
- }
625
-
626
- export function getBoard(): Promise<Board> {
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) return inflight.wait
630
- if (dirty === 'full') {
631
- const flight = startBuild('dirty')
632
- if (flight) return flight.wait
633
- }
634
- if (sessionFlight) return sessionFlight.wait
635
- if (dirty === 'none' && cached) return Promise.resolve(cached)
636
- const flight = startBuild('dirty')
637
- if (flight) return flight.wait
638
- return Promise.reject(lastFailure ?? new Error('graph build retry is temporarily backing off'))
639
- }
640
-
641
- export async function waitForPublishedRevision(required: number, publication: RevisionPublication): Promise<void> {
642
- while (publication.revision() < required) {
643
- publication.invalidate()
644
- await publication.wait()
645
- }
646
- }
647
-
648
- // A board flight can have captured a resident forge slice before its reconcile publishes a newer revision.
649
- // That older flight may settle, but cannot discharge this request: its invalidation stays owed and the fence
650
- // consumes the next publication instead of letting a fresh /api/issues read return the old review snapshot.
651
- export async function getBoardForForgeRevision(required: number): Promise<Board> {
652
- await waitForPublishedRevision(required, {
653
- revision: () => readReviewSnapshot().forgeRevision,
654
- invalidate: () => invalidateBoard('full'),
655
- wait: async () => {
656
- const current = inflight
657
- if (current) await current.settle
658
- else await getBoard()
659
- },
660
- })
661
- return cached ?? getBoard()
662
- }
663
-
664
- // Delta delivery may choose the sessions obligation while a route-owned full is still in flight. Starting
665
- // the full first preserves structural convergence; returning the splice first keeps the lifecycle surface live.
666
- export function getBoardForSessionRefresh(): Promise<Board> {
667
- if (cached && sessionOwed) {
668
- if (!inflight && dirty === 'full') startBuild('dirty')
669
- const flight = startSessionSplice()
670
- if (flight) return flight.wait
671
- }
672
- return getBoard()
673
- }
674
-
675
- // The delta-gated cold tick calls this instead of invalidating. Equal inputs resolve to the cached object;
676
- // changed inputs repair through the same flight and full producer as a watcher-owned invalidation.
677
- export function patrolBoard(): Promise<Board> {
678
- if (!cached) return getBoard()
679
- const flight = startBuild('patrol')
680
- if (flight) return flight.wait
681
- return Promise.reject(lastFailure ?? new Error('graph patrol retry is temporarily backing off'))
682
- }
683
-
684
- export async function readBoard(consistency: BoardConsistency = 'fresh'): Promise<BoardRead> {
685
- if (consistency === 'stale-ok' && cached) {
686
- const stale = dirty !== 'none' || inflight !== null || sessionFlight !== null || sessionOwed
687
- // A held session splice already owns this refresh. Returning stale bytes must not manufacture a full
688
- // producer beside it; a real full obligation still starts its one structural producer independently.
689
- const flight = inflight
690
- ?? (dirty === 'full' ? startBuild('dirty') : null)
691
- ?? sessionFlight
692
- ?? (sessionOwed ? startSessionSplice() : null)
693
- ?? (dirty === 'sessions' ? startBuild('dirty') : null)
694
- return { board: cached, freshness: stale ? 'stale' : 'fresh', refreshing: !!flight, ...(lastFailure ? { error: lastFailure.message } : {}) }
695
- }
696
- const board = await getBoard()
697
- return { board, freshness: 'fresh', refreshing: false }
698
- }
699
-
700
- // the SERIALIZED board for the /api/graph route — JSON.stringify runs ONCE per build, not once per poll,
701
- // so a poll storm of cache hits costs zero serialization CPU (only the etag hash for the 304 path). The SSE
702
- // path still takes the object (getBoard) because it decomposes it into delta units ([[graph-delta]]).
703
- export async function getBoardJson(consistency: BoardConsistency = 'fresh'): Promise<BoardJsonRead> {
704
- const result = await readBoard(consistency)
705
- const board = result.board
706
- if (board === cached && cachedJson !== null) return { ...result, json: cachedJson }
707
- const json = JSON.stringify(board)
708
- if (board === cached) cachedJson = json // memoize only the CURRENT build's serialization
709
- return { ...result, json }
710
- }