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,730 +0,0 @@
1
- import { serve } from '@hono/node-server'
2
- import type { Server as HttpServer, ServerResponse as HttpServerResponse } from 'node:http'
3
- import { randomUUID } from 'node:crypto'
4
- import { installConnectionReaper } from './reaper.js'
5
- import { Hono } from 'hono'
6
- import { cors } from 'hono/cors'
7
- import { etag } from 'hono/etag'
8
- import { createNodeWebSocket } from '@hono/node-ws'
9
- import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig, loadReviewConfig } from './specs.js'
10
- import { issuesEnabled, resolveRemark, retractRemark } from './localIssues.js'
11
- import { closeIssue, createIssue, findIssue, issueStores, mergedIssues, promote } from './issues.js'
12
- import { remarkWithLoopIn, replyIssueWithLoopIn } from './loop-in.js'
13
- import { residentForgeState, refreshForgeNow } from '../../spec-forge/src/resident.js'
14
- import { resolveForgeHost } from '../../spec-forge/src/drivers.js'
15
- import { summarizeLoopIn } from './mentions.js'
16
- import { resolveLayout, mainBranch } from './layout.js'
17
- import { getBoardJson } from './graphCache.js'
18
- import { boardStream, closeBoardFileWatchers, ensureBoardFileWatchers, notifyBoardChanged, flushDeferredWorktreeRegistryChange } from './graphStream.js'
19
- import { gitA, gitTry, repoRoot } from './git.js'
20
- import { cockpitReview } from './cockpit.js'
21
- import { listSessions, sendText, interruptSession, rawKey, stopSession, closeSession, quarantineCorruptRecord, restoreQuarantinedRecord, archiveSession, resumeSession, mergeSession, captureSessionResult, sessionPrompt, renameSession, setSessionSort, sessionCreateRequest, superviseQueue, superviseTurnFailures, superviseDelivery, SessionRecordUnusable, TMUX_SOCK } from './sessions.js'
22
- import { readTimeline } from './session-timeline.js'
23
- import { defaultHarness, HARNESSES, dashboardLauncherList, launcherDefault } from './harness.js'
24
- import { evalTimeline, readBlobByHash } from '../../spec-eval/src/evaltab.js'
25
- import { putBlob } from '../../spec-eval/src/cache.js'
26
- import { fileHumanReading } from '../../spec-eval/src/filing.js'
27
- import { fileHumanOk } from '../../spec-eval/src/humanok.js'
28
- import { buildExportModel, renderExportHtml, SessionEvalUnavailableError } from '../../spec-eval/src/sessioneval.js'
29
- import { appendUpload, cancelUpload, completeUpload, createUpload, evidenceMaxBytes, startUploadReaper, UploadError, uploadStatus } from './uploads.js'
30
- import { attachViewer, detachViewer, resizeBridge, hideViewer, forwardInput, superviseBridges, type Viewer } from './pty-bridge.js'
31
- import { installProcessGuards } from './resilience.js'
32
- import { resolveProjectIdentity } from './project-identity.js'
33
- import { evalDetailReview, evalsReview, issuesReview } from './reviews.js'
34
- import { collectResourceReport, ResourceConflict } from './host-resources.js'
35
-
36
- // last-resort net: an unforeseen async throw (e.g. a worktree vanishing mid-read during a worker
37
- // self-merge) is logged and the server KEEPS SERVING instead of exiting and dropping the public port.
38
- installProcessGuards()
39
-
40
- const app = new Hono()
41
- startUploadReaper()
42
- app.use('/api/*', cors())
43
- app.onError((error, c) => {
44
- if (error instanceof SessionEvalUnavailableError) return c.json({ error: error.message }, 503)
45
- // a record that cannot carry state is a CONFLICT with the caller's request, not a server fault: the refusal
46
- // is deliberate and already carries its own diagnosis + repair ([[sessions-core]]). Answering 500 with a
47
- // stack would hide exactly the sentence the human needs.
48
- if (error instanceof SessionRecordUnusable) return c.json({ error: error.message, code: error.code }, 409)
49
- if (error instanceof ResourceConflict) return c.json({ error: error.message, code: error.code }, 409)
50
- console.error(error)
51
- return c.text('Internal Server Error', 500)
52
- })
53
- const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
54
-
55
- app.get('/', (c) => c.text('spec-cli — GET /api/graph · /api/specs · /api/specs/:id/history · /api/settings · /api/sessions · /api/resources · /api/slash-commands'))
56
- // the supervisor's readiness gate (supervise.ts): a bare git-free 200 so a booting child reports ready the
57
- // instant Hono is listening. Not under /api/* — loopback-only (supervisor→child), no CORS needed.
58
- app.get('/health', (c) => c.text('ok'))
59
- // @@@ instance identity - who THIS backend is: the serve generation's instanceId (minted by the supervisor,
60
- // constant across zero-downtime reloads, handed down via env) and the project root it serves. This is the
61
- // answer the host gateway ([[host-gateway]]) compares an endpoint record against before proxying to it — a
62
- // recycled port serving another project or a stale record fails the match instead of being routed to. Git-free
63
- // after the first memoized resolution; a self-run child (no supervisor) answers instanceId:null, which no
64
- // record claims, so it is simply not hosted.
65
- const instanceStartedAt = new Date().toISOString()
66
- app.get('/api/instance', (c) => {
67
- const root = repoRoot()
68
- return c.json({
69
- instanceId: process.env.SPEXCODE_INSTANCE_ID ?? null,
70
- root,
71
- identity: resolveProjectIdentity(root, root),
72
- pid: process.pid,
73
- startedAt: instanceStartedAt,
74
- })
75
- })
76
- // the assembled graph (merged tree + overlay + sessions) — the dashboard's single source. Same data
77
- // as `spex graph --json`; the frontend only adds x/y pixels on top. Freshness is PUSH-first ([[graph-stream]]): the
78
- // dashboard reloads on a `/api/graph/stream` event, not a tight poll, so the route is a conditional-request
79
- // endpoint: `etag()` hashes the serialized body, and a reload whose `If-None-Match` matches gets a bodyless 304
80
- // instead of the full transfer (~1 MB on the dogfood board — it scales with the node count). The 304 saves the
81
- // WIRE only; the COMPUTE is saved by [[graph-cache]]: getBoard() is single-flight + cached, so a poll storm
82
- // shares ONE build instead of each running its own — the poll-frequency cut (push channel) and the
83
- // build-coalescing cut compound. A hard timeout bounds a wedged build to a loud 503 rather than an
84
- // unboundedly-held connection (the wall sits well above the legitimately-several-seconds cold first build);
85
- // a merely-slow single-flight build keeps running and caches for the next poll, while a NEVER-settling one
86
- // is bounded by [[graph-cache]]'s own build watchdog, so the next poll retries a fresh build.
87
- const BOARD_TIMEOUT_MS = Number(process.env.SPEXCODE_BOARD_TIMEOUT_MS || 20000)
88
- app.get('/api/graph', etag(), async (c) => {
89
- await ensureBoardFileWatchers()
90
- const timeout = Symbol('timeout')
91
- let timer: ReturnType<typeof setTimeout> | undefined
92
- const result = await Promise.race([
93
- getBoardJson('stale-ok'),
94
- new Promise<typeof timeout>((resolve) => {
95
- timer = setTimeout(() => resolve(timeout), BOARD_TIMEOUT_MS)
96
- timer.unref?.()
97
- }),
98
- ])
99
- clearTimeout(timer)
100
- if (result === timeout) return c.json({ error: 'graph build timed out' }, 503)
101
- const freshness = result.refreshing ? `${result.freshness}, refreshing` : result.freshness
102
- c.header('x-spexcode-graph', freshness)
103
- return c.body(result.json, 200, { 'content-type': 'application/json; charset=UTF-8' })
104
- })
105
- // the graph's push channel: an SSE that fires `board-changed` on any session-store write, so the dashboard
106
- // reloads the instant status moves instead of waiting for its slow fallback poll ([[graph-stream]]).
107
- app.get('/api/graph/stream', (c) => boardStream(c))
108
- app.get('/api/specs', async (c) => c.json(await loadSpecs()))
109
- // the search corpus ([[graph-lean]]): a filesystem-only {id,title,path,desc,body} for every node, NO git. The
110
- // board omits `body` to stay lean, so the search palette fetches this ONCE when it opens (cached client-side)
111
- // to rank nodes over their prose — off the board's hot poll. Review rows, including scenarios, come only
112
- // from their paged endpoints and cannot be reconstructed from this corpus.
113
- app.get('/api/specs/lite', (c) => c.json(loadSpecsLite()))
114
- // one node's body + parsed parts ([[graph-lean]]): the board no longer ships either, so the detail view
115
- // fetches this when a node opens. 404 for an unknown id.
116
- app.get('/api/specs/:id/content', (c) => {
117
- const x = specContent(c.req.param('id'))
118
- return x ? c.json(x) : c.json({ body: '', parts: null }, 404)
119
- })
120
- app.get('/api/specs/:id/history', async (c) => c.json(await specHistory(c.req.param('id'))))
121
- // the spec.md line diff one version introduced — the history tab's per-version proof-of-change, fetched
122
- // lazily when an older version's item expands (the latest version's diff ships with the board as node.lastDiff).
123
- app.get('/api/specs/:id/diff/:hash', async (c) => c.json(await specDiffAt(c.req.param('id'), c.req.param('hash'))))
124
- // a unified diff of a node's spec.md from its fork point (the worktree's merge-base with main) to that
125
- // worktree's working tree. An untracked brand-new node is invisible to `git diff <base>`, so when the base
126
- // diff is empty AND status is `??` synthesize an all-additions view via `diff --no-index` (gitTry — --no-index
127
- // exits 1, which gitA would swallow). Gated on `??` so a tracked file with no pending change stays empty.
128
- app.get('/api/edit', async (c) => {
129
- const source = c.req.query('source') || '', path = c.req.query('path') || ''
130
- if (!source || !path) return c.json({ patch: '' })
131
- const mb = mainBranch()
132
- const base = (await gitA(['-C', source, 'merge-base', mb, 'HEAD'])).trim() || mb
133
- let patch = await gitA(['-C', source, 'diff', base, '--', path])
134
- if (!patch) {
135
- const status = await gitA(['-C', source, 'status', '--porcelain', '--untracked-files=all', '--', path])
136
- if (status.startsWith('??')) patch = (await gitTry(['-C', source, 'diff', '--no-index', '--', '/dev/null', path])).stdout
137
- }
138
- return c.json({ patch })
139
- })
140
- // a node's eval timeline (read half of `spex eval`): eval-sidecar readings joined with a live freshness
141
- // flag, newest-first; `hasEvalFile:false` when none declared. Contract belongs to [[spec-eval]].
142
- app.get('/api/specs/:id/evals', async (c) => c.json(await evalTimeline(c.req.param('id'))))
143
- // the eval seam's WRITE half over HTTP ([[spec-eval]] filing.ts) — the REST pair of the GET above: a
144
- // programmatic caller files a reading (verdict + optional transcript) through the SAME append the CLI
145
- // uses. The dashboard does not call this — [[event-detail]] reads readings and hosts remarks, never files.
146
- app.post('/api/specs/:id/evals', async (c) => {
147
- const b = await c.req.json().catch(() => null)
148
- if (!b || typeof b.scenario !== 'string') return c.json({ error: 'body needs { scenario, status, note?, transcript? }' }, 400)
149
- const r = fileHumanReading(c.req.param('id'), b)
150
- return r.ok ? c.json({ ok: true, reading: r.reading }) : c.json({ error: r.error }, 400)
151
- })
152
- // the HUMAN SIGN-OFF write ([[human-ok]]) — the dashboard's ok affordance and `spex eval ok` share this ONE
153
- // write (LAW L: no dashboard-only path). Identity is SERVER-DERIVED 'human', never the request body (the
154
- // same rule as /api/remarks). The write appends a monotonic human-ok event bound to the scenario's latest
155
- // reading and — on the trunk checkout — commits it straight to trunk; the board cache is invalidated
156
- // atomically with persistence so the writer's own refetch never races a stale cache.
157
- app.post('/api/specs/:id/evals/ok', async (c) => {
158
- const b = await c.req.json().catch(() => null)
159
- if (!b || typeof b.scenario !== 'string') return c.json({ error: 'body needs { scenario }' }, 400)
160
- const r = fileHumanOk(c.req.param('id'), b.scenario, 'human')
161
- if (!r.ok) return c.json({ error: r.error }, 400)
162
- notifyBoardChanged('full')
163
- return c.json({ ok: true, already: r.already, humanOk: r.humanOk })
164
- })
165
- // serve a reading's evidence blob by content hash (bytes never enter git): bad hash → 400, missing → 404,
166
- // else the bytes with a sniffed MIME and an immutable cache header (the name IS the content hash).
167
- // HTTP Range is honored — a <video> can only SEEK when the server answers byte ranges (a browser clamps
168
- // currentTime to the seekable window, which stays [0,0] without them); one general mechanism at the
169
- // transport, so every evidence kind streams the same way. A trailing `.<ext>` on the hash is IGNORED
170
- // decoration for third-party markdown renderers (GitLab/GitHub only emit a <video> player when the URL
171
- // ends in a video extension); the served bytes and MIME stay the stored ones — a wrong suffix never lies.
172
- app.get('/api/evidence/:hash', (c) => {
173
- const r = readBlobByHash(c.req.param('hash').replace(/\.[a-z0-9]+$/i, ''))
174
- if (!r.ok) return c.text(r.message, r.reason === 'invalid' ? 400 : 404)
175
- const total = r.bytes.length
176
- const base = { 'Content-Type': r.mime, 'Cache-Control': 'public, max-age=31536000, immutable', 'Accept-Ranges': 'bytes' }
177
- const m = /^bytes=(\d*)-(\d*)$/.exec(c.req.header('range') ?? '')
178
- if (m && (m[1] || m[2])) {
179
- const start = m[1] ? parseInt(m[1], 10) : total - parseInt(m[2], 10)
180
- const end = m[1] && m[2] ? Math.min(parseInt(m[2], 10), total - 1) : total - 1
181
- if (!(start >= 0 && start <= end && end < total)) return c.body(null, 416, { 'Content-Range': `bytes */${total}` })
182
- return c.body(new Uint8Array(r.bytes.subarray(start, end + 1)), 206, { ...base, 'Content-Range': `bytes ${start}-${end}/${total}` })
183
- }
184
- return c.body(new Uint8Array(r.bytes), 200, base)
185
- })
186
- // the WRITE half of the blob store ([[annotator]]): the annotator captures a circled video frame to a PNG
187
- // and stashes the bytes here, content-addressed (same putBlob the eval cache uses). The returned hash is
188
- // what an anchored comment references (image link in the body, and the typed evidence[] on its thread) —
189
- // bytes never enter git. Raw body, sniffed by the same content-addressed name. Empty → 400, over cap → 413.
190
- app.post('/api/evidence', async (c) => {
191
- const buf = Buffer.from(await c.req.arrayBuffer())
192
- if (buf.length === 0) return c.json({ error: 'empty evidence' }, 400)
193
- if (buf.length > evidenceMaxBytes()) return c.json({ error: 'evidence too large' }, 413)
194
- return c.json({ hash: putBlob(buf) }, 201)
195
- })
196
- // the SETTINGS read surface — one route for everything spexcode.json / spexcode.local.json resolves to:
197
- // `layout` (resolveLayout()'s main/worktrees/branch shape — the write-guard's project-identity probe reads
198
- // `.layout.main`) and the dashboard-visible launcher profiles ([[launcher-visibility]]) the New-Session picker
199
- // offers — `{ name, harness, cmd, headless }`: the cmd is read-only display data for the picker (the dashboard sits
200
- // behind the gateway auth; the browser can read but never edit config) — plus the configured `default` NAME
201
- // so the picker pre-selects the SAME launcher a bare `spex session new` uses when that row is visible, else
202
- // its first visible row rather than a hidden headless default,
203
- // Missing defaultLauncher is returned as an actionable config error, not hidden by falling through to the
204
- // built-in `claude` launcher.
205
- // `tmuxSocket` is the `-L <name>` label our private tmux server runs under (a backend fact, env-overridable),
206
- // so the row's attach modal ([[attach-menu]]) can offer the RAW `tmux -L <socket> attach -t <id>` fallback
207
- // beside the blessed `spex session attach` command — the frontend never hardcodes the socket.
208
- app.get('/api/settings', async (c) => c.json({
209
- layout: await resolveLayout(),
210
- launchers: dashboardLauncherList(),
211
- tmuxSocket: TMUX_SOCK,
212
- ...launcherDefault(),
213
- }))
214
- // the `surface: command` plugin-root nodes (built/active only) for new-session and live-inbox `/` dropdowns — each with
215
- // its prompt `body` ({{targets}} placeholder), `kind`, and folder `dir` + co-located `files`. surface is a
216
- // frontmatter field, not a dir (specs.ts loadSurface); `surface: system` siblings are gathered elsewhere.
217
- // `?surface=review` lists the review-track presets instead ([[review-commands]] — the eval detail's
218
- // remark-composer `/` dropdown); the exposed surfaces stay this explicit whitelist, never a passthrough.
219
- app.get('/api/plugins', (c) => c.json(c.req.query('surface') === 'review' ? loadReviewConfig() : loadConfig()))
220
- // the ISSUES read surface ([[issues]]) for the dashboard's issues page — the merged list over every store
221
- // (local threads + the resident forge slice), the SAME mergedIssues() the CLI drain reads, verbatim
222
- // (the dashboard computes nothing over it: no re-sort, no salience ranking). The `enabled` flag mirrors
223
- // the issues-workflow on/off switch so the frontend hides the view when the feature is OFF.
224
- app.get('/api/issues', etag(), async (c) => c.json(await issuesReview(c.req.query('q'), c.req.query('page'))))
225
- // Evals uses the identical paged-review response. `scope:` inside q selects the worktree source; without
226
- // it the source is the current cached board. Filtering/counts always precede the one 25-row slice.
227
- app.get('/api/evals', etag(), async (c) => {
228
- const scope = c.req.query('q')?.match(/(?:^|\s)scope:([^\s]+)/)?.[1]
229
- await ensureBoardFileWatchers(scope)
230
- const page = await evalsReview(c.req.query('q'), c.req.query('page'), { view: c.req.query('view') })
231
- return page ? c.json(page) : c.json({ error: 'no such review source' }, 404)
232
- })
233
- // ONE bounded detail response for both source roots: the selected scenario's complete A/B history and at
234
- // most five lightweight neighbors. It never serializes another scenario's history or the scoped model.
235
- app.get('/api/evals/detail', etag(), async (c) => {
236
- await ensureBoardFileWatchers(c.req.query('scope')?.trim() || undefined)
237
- const node = c.req.query('node')?.trim()
238
- const scenario = c.req.query('scenario')?.trim()
239
- if (!node || !scenario) return c.json({ error: 'node and scenario are required' }, 400)
240
- const detail = await evalDetailReview(node, scenario, c.req.query('scope')?.trim() || null)
241
- return detail ? c.json(detail) : c.json({ error: 'no such review source' }, 404)
242
- })
243
- // the single-thread read ([[issues]]) behind `spex issue show <id>` — the SAME findIssue lookup, from the
244
- // resident forge slice (instant view, background reconcile — the list route's freshness contract). A local
245
- // id, or a forge id (`<host>#<n>`); unknown → 404 (eval-remark threads are not issues, so they 404 here too).
246
- app.get('/api/issues/:id', (c) => {
247
- const t = findIssue(c.req.param('id'), { host: resolveForgeHost(), state: residentForgeState() }, loadSpecsLite().map((s) => s.id))
248
- return t ? c.json(t) : c.json({ error: `no issue '${c.req.param('id')}'` }, 404)
249
- })
250
- // the WRITE surface ([[local-issues]] / [[issues-view]]) — the human reply path, STORE-ROUTED through the one
251
- // reply verb ([[issues]] replyIssue): a local id git-commits to the trunk store, a forge id ('github#N')
252
- // posts a REAL comment through the driver. @session remains prose in either store; it never sends or spawns.
253
- // The server owns its freshness: a forge write forces the resident slice's read-back before answering, so the
254
- // reload that follows shows the comment. Honor the on/off switch: 403 when the feature is OFF; an unknown
255
- // local thread → 404; a failed forge write → 502 with the driver's own message (fail loud, never queued).
256
- app.post('/api/issues/:id/reply', async (c) => {
257
- if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
258
- const body = await c.req.json().catch(() => ({}))
259
- const text = typeof body?.body === 'string' ? body.body : ''
260
- if (!text.trim()) return c.json({ error: 'empty reply' }, 400)
261
- // typed evidence[] — an anchored annotation's frame-blob hashes accrue onto the local thread (same shape
262
- // as the create route); a forge reply ignores them (its frame rides the comment body's image link).
263
- const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
264
- const id = c.req.param('id')
265
- try {
266
- // the mention prompt's node context, from the same resident merge the GET serves
267
- const node = id.includes('#')
268
- ? mergedIssues({ host: resolveForgeHost(), state: residentForgeState() }, loadSpecsLite().map((s) => s.id)).find((i) => i.id === id)?.nodes[0] ?? null
269
- : null
270
- const r = await replyIssueWithLoopIn(id, text, { author: 'human', node, evidence })
271
- if (r.store !== 'local') await refreshForgeNow()
272
- notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
273
- return c.json({ ok: true, replies: r.replies, url: r.url, outcomes: summarizeLoopIn(r.loopIn) })
274
- } catch (e) {
275
- const msg = String((e as Error).message || e)
276
- return c.json({ error: msg }, id.includes('#') ? 502 : 404)
277
- }
278
- })
279
- // store-routed lifecycle close ([[issues]]): local resolves the local thread, forge closes the remote
280
- // issue through the driver. A forge close forces read-back before the dashboard reloads the resident list.
281
- app.post('/api/issues/:id/close', async (c) => {
282
- if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
283
- const id = c.req.param('id')
284
- try {
285
- const r = await closeIssue(id)
286
- if (r.store !== 'local') await refreshForgeNow()
287
- notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
288
- return c.json({ ok: true, ...r })
289
- } catch (e) {
290
- const msg = String((e as Error).message || e)
291
- return c.json({ error: msg }, id.includes('#') ? 502 : 404)
292
- }
293
- })
294
- app.post('/api/issues', async (c) => {
295
- if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
296
- const body = await c.req.json().catch(() => ({}))
297
- const concern = typeof body?.concern === 'string' ? body.concern.trim() : ''
298
- if (!concern) return c.json({ error: 'empty concern' }, 400)
299
- const nodes = Array.isArray(body?.nodes) ? (body.nodes as unknown[]).filter((n): n is string => typeof n === 'string') : []
300
- const postBody = typeof body?.body === 'string' ? body.body : undefined
301
- const store = typeof body?.store === 'string' && body.store.trim() ? body.store.trim() : 'local'
302
- // typed evidence[] — content-addressed evidence hashes (the annotator's clip reference rides here, not prose)
303
- const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
304
- try {
305
- const r = await createIssue(concern, { store, nodes, body: postBody, evidence, author: 'human' })
306
- if (r.store !== 'local') await refreshForgeNow()
307
- notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
308
- return c.json({ ok: true, id: r.id, store: r.store, url: r.url }, 201)
309
- } catch (e) {
310
- return c.json({ error: String((e as Error).message || e) }, store === 'local' ? 500 : 502)
311
- }
312
- })
313
-
314
- // promotion moves an open local thread to the forge as one recorded action ([[issues]]'s promote verb,
315
- // verbatim: forge issue first, then the permalink reply + local close. The forced forge read-back means
316
- // the reload that follows shows the promoted issue in the merged list. Fail loud: an unreachable forge is a
317
- // 502 with the local thread untouched.
318
- app.post('/api/issues/:id/promote', async (c) => {
319
- if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
320
- const id = c.req.param('id')
321
- if (id.includes('#')) return c.json({ error: 'only a local issue promotes' }, 400)
322
- try {
323
- const r = await promote(id, { author: 'human' })
324
- await refreshForgeNow()
325
- notifyBoardChanged('full') // atomic with persistence — see the /api/remarks block below
326
- return c.json({ ok: true, ...r })
327
- } catch (e) {
328
- const msg = String((e as Error).message || e)
329
- return c.json({ error: msg }, /^no local issue/.test(msg) ? 404 : 502)
330
- }
331
- })
332
-
333
- // the REMARK write surface ([[remark-substrate]]) — server PARITY with the CLI: the dashboard can author /
334
- // resolve / retract a remark through the SAME functions `spex remark|resolve|retract` call, adding no
335
- // capability. A ref (`<thread-id>#<rid>`) rides the request BODY, not the path (a '#' in a URL is a
336
- // fragment). Identity is derived SERVER-SIDE — this is the dashboard's human surface, so the actor is
337
- // `'human'`, the SAME sentinel /api/issues stamps; it is NEVER read from the request body. That keeps R3's
338
- // teeth structural (identity is not spoofable over the wire) and identical on both surfaces: resolve is any
339
- // SECOND party's deliberate judgment — the human resolves an agent's remark here exactly as an agent
340
- // resolves through the CLI, and self-resolve stays rejected by the same identity comparison ('human' can
341
- // never resolve a human-authored remark) — and retract binds to the author (only the human's own remarks).
342
- // Who-may-resolve/retract cannot depend on transport.
343
- //
344
- // Every issue/remark write route below ends its success path with notifyBoardChanged('full') — the board
345
- // cache is invalidated ATOMICALLY with persistence ([[remark-substrate]] write-visibility), before the
346
- // response, so the writer's own post-write refetch can never race an async fs event into the stale cache.
347
- // This explicit nudge is the ONE in-process mechanism (the store dir is deliberately NOT in the watch set);
348
- // a cross-process write (a CLI `spex remark add`) reaches the board through its trunk commit via the
349
- // existing refs watcher instead.
350
- app.post('/api/remarks', async (c) => {
351
- if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
352
- const body = await c.req.json().catch(() => ({}))
353
- const text = typeof body?.body === 'string' ? body.body : ''
354
- if (!text.trim()) return c.json({ error: 'empty remark' }, 400)
355
- const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
356
- const host = typeof body?.scenario === 'string' && body.scenario
357
- ? { node: typeof body?.node === 'string' ? body.node : undefined, scenario: body.scenario as string }
358
- : { issue: typeof body?.issue === 'string' ? body.issue : undefined }
359
- const codeSha = typeof body?.codeSha === 'string' ? body.codeSha : undefined
360
- try {
361
- const r = await remarkWithLoopIn(host, text, { codeSha, author: 'human', evidence })
362
- notifyBoardChanged('full')
363
- return c.json({ ok: true, ref: r.ref, rid: r.rid, codeSha: r.codeSha, outcomes: summarizeLoopIn(r.loopIn) }, 201)
364
- } catch (e) {
365
- return c.json({ error: String((e as Error).message || e) }, 400)
366
- }
367
- })
368
- app.post('/api/remarks/:action{resolve|retract}', async (c) => {
369
- if (!issuesEnabled()) return c.json({ error: 'issues workflow is off' }, 403)
370
- const body = await c.req.json().catch(() => ({}))
371
- const ref = typeof body?.ref === 'string' ? body.ref : ''
372
- if (!ref) return c.json({ error: 'missing remark ref' }, 400)
373
- const by = 'human' // server-derived identity — never the request body (see /api/remarks above)
374
- try {
375
- if (c.req.param('action') === 'resolve') resolveRemark(ref, by)
376
- else retractRemark(ref, by)
377
- notifyBoardChanged('full')
378
- return c.json({ ok: true, ref })
379
- } catch (e) {
380
- return c.json({ error: String((e as Error).message || e) }, 400)
381
- }
382
- })
383
- // the harness slice of the dashboard input's `/` dropdown — computed by the launcher's HARNESS adapter the same way that harness
384
- // computes its own `/` menu ([[harness-adapter]]). The client passes `?harness=<id>` for the ACTIVE session,
385
- // so a codex tab gets CODEX's menu, not the default's; unknown/absent → default. Insert-only on the client.
386
- app.get('/api/slash-commands', (c) => {
387
- const h = HARNESSES.find((x) => x.id === c.req.query('harness')) || defaultHarness
388
- return c.json(h.slashCommands())
389
- })
390
-
391
- function uploadFailure(error: unknown): Response {
392
- if (!(error instanceof UploadError)) throw error
393
- const body: { error: string; offset?: number } = { error: error.message }
394
- if (error.offset != null) body.offset = error.offset
395
- return new Response(JSON.stringify(body), { status: error.status, headers: { 'content-type': 'application/json' } })
396
- }
397
-
398
- // One offset protocol for every attachment. Chunks stream through the existing /api proxy and stage only on
399
- // the worker machine; completion is the one boundary that makes a prompt-visible absolute path exist.
400
- app.post('/api/uploads', async (c) => {
401
- const body = await c.req.json().catch(() => null) as { name?: unknown; size?: unknown } | null
402
- try {
403
- return c.json(createUpload(body?.name, body?.size), 201)
404
- } catch (error) {
405
- return uploadFailure(error)
406
- }
407
- })
408
- app.get('/api/uploads/:id', (c) => {
409
- try {
410
- return c.json(uploadStatus(c.req.param('id')))
411
- } catch (error) {
412
- return uploadFailure(error)
413
- }
414
- })
415
- app.patch('/api/uploads/:id', async (c) => {
416
- try {
417
- return c.json(await appendUpload(
418
- c.req.param('id'), Number(c.req.header('upload-offset')), c.req.raw.body, c.req.header('content-length'),
419
- ))
420
- } catch (error) {
421
- return uploadFailure(error)
422
- }
423
- })
424
- app.post('/api/uploads/:id/complete', (c) => {
425
- try {
426
- return c.json({ path: completeUpload(c.req.param('id')) }, 201)
427
- } catch (error) {
428
- return uploadFailure(error)
429
- }
430
- })
431
- app.delete('/api/uploads/:id', (c) => {
432
- try {
433
- cancelUpload(c.req.param('id'))
434
- return c.body(null, 204)
435
- } catch (error) {
436
- return uploadFailure(error)
437
- }
438
- })
439
-
440
- // sessions: real tmux-backed Claude Code sessions. List + spawn, stream the live pane (WebSocket),
441
- // forward keystrokes, and close.
442
- app.get('/api/sessions', async (c) => c.json(await listSessions(c.req.query('all') === '1' || c.req.query('all') === 'true')))
443
- app.get('/api/resources', async (c) => c.json(await collectResourceReport()))
444
- app.post('/api/sessions', async (c) => {
445
- const requestKey = c.req.header('idempotency-key') || randomUUID()
446
- const controller = new AbortController()
447
- const rawSignal = c.req.raw.signal
448
- const outgoing = (c.env as { outgoing?: HttpServerResponse }).outgoing
449
- const cancel = () => {
450
- if (!outgoing?.writableEnded) controller.abort(new Error('session-create caller disconnected'))
451
- }
452
- const cancelFromRequest = () => controller.abort(rawSignal.reason)
453
- rawSignal.addEventListener('abort', cancelFromRequest, { once: true })
454
- outgoing?.once('close', cancel)
455
- try {
456
- const body = await c.req.json().catch(() => null)
457
- const result = await sessionCreateRequest(body, { requestKey, signal: controller.signal })
458
- // The durable row is now public. Nudge the cheap session projection explicitly so a dashboard does not
459
- // wait for the best-effort store watcher; any held candidate worktree event remains a separate full claim.
460
- if (result.status === 201) notifyBoardChanged('sessions')
461
- // A candidate registry event is intentionally held while Git creates the private worktree. Once the
462
- // transaction has published or cleaned up its record, release the one deferred full refresh.
463
- flushDeferredWorktreeRegistryChange()
464
- if (result.status === 201) {
465
- c.header('Idempotency-Key', requestKey)
466
- return c.json(result.session, 201)
467
- }
468
- return c.json({ error: result.error, ...(result.code ? { code: result.code } : {}), ...(result.phase ? { phase: result.phase } : {}) }, result.status as any)
469
- } finally {
470
- flushDeferredWorktreeRegistryChange()
471
- rawSignal.removeEventListener('abort', cancelFromRequest)
472
- outgoing?.off('close', cancel)
473
- }
474
- })
475
- // one server-side merge bundle (ahead/dirty/diff(merge-base)/gates/proposal) for the manager cockpit;
476
- // dashboard and `spex session review` are thin callers. 404 for an unknown id. See [[manager-cockpit]].
477
- app.get('/api/sessions/:id/review', async (c) => {
478
- const r = await cockpitReview(c.req.param('id'))
479
- return r ? c.json(r) : c.json({ error: 'no such session' }, 404)
480
- })
481
- // The self-contained HTML is the sole full-model transport exception. Interactive rows, including the CLI,
482
- // use /api/evals pages; a bare request fails loudly rather than reopening a hidden full JSON path.
483
- app.get('/api/sessions/:id/evals', async (c) => {
484
- if (c.req.query('format') === 'html') {
485
- const m = await buildExportModel(c.req.param('id'))
486
- return m ? c.html(renderExportHtml(m)) : c.text('no such session', 404)
487
- }
488
- return c.json({ error: 'interactive eval rows use /api/evals pagination; use ?format=html only for export' }, 400)
489
- })
490
- // the session's live pane as text (one-shot snapshot) for a backend client (`spex session show --capture`). Empty and fail
491
- // stay distinct: an empty pane is 200 with empty body; unknown id → 404, offline (no live pane) → 409, error → 502.
492
- app.get('/api/sessions/:id/capture', async (c) => {
493
- const r = await captureSessionResult(c.req.param('id'))
494
- if (r.ok) return c.text(r.pane)
495
- if (r.reason === 'unknown') return c.text('no such session', 404)
496
- if (r.reason === 'offline') return c.text('session offline (no live pane)', 409)
497
- return c.text('capture failed', 502)
498
- })
499
- // the session's persisted interaction history ([[session-timeline]]): authored status transitions (with the
500
- // FULL note text) + delivered prompts, timestamped, oldest first — what a terminal-free surface renders as
501
- // the conversation. `?limit=<n>` caps the tail (default 500). 404 for an unknown/non-governed id.
502
- app.get('/api/sessions/:id/timeline', (c) => {
503
- const limit = Number(c.req.query('limit'))
504
- const r = readTimeline(c.req.param('id'), Number.isFinite(limit) && limit > 0 ? limit : undefined)
505
- return r ? c.json(r) : c.json({ error: 'no such session' }, 404)
506
- })
507
- // the session RECORD detail (`spex session show`): the board row (status · node · branch · launcher · …)
508
- // plus the full originating prompt (the row itself carries only the preview). One id-addressed read backs
509
- // the CLI's show; 404 for an unknown id.
510
- app.get('/api/sessions/:id', async (c) => {
511
- const id = c.req.param('id')
512
- const row = (await listSessions(true)).find((s) => s.id === id)
513
- if (!row) return c.json({ error: 'no such session' }, 404)
514
- return c.json({ ...row, prompt: await sessionPrompt(id) })
515
- })
516
- // lifecycle transitions (thin callers of the session state machine)
517
- // relaunch ONLY if confirmed offline; demotes working→idle, keeps any declaration. The RESUME GUARD refuses
518
- // (409) when the agent is alive or its liveness is unproven — restore-on-alive was the incident's kill-shot.
519
- // `force` (query ?force=1 or JSON {force:true}) overrides for a wedged-but-alive process.
520
- app.post('/api/sessions/:id/resume', async (c) => {
521
- const body = await c.req.json().catch(() => ({} as { force?: boolean }))
522
- const force = body?.force === true || c.req.query('force') === '1'
523
- const r = await resumeSession(c.req.param('id'), { force })
524
- return c.json(r, r.ok ? 200 : (r.refused ? 409 : 404))
525
- })
526
- // a dispatch to the session's own agent (it runs the merge), never a server merge — the server never touches
527
- // main's tree. 200 {dispatched:true} once the prompt is accepted, 409 {dispatched:false} if the agent is unreachable.
528
- app.post('/api/sessions/:id/merge', async (c) => {
529
- const r = await mergeSession(c.req.param('id'))
530
- return c.json(r, r.dispatched ? 200 : 409)
531
- })
532
-
533
- // one WS owns one native tmux client (pty-bridge): server→client = that client's rendered PTY bytes (binary);
534
- // client→server text controls resize, visibility, and xterm-native input (which carries the mouse/wheel
535
- // SGR reports xterm produces natively in mouse-report mode). Server→client text commits a completed resize immediately before its binary tmux transaction;
536
- // hiding starts that viewer's bounded helper release without closing the warm socket. tmux itself resolves wheel input between
537
- // copy-mode and a mouse-owning TUI. The bridge never splices capture-pane state into this stream.
538
- // keep-alive ping cadence for the terminal socket — the server half of [[reconnect]]'s heartbeat contract,
539
- // and the contract's ONE primitive number: the client mirrors it (SERVER_PING_MS in the dashboard's
540
- // resilientSocket.js, pinned by its test) and DERIVES its silence deadline (2.5×) from it.
541
- // A healthy link is guaranteed inbound traffic every PING window, so the client may presume an OPEN socket
542
- // silent past its derived window dead. The same tick also sends a WebSocket protocol ping; browsers answer its
543
- // pong below JavaScript, so a backend reload stays compatible with a tab running the previous frontend bundle.
544
- // The server owns that mirror deadline and detaches the viewer itself when a half-open link never reports close.
545
- // Terminal pixels remain binary frames, so heartbeat controls never enter xterm.
546
- const TERM_PING_MS = 10000
547
- const TERM_DEAD_MS = 2.5 * TERM_PING_MS
548
- app.get('/api/sessions/:id/socket', upgradeWebSocket((c) => {
549
- const id = c.req.param('id') as string
550
- let viewer: Viewer | null = null
551
- let ping: ReturnType<typeof setInterval> | undefined
552
- let pongDeadline: ReturnType<typeof setTimeout> | undefined
553
- let cleaned = false
554
- const disarmPongDeadline = () => { if (pongDeadline) clearTimeout(pongDeadline); pongDeadline = undefined }
555
- let armPongDeadline = () => {}
556
- const cleanup = () => {
557
- if (cleaned) return
558
- cleaned = true
559
- if (ping) clearInterval(ping)
560
- disarmPongDeadline()
561
- if (viewer) detachViewer(id, viewer)
562
- viewer = null
563
- }
564
- return {
565
- onOpen(_evt, ws) {
566
- viewer = {
567
- send: (buf) => { try { ws.send(Uint8Array.from(buf)) } catch { /* viewer gone */ } },
568
- commitSize: (cols, rows) => { try { ws.send(JSON.stringify({ t: 'resize-commit', cols, rows })) } catch { /* viewer gone */ } },
569
- }
570
- attachViewer(id, viewer)
571
- armPongDeadline = () => {
572
- if (cleaned) return
573
- disarmPongDeadline()
574
- pongDeadline = setTimeout(() => {
575
- cleanup()
576
- try { ws.close() } catch { /* cleanup already detached the dead viewer */ }
577
- }, TERM_DEAD_MS)
578
- pongDeadline.unref()
579
- }
580
- // `raw` is @hono/node-ws's real ws.WebSocket. Protocol pong is intentionally the server-side liveness
581
- // signal: unlike an application text reply, every browser generation answers it automatically.
582
- ws.raw.on('pong', armPongDeadline)
583
- armPongDeadline()
584
- ping = setInterval(() => {
585
- try { ws.raw.ping() } catch { /* viewer gone; onClose reaps */ }
586
- try { ws.send('ping') } catch { /* client dead-man still needs observable inbound traffic */ }
587
- }, TERM_PING_MS)
588
- },
589
- onMessage(evt) {
590
- if (!viewer) return
591
- const data = evt.data
592
- // Binary input is ignored; JSON keeps terminal input distinct from binary pane output while preserving
593
- // xterm's ordered string exactly. The bridge accepts input only from this viewer's visible claim.
594
- if (typeof data === 'string') {
595
- if (data === 'pong') {
596
- armPongDeadline()
597
- return
598
- }
599
- try {
600
- const m = JSON.parse(data)
601
- if (m?.t === 'resize') resizeBridge(id, viewer, Number(m.cols), Number(m.rows))
602
- else if (m?.t === 'visible' && m.visible === false) hideViewer(id, viewer)
603
- else if (m?.t === 'input' && typeof m.data === 'string') forwardInput(id, viewer, m.data)
604
- } catch { /* ignore */ }
605
- }
606
- },
607
- onClose() { cleanup() },
608
- }
609
- }))
610
- // ONE input route, `kind` the discriminator — the transport split is an implementation fact, not API surface.
611
- // kind:"text" (`spex session send`, the server-side merge dispatch) appends the prompt to the
612
- // target timeline, then best-effort pokes its adapter. A dead channel delays context injection but does not
613
- // change the successful append response; 502 means the record rejected the write.
614
- // kind:"keys" is the LAST-RESORT raw face (`spex session send --keys`): an ORDERED BATCH of
615
- // nav-mode key tokens over tmux send-keys, delivered in array order so tap order survives
616
- // ([[nav-mode-key-ordering]]); unstable by nature — callers try a plain text send first. An unknown kind is a
617
- // loud 400, never a guessed channel.
618
- app.post('/api/sessions/:id/input', async (c) => {
619
- const body = await c.req.json().catch(() => ({}))
620
- if (body?.kind === 'text' || body?.kind === 'command') {
621
- // `from` (the sender's session id) rides only an agent-to-agent send → the backend records the comms
622
- // edge ([[session-timeline]]); a raw human dispatch omits it and is not logged. `replyVia:"note"` marks a
623
- // terminal-free sender ([[session-timeline]]): the server appends the note-reply insert to the delivery.
624
- const r = await sendText(c.req.param('id'), typeof body?.text === 'string' ? body.text : '', typeof body?.from === 'string' ? body.from : undefined, {
625
- ...(body?.replyVia === 'note' ? { replyVia: 'note' as const } : {}),
626
- })
627
- return c.json(r, r.ok ? 200 : 502)
628
- }
629
- if (body?.kind === 'keys') {
630
- const keys = Array.isArray(body?.keys) ? body.keys.filter((k: unknown) => typeof k === 'string') : []
631
- const ok = await rawKey(c.req.param('id'), keys)
632
- return c.json({ ok }, ok ? 200 : 404)
633
- }
634
- return c.json({ error: 'input needs kind: "text" | "command" | "keys"' }, 400)
635
- })
636
- // soft stop: kill the agent's tmux + socket but KEEP the worktree (resumable). Distinct from close, which
637
- // removes the worktree. {ok:false} = no such session.
638
- app.post('/api/sessions/:id/stop', async (c) => {
639
- const sessionId = c.req.param('id')
640
- const ok = await stopSession(sessionId)
641
- return c.json(ok ? { ok: true } : { ok: false, error: `no stop transition was committed for session ${sessionId}` }, ok ? 200 : 404)
642
- })
643
- app.post('/api/sessions/:id/interrupt', async (c) => {
644
- const result = await interruptSession(c.req.param('id'))
645
- return c.json(result, result.ok ? 200 : 502)
646
- })
647
- app.post('/api/sessions/:id/close', async (c) => {
648
- const sessionId = c.req.param('id')
649
- const ok = await closeSession(sessionId)
650
- // The close route owns its write's visible boundary: filesystem watchers can be unavailable, so cache
651
- // invalidation must happen before the success response rather than leaving the confirming board to patrol.
652
- if (ok) notifyBoardChanged('sessions')
653
- return c.json(ok ? { ok: true } : { ok: false, error: `no close transition was committed for session ${sessionId}` }, ok ? 200 : 404)
654
- })
655
- app.post('/api/sessions/:id/quarantine', async (c) => {
656
- const body = await c.req.json().catch(() => null)
657
- const result = await quarantineCorruptRecord(c.req.param('id'), body)
658
- return c.json({ ok: true, ...result })
659
- })
660
- app.post('/api/sessions/:id/quarantine/restore', async (c) => {
661
- const result = await restoreQuarantinedRecord(c.req.param('id'))
662
- return c.json({ ok: true, ...result })
663
- })
664
- // archive / legacy unarchive signpost ([[archive]]) — archive proves exact cold/offline ownership before filing;
665
- // `{on:false}` enters the same resume transition and recreates the preserved conversation. {ok:false}=no such session.
666
- app.post('/api/sessions/:id/archive', async (c) => {
667
- const body = await c.req.json().catch(() => ({}))
668
- return c.json({ ok: await archiveSession(c.req.param('id'), body?.on !== false) })
669
- })
670
- // set (or clear, with a blank) a session's display-name override; persists to the session's global record
671
- // (`session.json`) so it survives a restart. Unknown id → 404. That record sits INSIDE the watched store, but
672
- // the store watch is best-effort (it can fail to attach), so the route still nudges the stream explicitly
673
- // ([[graph-stream]]) — the rename shows in ~150ms deterministically, never waiting out a cold tick.
674
- app.post('/api/sessions/:id/rename', async (c) => {
675
- const body = await c.req.json().catch(() => ({}))
676
- const ok = await renameSession(c.req.param('id'), typeof body?.name === 'string' ? body.name : '')
677
- if (ok) notifyBoardChanged('sessions')
678
- return c.json({ ok }, ok ? 200 : 404)
679
- })
680
-
681
- // set/clear a session's sort-key ([[session-reorder]]): a finite number pins the row's slot, null (or
682
- // non-numeric) restores birth order. Mirrors /rename.
683
- app.post('/api/sessions/:id/sort', async (c) => {
684
- const body = await c.req.json().catch(() => ({}))
685
- const key = typeof body?.key === 'number' && Number.isFinite(body.key) ? body.key : null
686
- const ok = await setSessionSort(c.req.param('id'), key)
687
- return c.json({ ok }, ok ? 200 : 404)
688
- })
689
-
690
- const port = Number(process.env.PORT || 8787)
691
- // @@@ server-side connection reaping ([[spec-cli]]) - abandoned connections must die SERVER-SIDE, or they
692
- // pile up and wedge the backend (135 leaked conns once starved :8787 into looking dead — the cascade that
693
- // triggered the mass-restore incident, since every client-side timeout-kill leaks one). The ONE mechanism is
694
- // the socket-level `installConnectionReaper` below (reaper.ts): a per-socket deadline that reaps a
695
- // slow-loris / idle keep-alive but exempts an ACTIVE WS/SSE stream (board-stream, terminal socket) for as
696
- // long as it streams. Deliberately NO `serverOptions` timeouts here: they were measured to be not harmless
697
- // but a second mechanism racing the reaper (issue #65 — a 20s headersTimeout won at default config and
698
- // silently capped SPEXCODE_REAP_HEADER_MS); the install disables Node's overlapping timeouts so the
699
- // deadlines have a single owner.
700
- // @@@ loopback bind ([[public-mode]]) - this child is NEVER the internet face: the supervisor (and in public
701
- // mode the gateway) fronts it, and dials it only via 127.0.0.1. Binding loopback is what makes "loopback is
702
- // the trust boundary" true — without a hostname Node binds all interfaces and the child is reachable from
703
- // the LAN with no password, bypassing the gate entirely (measured: eval auth-boundary).
704
- const server = serve({ fetch: app.fetch, port, hostname: '127.0.0.1' })
705
- installConnectionReaper(server as unknown as HttpServer)
706
- injectWebSocket(server)
707
- superviseBridges() // restore visible helpers after failure; their viewer subscriptions survive replacement
708
- superviseQueue() // launch queued sessions as slots free (catches agent-authored proposals/crashes the server never sees directly)
709
- superviseTurnFailures() // reconcile adapter-owned native failure subscriptions across backend replacement
710
- superviseDelivery() // hand over messages an earlier pass could not ([[delivery-queue]]): the retry half of dispatch
711
- console.log(`spec-cli serving .spec (from git) on http://localhost:${port}`)
712
-
713
- let graphWatchersClosed = false
714
- const closeGraphWatchers = (): void => {
715
- if (graphWatchersClosed) return
716
- graphWatchersClosed = true
717
- closeBoardFileWatchers()
718
- }
719
- process.once('exit', closeGraphWatchers)
720
-
721
- // graceful drain (the other half of zero-downtime reload, supervise.ts): on SIGTERM stop accepting new
722
- // connections, let in-flight requests finish, and sweep now-idle keep-alive sockets so close() fires the
723
- // instant the last request drains. A hard cap still forces exit if a connection won't close.
724
- process.on('SIGTERM', () => {
725
- closeGraphWatchers()
726
- const srv = server as unknown as { close(cb?: () => void): void; closeIdleConnections?(): void }
727
- const sweep = setInterval(() => srv.closeIdleConnections?.(), 200)
728
- srv.close(() => { clearInterval(sweep); process.exit(0) })
729
- setTimeout(() => process.exit(0), 10000).unref()
730
- })