spexcode 0.6.2 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/README.md +3 -1
  2. package/bin/spex.mjs +4 -0
  3. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +23 -0
  4. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +195 -0
  5. package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
  6. package/node_modules/@spexcode/session-core/dist/index.js +5 -0
  7. package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
  8. package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
  9. package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
  10. package/node_modules/@spexcode/session-core/dist/message.js +52 -0
  11. package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
  12. package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
  13. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +50 -0
  14. package/node_modules/@spexcode/session-core/dist/runtime-session.js +286 -0
  15. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +14 -0
  16. package/node_modules/@spexcode/session-core/dist/session-cursors.js +82 -0
  17. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +46 -0
  18. package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
  19. package/node_modules/@spexcode/session-core/package.json +33 -0
  20. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +77 -0
  21. package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
  22. package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
  23. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +36 -0
  24. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +316 -0
  25. package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
  26. package/node_modules/@spexcode/spec-cli/dist/cli.js +1908 -0
  27. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +112 -0
  28. package/node_modules/@spexcode/spec-cli/dist/client.js +481 -0
  29. package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
  30. package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
  31. package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
  32. package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
  33. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +58 -0
  34. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +798 -0
  35. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
  36. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
  37. package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
  38. package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
  39. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
  40. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
  41. package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
  42. package/node_modules/@spexcode/spec-cli/dist/doctor.js +553 -0
  43. package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
  44. package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
  45. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
  46. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
  47. package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
  48. package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
  49. package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
  50. package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
  51. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
  52. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
  53. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
  54. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +363 -0
  55. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +38 -0
  56. package/node_modules/@spexcode/spec-cli/dist/gateway.js +527 -0
  57. package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
  58. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +717 -0
  59. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
  60. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +36 -0
  61. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
  62. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
  63. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
  64. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
  65. package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
  66. package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +36 -32
  67. package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
  68. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
  69. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +338 -0
  70. package/node_modules/@spexcode/spec-cli/dist/harness.js +3146 -0
  71. package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
  72. package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
  73. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +15 -0
  74. package/node_modules/@spexcode/spec-cli/dist/help.js +594 -0
  75. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
  76. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
  77. package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
  78. package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
  79. package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
  80. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
  81. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
  82. package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
  83. package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
  84. package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
  85. package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
  86. package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
  87. package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
  88. package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
  89. package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
  90. package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
  91. package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
  92. package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
  93. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +8 -0
  94. package/node_modules/@spexcode/spec-cli/dist/listen.js +24 -0
  95. package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
  96. package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
  97. package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
  98. package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
  99. package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
  100. package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
  101. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
  102. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
  103. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
  104. package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
  105. package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
  106. package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
  107. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +5 -0
  108. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +215 -0
  109. package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
  110. package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
  111. package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
  112. package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
  113. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
  114. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
  115. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
  116. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
  117. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
  118. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
  119. package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
  120. package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
  121. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
  122. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
  123. package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
  124. package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
  125. package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
  126. package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
  127. package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
  128. package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
  129. package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
  130. package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
  131. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
  132. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
  133. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
  134. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
  135. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.d.ts +1 -0
  136. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -0
  137. package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
  138. package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
  139. package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
  140. package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
  141. package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
  142. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
  143. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
  144. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
  145. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
  146. package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
  147. package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
  148. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
  149. package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
  150. package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
  151. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +11 -0
  152. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +20 -0
  153. package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
  154. package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
  155. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +433 -0
  156. package/node_modules/@spexcode/spec-cli/dist/sessions.js +4667 -0
  157. package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
  158. package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
  159. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
  160. package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
  161. package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
  162. package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
  163. package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
  164. package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
  165. package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
  166. package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
  167. package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
  168. package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
  169. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
  170. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
  171. package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
  172. package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
  173. package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
  174. package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
  175. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
  176. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
  177. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
  178. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +1 -1
  179. package/node_modules/@spexcode/spec-cli/package.json +49 -0
  180. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  181. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  182. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  183. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  184. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  185. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  186. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  187. package/node_modules/@spexcode/spec-core/dist/git.d.ts +170 -0
  188. package/node_modules/@spexcode/spec-core/dist/git.js +2768 -0
  189. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  190. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  191. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  192. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  193. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  194. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  195. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  196. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  197. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  198. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  199. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  200. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  201. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +187 -0
  202. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  203. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  204. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  205. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  206. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  207. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  208. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  209. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  210. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  211. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  212. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  213. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  214. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  215. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  216. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  217. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  218. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  219. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  220. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  221. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  222. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  223. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  224. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  225. package/node_modules/@spexcode/spec-core/package.json +28 -0
  226. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  227. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  228. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  229. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  230. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  231. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  232. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  233. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  234. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  235. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  236. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  237. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  238. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  239. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  240. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  241. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  242. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  243. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  244. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  245. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  246. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  247. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  248. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  249. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  250. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  251. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  252. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  253. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  254. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  255. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  256. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  257. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  258. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  259. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  260. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  261. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  262. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  263. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  264. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  265. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  266. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  267. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  268. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  269. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  270. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  271. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  272. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  273. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  274. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  275. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  276. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  277. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  278. package/package.json +33 -19
  279. package/spec-cli/bin/spex.mjs +0 -69
  280. package/spec-cli/src/anchors.ts +0 -728
  281. package/spec-cli/src/attach.ts +0 -43
  282. package/spec-cli/src/claude-headless.ts +0 -321
  283. package/spec-cli/src/cli.ts +0 -1258
  284. package/spec-cli/src/client.ts +0 -414
  285. package/spec-cli/src/cockpit.ts +0 -43
  286. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  287. package/spec-cli/src/commit-surgery.ts +0 -68
  288. package/spec-cli/src/contract-filter.ts +0 -202
  289. package/spec-cli/src/delivery-queue.ts +0 -165
  290. package/spec-cli/src/doctor.ts +0 -516
  291. package/spec-cli/src/execution-trace.ts +0 -444
  292. package/spec-cli/src/file-write.ts +0 -22
  293. package/spec-cli/src/gateway-auth.ts +0 -181
  294. package/spec-cli/src/gateway-hub.ts +0 -358
  295. package/spec-cli/src/gateway.ts +0 -461
  296. package/spec-cli/src/git.ts +0 -2556
  297. package/spec-cli/src/graph.ts +0 -271
  298. package/spec-cli/src/graphCache.ts +0 -710
  299. package/spec-cli/src/graphDelta.ts +0 -77
  300. package/spec-cli/src/graphStream.ts +0 -1019
  301. package/spec-cli/src/harness-select.ts +0 -77
  302. package/spec-cli/src/harness.ts +0 -3025
  303. package/spec-cli/src/headless-controller.ts +0 -63
  304. package/spec-cli/src/help.ts +0 -492
  305. package/spec-cli/src/host-resources.ts +0 -762
  306. package/spec-cli/src/host.ts +0 -611
  307. package/spec-cli/src/identity-presets.d.ts +0 -13
  308. package/spec-cli/src/identity-presets.js +0 -138
  309. package/spec-cli/src/index.ts +0 -818
  310. package/spec-cli/src/init.ts +0 -258
  311. package/spec-cli/src/issues-cli.ts +0 -276
  312. package/spec-cli/src/issues.ts +0 -237
  313. package/spec-cli/src/layout.ts +0 -640
  314. package/spec-cli/src/lint.ts +0 -488
  315. package/spec-cli/src/listen.ts +0 -28
  316. package/spec-cli/src/localIssues.ts +0 -538
  317. package/spec-cli/src/materialize.ts +0 -417
  318. package/spec-cli/src/mentions.ts +0 -81
  319. package/spec-cli/src/opencode-headless.ts +0 -205
  320. package/spec-cli/src/pi-headless.ts +0 -153
  321. package/spec-cli/src/plugin-harness.ts +0 -157
  322. package/spec-cli/src/process-identity.ts +0 -207
  323. package/spec-cli/src/project-identity.ts +0 -73
  324. package/spec-cli/src/project-store.ts +0 -17
  325. package/spec-cli/src/pty-bridge.ts +0 -396
  326. package/spec-cli/src/pty-helper.mjs +0 -96
  327. package/spec-cli/src/pty-native-helper.mjs +0 -22
  328. package/spec-cli/src/ranker.ts +0 -191
  329. package/spec-cli/src/reaper.ts +0 -125
  330. package/spec-cli/src/resilience.ts +0 -41
  331. package/spec-cli/src/reviewSnapshot.ts +0 -28
  332. package/spec-cli/src/reviews.ts +0 -434
  333. package/spec-cli/src/runtime-guard.ts +0 -35
  334. package/spec-cli/src/runtime-ownership.ts +0 -88
  335. package/spec-cli/src/search.bench.mjs +0 -80
  336. package/spec-cli/src/search.ts +0 -63
  337. package/spec-cli/src/session-cursors.ts +0 -91
  338. package/spec-cli/src/session-execution.ts +0 -68
  339. package/spec-cli/src/session-files.ts +0 -121
  340. package/spec-cli/src/session-follow.ts +0 -182
  341. package/spec-cli/src/session-reparent.ts +0 -33
  342. package/spec-cli/src/session-timeline.ts +0 -247
  343. package/spec-cli/src/session-web.ts +0 -135
  344. package/spec-cli/src/sessions.ts +0 -3883
  345. package/spec-cli/src/sh.ts +0 -3
  346. package/spec-cli/src/slash-commands.ts +0 -318
  347. package/spec-cli/src/source-files.ts +0 -104
  348. package/spec-cli/src/specs.ts +0 -498
  349. package/spec-cli/src/supervise.ts +0 -245
  350. package/spec-cli/src/tree.ts +0 -118
  351. package/spec-cli/src/tsx-bin.ts +0 -18
  352. package/spec-cli/src/uninstall.ts +0 -171
  353. package/spec-cli/src/uploads.ts +0 -253
  354. package/spec-cli/src/worktree-sources.ts +0 -42
  355. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  356. package/spec-dashboard/dist/assets/App-B6IbFbGu.js +0 -2
  357. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  358. package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +0 -27
  359. package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +0 -2
  360. package/spec-dashboard/dist/assets/IssuesPage-BG9rNOgV.js +0 -1
  361. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  404. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  405. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  406. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  407. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  408. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  409. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  410. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  411. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  412. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  413. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  414. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  415. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  416. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  417. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  418. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  419. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  420. package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +0 -2
  421. package/spec-dashboard/dist/assets/Modal-B3EcgeA3.js +0 -1
  422. package/spec-dashboard/dist/assets/PageScroll-CNZOugWV.js +0 -1
  423. package/spec-dashboard/dist/assets/ProjectsPage-C2CRHmvs.js +0 -1
  424. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  425. package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +0 -40
  426. package/spec-dashboard/dist/assets/SessionWindow-1nYgZ4CL.js +0 -1
  427. package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +0 -1
  428. package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +0 -13
  429. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  430. package/spec-dashboard/dist/assets/data-SlvxNA2r.js +0 -1
  431. package/spec-dashboard/dist/assets/index-DLVeOHL-.js +0 -41
  432. package/spec-dashboard/dist/assets/index-DYLGfPjx.css +0 -1
  433. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  434. package/spec-dashboard/dist/assets/launch-yxCYr64x.js +0 -276
  435. package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +0 -1
  436. package/spec-dashboard/dist/index.html +0 -29
  437. package/spec-dashboard/src/reviewFilters.js +0 -322
  438. package/spec-dashboard/src/reviewQuery.js +0 -174
  439. package/spec-dashboard/src/session.js +0 -199
  440. package/spec-eval/src/cache.ts +0 -57
  441. package/spec-eval/src/cli.ts +0 -894
  442. package/spec-eval/src/evaltab.ts +0 -264
  443. package/spec-eval/src/filing.ts +0 -39
  444. package/spec-eval/src/freshness.ts +0 -836
  445. package/spec-eval/src/humanok.ts +0 -28
  446. package/spec-eval/src/scenariofresh.ts +0 -244
  447. package/spec-eval/src/scenarios.ts +0 -672
  448. package/spec-eval/src/sessioneval.ts +0 -2220
  449. package/spec-eval/src/sidecar.ts +0 -93
  450. package/spec-eval/src/timeline.ts +0 -54
  451. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  452. package/spec-forge/src/cache.ts +0 -66
  453. package/spec-forge/src/cli.ts +0 -99
  454. package/spec-forge/src/drivers/github.ts +0 -132
  455. package/spec-forge/src/drivers/gitlab.ts +0 -153
  456. package/spec-forge/src/drivers.ts +0 -73
  457. package/spec-forge/src/links.ts +0 -72
  458. package/spec-forge/src/needs-eval.ts +0 -29
  459. package/spec-forge/src/port.ts +0 -36
  460. package/spec-forge/src/resident.ts +0 -46
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  480. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  481. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  482. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  483. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  484. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec.md +0 -0
  485. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -0
  486. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -0
  487. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -0
  488. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -0
  489. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  490. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  491. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  492. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  493. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  494. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  495. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -0,0 +1,1908 @@
1
+ // static import is fine here: mentions.ts is dependency-free at module level, and stripRefSigil is needed
2
+ // by several verbs (spec owner, graph, issue/eval node args) — a CLI reference arg tolerates an optional @/[[ ]]
3
+ // sigil ([[mentions]]).
4
+ import { stripRefSigil } from './mentions.js';
5
+ import { installEvalHost } from './eval-host.js';
6
+ installEvalHost();
7
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ const cmd = process.argv[2];
9
+ function levenshtein(a, b) {
10
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
11
+ for (let i = 1; i <= a.length; i++) {
12
+ let previous = row[0];
13
+ row[0] = i;
14
+ for (let j = 1; j <= b.length; j++) {
15
+ const current = row[j];
16
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
17
+ previous = current;
18
+ }
19
+ }
20
+ return row[b.length];
21
+ }
22
+ function nearestPublicCommand(input, commands) {
23
+ const query = input.toLowerCase();
24
+ if (!/^[a-z0-9-]+$/.test(query))
25
+ return null;
26
+ const similarity = (a, b) => 1 - levenshtein(a, b) / Math.max(a.length, b.length);
27
+ const words = (text) => text.toLowerCase().match(/[a-z0-9-]+/g) ?? [];
28
+ let best = null;
29
+ for (const command of commands) {
30
+ const [headline = '', ...detail] = command.text.split('\n');
31
+ const score = Math.max(similarity(query, command.name), ...words(headline).map((word) => similarity(query, word)), ...words(detail.join('\n')).map((word) => similarity(query, word) * 0.8));
32
+ if (!best || score > best.score)
33
+ best = { name: command.name, score };
34
+ }
35
+ return best && best.score >= 0.8 ? best.name : null;
36
+ }
37
+ if (cmd === '--version' || cmd === '-v') {
38
+ const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
39
+ console.log(manifest.version);
40
+ process.exit(0);
41
+ }
42
+ const DAEMON_DEPENDENCIES = ['hono', '@hono/node-server', '@hono/node-ws', 'node-pty'];
43
+ async function assertDaemonDependencies(command) {
44
+ const { createRequire } = await import('node:module');
45
+ const resolve = createRequire(import.meta.url).resolve;
46
+ const missing = DAEMON_DEPENDENCIES.filter((dependency) => {
47
+ try {
48
+ resolve(dependency);
49
+ return false;
50
+ }
51
+ catch {
52
+ return true;
53
+ }
54
+ });
55
+ if (!missing.length)
56
+ return;
57
+ console.error(`${command}: missing optional daemon dependencies: ${missing.join(', ')}`);
58
+ console.error(`Install them with: npm install ${missing.join(' ')}`);
59
+ process.exit(1);
60
+ }
61
+ // Registered before any await so a fatal top-level error lands here. Errors we OWN — BackendError, the
62
+ // loud malformed-config ConfigError, the --api/--port UsageError, the write-guard GuardError, and a
63
+ // GitWorkspaceError that teaches a fresh directory how to continue — are
64
+ // matched BY NAME (to avoid importing them) and rendered as a one-line `spex: <message>` (a user's
65
+ // config typo or a refused cross-project write must read as their situation, not a SpexCode stack dump);
66
+ // anything else prints in full so a real bug keeps its trace. A synchronous throw inside an awaited call
67
+ // (loadConfig on a malformed spexcode.json) surfaces as uncaughtException, not unhandledRejection, so BOTH
68
+ // paths route through the same printer.
69
+ function fatal(e) {
70
+ if (e instanceof Error && ['BackendError', 'ConfigError', 'UsageError', 'GuardError', 'DashboardAssetError', 'GitWorkspaceError'].includes(e.name))
71
+ console.error(`spex: ${e.message}`);
72
+ else
73
+ console.error(e);
74
+ process.exit(1);
75
+ }
76
+ process.on('unhandledRejection', fatal);
77
+ process.on('uncaughtException', fatal);
78
+ // tiny flag reader: --key value (and bare positionals)
79
+ function flag(name) {
80
+ const i = process.argv.indexOf(`--${name}`);
81
+ return i >= 0 ? process.argv[i + 1] : undefined;
82
+ }
83
+ // Exit AFTER stdout has flushed. process.exit() force-quits without draining buffered pipe writes, so a
84
+ // large piped dump (`spex issue ls --json | …`, graph --json, review --json) is silently cut off at the
85
+ // pipe buffer (~64KB). The empty write's callback fires once every prior queued chunk has drained; the
86
+ // returned promise never resolves (process.exit ends the process inside the callback), so
87
+ // `await flushExit(code)` halts execution here exactly like process.exit did — safe to drop in on any
88
+ // unbounded-output verb. EPIPE (a reader that closed early — `| head`, `| jq` exiting) can never drain, so
89
+ // we ALSO exit on the stream error rather than hang: the truncation is the reader's choice then, not ours.
90
+ function flushExit(code = 0) {
91
+ return new Promise(() => {
92
+ const done = () => process.exit(code);
93
+ process.stdout.on('error', done);
94
+ process.stdout.write('', done);
95
+ });
96
+ }
97
+ const has = (name) => process.argv.includes(`--${name}`);
98
+ // bare positionals after argv index `from`, skipping flags and their values (selectors for ls/watch).
99
+ const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--prompt-file', '--timeout', '--reason', '--out', '--content-dir', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api', '--api-port', '--host', '--preset', '--limit', '--session', '--depth', '--focus', '--keys', '--ssh', '--allow-stop', '--allow-resume', '--ttl-ms', '--wait-ms', '--adapter', '--thread', '--tmux', '--worktree', '--branch', '--to', '--name', '--base', '--path', '--owner', '--details', '--variant', '--cli', '--count', '--ids']);
100
+ const EXPLICIT_BACKEND_ROUTE_FLAGS = ['api', 'port', 'password', 'insecure'];
101
+ const EXPLICIT_BACKEND_VALUE_FLAGS = EXPLICIT_BACKEND_ROUTE_FLAGS
102
+ .filter((name) => VALUE_FLAGS.has(`--${name}`))
103
+ .map((name) => `--${name}`);
104
+ const EXPLICIT_BACKEND_BARE_FLAGS = EXPLICIT_BACKEND_ROUTE_FLAGS
105
+ .filter((name) => !VALUE_FLAGS.has(`--${name}`))
106
+ .map((name) => `--${name}`);
107
+ function positionals(from) {
108
+ const out = [];
109
+ for (let i = from; i < process.argv.length; i++) {
110
+ const t = process.argv[i];
111
+ if (t.startsWith('--')) {
112
+ if (VALUE_FLAGS.has(t))
113
+ i++;
114
+ continue;
115
+ }
116
+ out.push(t);
117
+ }
118
+ return out;
119
+ }
120
+ function rejectFlags(command, from, allowed, attached = []) {
121
+ const known = new Set(allowed.map((name) => `--${name}`));
122
+ for (let i = from; i < process.argv.length; i++) {
123
+ const token = process.argv[i];
124
+ if (!token.startsWith('--'))
125
+ continue;
126
+ if (attached.some((name) => token.startsWith(`--${name}=`)))
127
+ continue;
128
+ if (!known.has(token)) {
129
+ console.error(`${command}: unknown flag ${token}`);
130
+ process.exit(2);
131
+ }
132
+ if (VALUE_FLAGS.has(token))
133
+ i++;
134
+ }
135
+ }
136
+ function rejectUnknownFlags(command, from, allowed, attached = []) {
137
+ rejectFlags(command, from, allowed, attached);
138
+ }
139
+ function rejectUnknownBackendFlags(command, from, allowed, attached = []) {
140
+ rejectFlags(command, from, [...allowed, ...EXPLICIT_BACKEND_ROUTE_FLAGS], attached);
141
+ }
142
+ // `--children` deliberately has an optional value only in its attached form. A separated following token
143
+ // remains a normal ls selector, so the long-standing `ls --children <child-SEL>` grammar keeps its meaning.
144
+ function childrenScopeOption() {
145
+ const forms = process.argv.slice(4).filter((token) => token === '--children' || token.startsWith('--children='));
146
+ if (!forms.length)
147
+ return undefined;
148
+ if (forms.length > 1) {
149
+ console.error('spex session ls: --children may appear only once');
150
+ process.exit(2);
151
+ }
152
+ if (forms[0] === '--children')
153
+ return null;
154
+ const selector = forms[0].slice('--children='.length);
155
+ if (!selector) {
156
+ console.error('spex session ls: --children=<PARENT-SEL> requires a non-empty parent selector');
157
+ process.exit(2);
158
+ }
159
+ return selector;
160
+ }
161
+ const idLikeSelector = (selector) => /^[0-9a-f-]{8,}$/i.test(stripRefSigil(selector));
162
+ function sessionSendUsage(detail, keys = false) {
163
+ console.error(`spex session send: ${detail}`);
164
+ console.error(keys
165
+ ? 'usage: spex session send <SEL> --keys "<keys>" (e.g. "Up Up Enter", "C-r", single chars — last resort; try a plain send first)'
166
+ : 'usage: spex session send <SEL> "<msg>" [--api <url> | --port <n>]\n spex session send --ssh <address> <FULL-SESSION-ID> "<msg>"\n spex session send <SEL> [--api <url> | --port <n>] -- <option-shaped-msg>');
167
+ process.exit(2);
168
+ }
169
+ function parseSessionSendArgs(args) {
170
+ const valueFlags = new Set([...EXPLICIT_BACKEND_VALUE_FLAGS, '--keys', '--ssh']);
171
+ const bareFlags = new Set(EXPLICIT_BACKEND_BARE_FLAGS);
172
+ const values = new Map();
173
+ const positionals = [];
174
+ let endOfOptions = false;
175
+ for (let i = 0; i < args.length; i++) {
176
+ const token = args[i];
177
+ if (!endOfOptions && token === '--') {
178
+ endOfOptions = true;
179
+ continue;
180
+ }
181
+ if (!endOfOptions && token.startsWith('--')) {
182
+ if (!valueFlags.has(token) && !bareFlags.has(token))
183
+ sessionSendUsage(`unknown flag ${token}`);
184
+ if (values.has(token))
185
+ sessionSendUsage(`${token} may appear only once`, token === '--keys');
186
+ values.set(token, '');
187
+ if (bareFlags.has(token))
188
+ continue;
189
+ const value = args[++i];
190
+ if (value === undefined || value === '' || value.startsWith('--'))
191
+ sessionSendUsage(`${token} expects one non-empty value`, token === '--keys');
192
+ values.set(token, value);
193
+ continue;
194
+ }
195
+ positionals.push(token);
196
+ }
197
+ if ((values.has('--api') && values.has('--port')) || (values.has('--ssh') && (values.has('--api') || values.has('--port')))) {
198
+ sessionSendUsage('--ssh, --api, and --port are alternate routes; choose one');
199
+ }
200
+ if (values.has('--ssh') && (values.has('--password') || values.has('--insecure'))) {
201
+ sessionSendUsage('--password and --insecure apply only to an explicit --api route, not --ssh');
202
+ }
203
+ const rawKeys = values.get('--keys');
204
+ if (rawKeys !== undefined) {
205
+ if (values.has('--ssh'))
206
+ sessionSendUsage('--keys cannot cross a machine peer', true);
207
+ if (positionals.length !== 1)
208
+ sessionSendUsage('raw keys take one selector and no text message', true);
209
+ const keys = rawKeys.split(/\s+/).filter(Boolean);
210
+ if (!keys.length)
211
+ sessionSendUsage('--keys expects one non-empty value', true);
212
+ return { selector: positionals[0], kind: 'keys', keys };
213
+ }
214
+ if (positionals.length !== 2)
215
+ sessionSendUsage('plain send requires exactly one selector and one message');
216
+ return { selector: positionals[0], kind: 'text', text: positionals[1], ...(values.has('--ssh') ? { sshAddress: values.get('--ssh') } : {}) };
217
+ }
218
+ const FULL_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
219
+ function sessionPeerAnchorUsage(verb, detail) {
220
+ console.error(`spex session ${verb}: ${detail}`);
221
+ console.error(verb === 'ls'
222
+ ? 'usage: spex session ls [SEL...] [--status a,b] [--all] [--json]\n spex session ls --ssh <address> <FULL-SESSION-ID> [--status a,b] [--json]'
223
+ : 'usage: spex session new "<prompt>" [--prompt-file <path>|-] [--launcher <name>] [--name <name>] [--base <commit-ish>]\n spex session new --ssh <address> <FULL-SESSION-ID> "<prompt>" [--prompt-file <path>|-] [--launcher <name>] [--name <name>] [--base <commit-ish>]');
224
+ process.exit(2);
225
+ }
226
+ // With SSH, every session verb starts with the same full-id anchor. Target operations address that record;
227
+ // ls/new derive its project at the remote gateway. This keeps project identity out of a second flag grammar.
228
+ function parseSessionPeerAnchor(verb, args) {
229
+ const sshCount = process.argv.filter((token) => token === '--ssh').length;
230
+ if (!sshCount)
231
+ return null;
232
+ if (sshCount !== 1)
233
+ sessionPeerAnchorUsage(verb, '--ssh may appear only once');
234
+ const sshAddress = flag('ssh');
235
+ if (!sshAddress || sshAddress.startsWith('--'))
236
+ sessionPeerAnchorUsage(verb, '--ssh expects one non-empty address');
237
+ if (has('api') || has('port'))
238
+ sessionPeerAnchorUsage(verb, '--ssh, --api, and --port are alternate routes; choose one');
239
+ if (has('password') || has('insecure'))
240
+ sessionPeerAnchorUsage(verb, '--password and --insecure apply only to an explicit --api route, not --ssh');
241
+ const sessionId = args[0];
242
+ if (!FULL_SESSION_ID.test(sessionId ?? ''))
243
+ sessionPeerAnchorUsage(verb, '--ssh requires a full session id as its first positional');
244
+ return { sshAddress, sessionId };
245
+ }
246
+ function sessionTargetUsage(verb, detail) {
247
+ console.error(`spex session ${verb}: ${detail}`);
248
+ console.error(verb === 'show'
249
+ ? 'usage: spex session show <SEL> [--capture] [--json] [--api <url> | --port <n>]\n spex session show --ssh <address> <FULL-SESSION-ID> [--json]'
250
+ : 'usage: spex session close <SEL> [--api <url> | --port <n>]\n spex session close --ssh <address> <FULL-SESSION-ID>');
251
+ process.exit(2);
252
+ }
253
+ function parseSessionTargetArgs(verb, args) {
254
+ const values = new Map();
255
+ const valueFlags = new Set([...EXPLICIT_BACKEND_VALUE_FLAGS, '--ssh']);
256
+ const bareFlags = new Set([...EXPLICIT_BACKEND_BARE_FLAGS, ...(verb === 'show' ? ['--capture', '--json'] : [])]);
257
+ const positionals = [];
258
+ for (let i = 0; i < args.length; i++) {
259
+ const token = args[i];
260
+ if (!token.startsWith('--')) {
261
+ positionals.push(token);
262
+ continue;
263
+ }
264
+ if (!valueFlags.has(token) && !bareFlags.has(token))
265
+ sessionTargetUsage(verb, `unknown flag ${token}`);
266
+ if (values.has(token))
267
+ sessionTargetUsage(verb, `${token} may appear only once`);
268
+ values.set(token, '');
269
+ if (bareFlags.has(token))
270
+ continue;
271
+ const value = args[++i];
272
+ if (value === undefined || value === '' || value.startsWith('--'))
273
+ sessionTargetUsage(verb, `${token} expects one non-empty value`);
274
+ values.set(token, value);
275
+ }
276
+ if ((values.has('--api') && values.has('--port')) || (values.has('--ssh') && (values.has('--api') || values.has('--port')))) {
277
+ sessionTargetUsage(verb, '--ssh, --api, and --port are alternate routes; choose one');
278
+ }
279
+ if (values.has('--ssh') && (values.has('--password') || values.has('--insecure'))) {
280
+ sessionTargetUsage(verb, '--password and --insecure apply only to an explicit --api route, not --ssh');
281
+ }
282
+ if (values.has('--ssh') && verb === 'show' && values.has('--capture'))
283
+ sessionTargetUsage(verb, '--capture cannot cross a machine peer');
284
+ if (positionals.length !== 1)
285
+ sessionTargetUsage(verb, 'requires exactly one session selector');
286
+ return { selector: positionals[0], ...(values.has('--ssh') ? { sshAddress: values.get('--ssh') } : {}) };
287
+ }
288
+ const SIGNPOSTS = {
289
+ search: 'spex spec search <query>',
290
+ owner: 'spex spec owner <path>',
291
+ lint: 'spex spec lint',
292
+ ack: 'spex spec ack <node>… --reason "<why>"',
293
+ tree: 'spex graph',
294
+ board: 'spex graph --json',
295
+ blob: 'spex evidence put|get',
296
+ issues: 'spex issue — ls (was: bare issues) · show · open · reply · close · promote; on|off|status → the `issues.enabled` key in spexcode.json; `issues nudge` → spex internal nudge',
297
+ forge: 'spex issue links [--pending] [--store <host>] (--host is now --store)',
298
+ runtime: 'spex doctor repair app-server',
299
+ new: 'spex session new',
300
+ ls: 'spex session ls',
301
+ watch: 'spex session watch',
302
+ wait: 'spex session wait',
303
+ review: 'spex session review',
304
+ merge: 'spex session merge',
305
+ send: 'spex session send',
306
+ reopen: 'spex session resume',
307
+ done: 'spex session done',
308
+ park: 'spex session park',
309
+ ask: 'spex session ask',
310
+ exit: 'spex session stop',
311
+ close: 'spex session close',
312
+ capture: 'spex session show <SEL> --capture',
313
+ attach: 'spex session attach',
314
+ rename: 'spex session rename',
315
+ prompt: 'spex session show <SEL>',
316
+ rawkey: 'spex session send <SEL> --keys "<keys>"',
317
+ resolve: 'spex remark resolve <ref>',
318
+ retract: 'spex remark retract <ref>',
319
+ };
320
+ const SESSION_SIGNPOSTS = {
321
+ rawkey: 'spex session send <SEL> --keys "<keys>"',
322
+ exit: 'spex session stop <SEL>',
323
+ reopen: 'spex session resume <SEL> [--force]',
324
+ capture: 'spex session show <SEL> --capture',
325
+ prompt: 'spex session show <SEL>',
326
+ state: 'spex internal session-state',
327
+ fail: 'spex internal session-fail',
328
+ idle: 'spex internal session-idle',
329
+ 'commit-gate': 'spex internal commit-gate',
330
+ };
331
+ function signpost(oldSpelling, newSpelling) {
332
+ console.error(`spex: \`${oldSpelling}\` was removed in v0.3.0 — use: ${newSpelling} (map: spex help)`);
333
+ process.exit(2);
334
+ }
335
+ if (cmd !== undefined && SIGNPOSTS[cmd])
336
+ signpost(`spex ${cmd}`, SIGNPOSTS[cmd]);
337
+ async function followKit(selectors, verb) {
338
+ const { followSessions } = await import('./session-follow.js');
339
+ const { localCachedSessions } = await import('./client.js');
340
+ const { fromRaw, ownSessionId, selectSessions, toSession } = await import('./sessions.js');
341
+ const { listSessionIds, readPublicRecordEntry } = await import('@spexcode/spec-core');
342
+ const real = selectors.filter((sel) => sel && sel !== '@all');
343
+ let picked = [];
344
+ if (real.length) {
345
+ picked = selectSessions(localCachedSessions(true), real).map((s) => s.id);
346
+ if (!picked.length) {
347
+ console.error(`${verb}: no such session: ${real.join(' ')}`);
348
+ process.exit(2);
349
+ }
350
+ }
351
+ const targets = real.length ? () => picked : () => listSessionIds();
352
+ // A feed line needs a session's NAME, which is record data; its STATUS comes from the log event, never from
353
+ // a fresh sample of the mutable record — that resample is what made the old poll lose moves.
354
+ const row = (id, status, note) => {
355
+ const entry = readPublicRecordEntry(id);
356
+ if (entry.kind !== 'ok')
357
+ return null;
358
+ return { ...toSession(fromRaw(entry.raw), status, 'unknown'), note };
359
+ };
360
+ const watcher = ownSessionId();
361
+ return { watcher, follow: (emit, opts) => followSessions(emit, { ...opts, targets, self: watcher, row }) };
362
+ }
363
+ async function localWatchTargetsOrExit(selectors, verb) {
364
+ if (!selectors.length) {
365
+ console.error(`usage: ${verb} <SEL...>`);
366
+ process.exit(2);
367
+ }
368
+ const { localCachedSessions } = await import('./client.js');
369
+ const { selectSessions } = await import('./sessions.js');
370
+ const targets = selectSessions(localCachedSessions(true), selectors).map((session) => session.id);
371
+ if (!targets.length) {
372
+ console.error(`${verb}: no such local session: ${selectors.join(' ')}`);
373
+ process.exit(2);
374
+ }
375
+ return targets;
376
+ }
377
+ async function resolveSessionOrExit(selector) {
378
+ if (!selector) {
379
+ console.error('spex: missing session selector (id | id-prefix | node | branch | . for self)');
380
+ process.exit(2);
381
+ }
382
+ const { resolveClientSession } = await import('./client.js');
383
+ const { sessionTitle } = await import('./sessions.js');
384
+ const r = await resolveClientSession(selector);
385
+ if ('ok' in r)
386
+ return r.ok;
387
+ if ('none' in r) {
388
+ console.error(`spex: no such session: ${selector}`);
389
+ process.exit(2);
390
+ }
391
+ console.error(`spex: ambiguous selector "${selector}" matches ${r.ambiguous.length} sessions — be more specific:`);
392
+ for (const s of r.ambiguous)
393
+ console.error(` ${s.id.slice(0, 8)} ${sessionTitle(s)}`);
394
+ process.exit(2);
395
+ }
396
+ async function resolveSelectorOrExit(selector) {
397
+ return (await resolveSessionOrExit(selector)).id;
398
+ }
399
+ // the [[session-eval]] EXPORT artifact behind `spex eval ls --session <SEL> --export`: fetch the
400
+ // backend-rendered self-contained HTML, write it (--out, else a tmp file) or open it (--open). Never returns.
401
+ async function evalExport(id) {
402
+ const { clientEvalExport } = await import('./client.js');
403
+ const r = await clientEvalExport(id);
404
+ if (!r.ok) {
405
+ console.error(`no export for ${id} (status ${r.status})`);
406
+ process.exit(1);
407
+ }
408
+ const { writeFileSync } = await import('node:fs');
409
+ const { join } = await import('node:path');
410
+ const { tmpdir } = await import('node:os');
411
+ const out = flag('out') ?? join(tmpdir(), `spexcode-eval-${id.slice(0, 8)}.html`);
412
+ writeFileSync(out, r.body);
413
+ if (has('open')) {
414
+ const { spawn } = await import('node:child_process');
415
+ const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
416
+ try {
417
+ spawn(opener, [out], { detached: true, stdio: 'ignore' }).unref();
418
+ console.log(`opened ${out}`);
419
+ }
420
+ catch {
421
+ console.log(`wrote ${out} — couldn't auto-open, open it in a browser`);
422
+ }
423
+ }
424
+ else
425
+ console.log(out);
426
+ process.exit(0);
427
+ }
428
+ // Internal lifecycle writers use the same local record-resolution rules as worker declarations, but the
429
+ // porcelain done/park/ask dispatch below goes straight to its named handler.
430
+ async function stateKit() {
431
+ const { sessionStateKit } = await import('./session-declarations.js');
432
+ return sessionStateKit(flag('session'));
433
+ }
434
+ // a trailing --help/-h prints help and exits BEFORE any verb runs, so a help probe never fires a
435
+ // streaming/mutating command. It prints THAT command's usage when an entry exists (the second layer
436
+ // of the help journey — see help.ts); session noun-verb probes project the exact verb from the shared
437
+ // drawer definition. Unknown tokens preserve the existing drawer/map fallback. (Removed spellings never
438
+ // reach here — the signpost table above already exited.)
439
+ if (cmd && cmd !== 'help' && (has('help') || process.argv.includes('-h'))) {
440
+ const { commandHelp, overviewHelp } = await import('./help.js');
441
+ console.log(commandHelp(cmd, cmd === 'session' || cmd === 'doctor' ? process.argv[3] : undefined) ?? overviewHelp());
442
+ process.exit(0);
443
+ }
444
+ if (cmd === 'serve') {
445
+ // `spex serve [api]` runs the backend; `spex serve ui` serves the dashboard on top of a running backend —
446
+ // two processes, two verbs in one operator drawer.
447
+ const target = positionals(3)[0];
448
+ if (target === 'ui') {
449
+ const { ensureDashboardArtifact } = await import('./dashboard-assets.js');
450
+ ensureDashboardArtifact('dist');
451
+ await assertDaemonDependencies('spex serve');
452
+ // the natural post-install UI: serve the bundled dashboard on its OWN port (loopback by default;
453
+ // --host widens the bind for LAN/tailnet viewing), proxying /api + the terminal socket to a
454
+ // separately-run `spex serve`. Replaces the dogfood-only `npm run web` (vite).
455
+ const { serveDashboardLocal } = await import('./gateway.js');
456
+ const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173);
457
+ const apiPort = Number(flag('api-port') ?? process.env.PORT ?? 8787);
458
+ const host = flag('host') ?? '127.0.0.1';
459
+ if (!Number.isInteger(port) || !Number.isInteger(apiPort)) {
460
+ console.error('spex serve ui: --port and --api-port must be integers');
461
+ process.exit(2);
462
+ }
463
+ serveDashboardLocal({ port, apiPort, host });
464
+ }
465
+ else if (target === undefined || target === 'api') {
466
+ await assertDaemonDependencies('spex serve');
467
+ // fail loud, not cryptic ([[platform-support]]): serve IS the entry to the session runtime, which needs a
468
+ // POSIX host (tmux/bash/unix-sockets). On a non-POSIX host (native Windows) point at WSL2 and exit here,
469
+ // before importing the supervisor starts the backend.
470
+ const { assertSessionRuntime } = await import('./runtime-guard.js');
471
+ assertSessionRuntime();
472
+ // the supervisor owns the public port and runs the compiled index.js child for zero-downtime reloads.
473
+ // --port is sugar over the PORT env supervise.ts reads — set BEFORE importing so it takes effect. This
474
+ // mirrors `spex serve ui --api-port`, so one host runs many projects (each `serve --port N` paired with
475
+ // a `serve ui --api-port N`), cwd picking which project's .spec is served — no shared default collides.
476
+ const portArg = flag('port');
477
+ if (portArg !== undefined) {
478
+ if (!Number.isInteger(Number(portArg))) {
479
+ console.error('spex serve: --port must be an integer');
480
+ process.exit(2);
481
+ }
482
+ process.env.PORT = portArg;
483
+ }
484
+ await import('./supervise.js');
485
+ }
486
+ else {
487
+ console.error(`spex serve: unknown target '${target}' — spex serve [api] (the backend) | spex serve ui (the dashboard)`);
488
+ process.exit(2);
489
+ }
490
+ }
491
+ else if (cmd === 'dashboard') {
492
+ const { ensureDashboardArtifact } = await import('./dashboard-assets.js');
493
+ ensureDashboardArtifact('dist');
494
+ await assertDaemonDependencies('spex dashboard');
495
+ // the HOST-level dashboard ([[host-gateway]]): ONE gateway for every project this user serves. The
496
+ // engine is [[gateway-hub]] (routing + [[gateway-auth]] authorization: admin scope implicit from
497
+ // loopback until an admin password is set; per-project gates as configured); the host layer mounts the
498
+ // instance-validated project registry, its SSE stream, the durable catalog, and the /projects
499
+ // operations (register · init · doctor · start a backend) as the hub's admin extension. No --api-port
500
+ // pairing: which backend a request reaches is named in its /p/:projectId path, resolved per request.
501
+ // `spex serve ui` remains the explicit one-backend pairing; this verb is the zero-config many-project face.
502
+ const { startHostDashboard } = await import('./host.js');
503
+ const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173);
504
+ const host = flag('host') ?? '127.0.0.1';
505
+ if (!Number.isInteger(port)) {
506
+ console.error('spex dashboard: --port must be an integer');
507
+ process.exit(2);
508
+ }
509
+ startHostDashboard({ port, host });
510
+ }
511
+ else if (cmd === 'guidance') {
512
+ let out;
513
+ for (let i = 3; i < process.argv.length; i++) {
514
+ const token = process.argv[i];
515
+ if (token === '--json')
516
+ continue;
517
+ if (token === '--out') {
518
+ if (out !== undefined) {
519
+ console.error('spex guidance: --out may appear only once');
520
+ process.exit(2);
521
+ }
522
+ const value = process.argv[++i];
523
+ if (!value || value.startsWith('--')) {
524
+ console.error('spex guidance: --out expects a file path');
525
+ process.exit(2);
526
+ }
527
+ out = value;
528
+ continue;
529
+ }
530
+ if (token.startsWith('--')) {
531
+ console.error(`spex guidance: unknown flag ${token}`);
532
+ process.exit(2);
533
+ }
534
+ console.error(`spex guidance: unexpected argument '${token}'`);
535
+ process.exit(2);
536
+ }
537
+ const { buildGuidanceCatalog } = await import('./guidance-catalog.js');
538
+ const catalog = buildGuidanceCatalog();
539
+ if (out) {
540
+ catalog.write(out);
541
+ console.log(out);
542
+ process.exit(0);
543
+ }
544
+ process.stdout.write(catalog.exportJson());
545
+ await flushExit(0);
546
+ }
547
+ else if (cmd === undefined || cmd === 'help' || cmd === '--help' || cmd === '-h') {
548
+ // `spex help <cmd>` drills into one command; bare help is the map. Both name the next layer down.
549
+ const { commandHelp, overviewHelp } = await import('./help.js');
550
+ const topic = positionals(3)[0];
551
+ if (cmd === 'help' && topic) {
552
+ if (SIGNPOSTS[topic])
553
+ signpost(`spex ${topic}`, SIGNPOSTS[topic]);
554
+ const h = commandHelp(topic);
555
+ if (!h) {
556
+ console.error(`spex help: no command '${topic}' — run \`spex help\` for the map`);
557
+ process.exit(2);
558
+ }
559
+ console.log(h);
560
+ }
561
+ else
562
+ console.log(overviewHelp());
563
+ }
564
+ else if (cmd === 'guide') {
565
+ const { guideText, guideTopics } = await import('./guide.js');
566
+ if (process.argv[3] === 'config')
567
+ signpost('spex guide config', 'spex guide settings');
568
+ const text = guideText(process.argv[3]);
569
+ if (text === null) {
570
+ console.error(`spex guide: no topic '${process.argv[3]}'. Topics: ${guideTopics().join(', ')}. Run \`spex guide\` (no topic) for the setup workflow, \`spex help\` for the command map.`);
571
+ process.exit(2);
572
+ }
573
+ console.log(text);
574
+ }
575
+ else if (cmd === 'graph') {
576
+ if (flag('node') !== undefined) {
577
+ console.error('spex graph: --node was renamed — use --focus <id>');
578
+ process.exit(2);
579
+ }
580
+ if (has('public')) {
581
+ const focusRaw = flag('focus');
582
+ const depthRaw = flag('depth');
583
+ const out = flag('out');
584
+ const contentDir = flag('content-dir');
585
+ if (focusRaw !== undefined || depthRaw !== undefined) {
586
+ console.error('spex graph --public: --focus and --depth are not supported');
587
+ process.exit(2);
588
+ }
589
+ const { buildPublicGraphArtifact, publicGraphJson } = await import('./public-graph.js');
590
+ const artifact = await buildPublicGraphArtifact();
591
+ const payload = publicGraphJson(artifact.graph);
592
+ if (out) {
593
+ writeFileSync(out, payload);
594
+ console.log(out);
595
+ }
596
+ else {
597
+ process.stdout.write(payload);
598
+ }
599
+ if (contentDir) {
600
+ mkdirSync(contentDir, { recursive: true });
601
+ for (const document of artifact.documents) {
602
+ writeFileSync(`${contentDir}/${encodeURIComponent(document.id)}.json`, `${JSON.stringify(document, null, 2)}\n`);
603
+ }
604
+ }
605
+ await flushExit(0);
606
+ }
607
+ if (flag('content-dir') !== undefined) {
608
+ console.error('spex graph: --content-dir requires --public');
609
+ process.exit(2);
610
+ }
611
+ const { buildBoard } = await import('./graphSnapshot.js');
612
+ const focusRaw = flag('focus');
613
+ const depthRaw = flag('depth');
614
+ const depth = depthRaw === undefined ? undefined : Number(depthRaw);
615
+ if (depth !== undefined && (!Number.isInteger(depth) || depth < 0)) {
616
+ console.error('spex graph: --depth must be a non-negative integer');
617
+ process.exit(2);
618
+ }
619
+ const board = await buildBoard();
620
+ if (has('json') && focusRaw === undefined && depth === undefined) {
621
+ // interactive-only stderr hint: the JSON dump is machine food; a human at a tty gets pointed at
622
+ // the readable twin. Piped/redirected stdout stays byte-identical (the hint never touches stdout).
623
+ if (process.stdout.isTTY)
624
+ console.error('(human-readable: spex graph)');
625
+ console.log(JSON.stringify(board, null, 2));
626
+ await flushExit(0);
627
+ }
628
+ const { renderTree, treeJson } = await import('./tree.js');
629
+ const opts = { node: focusRaw && stripRefSigil(focusRaw), depth, color: process.stdout.isTTY && !process.env.NO_COLOR };
630
+ try {
631
+ // --json on a FOCUSED/pruned view is that view's JSON (the filtered subtree, badge counts precomputed);
632
+ // --json bare is the full payload above — the filter chooses which representation you asked for.
633
+ console.log(has('json') ? JSON.stringify(treeJson(board.nodes, opts), null, 2) : renderTree(board.nodes, opts));
634
+ }
635
+ catch (e) {
636
+ console.error(`spex graph: ${e?.message ?? e}`);
637
+ process.exit(2);
638
+ }
639
+ await flushExit(0);
640
+ }
641
+ else if (cmd === 'spec') {
642
+ const sub = process.argv[3];
643
+ if (sub === undefined) {
644
+ console.log((await import('./help.js')).commandHelp('spec'));
645
+ }
646
+ else if (sub === 'search') {
647
+ const { searchSpecs, nearestTitles } = await import('./search.js');
648
+ const query = positionals(4).join(' ');
649
+ if (!query.trim()) {
650
+ console.error('usage: spex spec search <query> [--json] [--limit N]');
651
+ process.exit(2);
652
+ }
653
+ const limit = Number(flag('limit')) || 10;
654
+ const results = await searchSpecs(query, { limit, onStats: (s) => console.error(`[spec-search] compute ${s.ms.toFixed(1)}ms · ${s.nodes} nodes · ${s.tokens} tokens (excludes process start)`) });
655
+ // zero-result fail-loud + route-to-next-step: always the corpus-is-English fact (unconditional — no
656
+ // language sniffing, no score threshold), plus the nearest titles when anything is even near (typo
657
+ // recovery) and the browse-all pointer, so no query dead-ends.
658
+ const NO_MATCH = (q) => {
659
+ const near = nearestTitles(q, 3);
660
+ return [
661
+ `no spec node matches "${q}" (the corpus is English — if your query isn't, translate and retry)`,
662
+ ...(near.length ? ['nearest titles:', ...near.map((t) => ` ${t.title} [${t.id}]`)] : []),
663
+ 'browse all: spex graph',
664
+ ].join('\n');
665
+ };
666
+ if (has('json')) {
667
+ if (!results.length)
668
+ console.error(NO_MATCH(query)); // stderr: the stdout JSON contract stays verbatim
669
+ console.log(JSON.stringify(results));
670
+ await flushExit(0);
671
+ }
672
+ if (!results.length) {
673
+ console.log(NO_MATCH(query));
674
+ process.exit(0);
675
+ }
676
+ results.forEach((r, i) => {
677
+ console.log(`${String(i + 1).padStart(2)}. ${r.title} [${r.id}] · score ${r.score}`);
678
+ console.log(` ${r.path}`);
679
+ if (r.snippet)
680
+ console.log(` ${r.snippet}`);
681
+ });
682
+ process.exit(0);
683
+ }
684
+ else if (sub === 'owner') {
685
+ // BOTH [[governed-related]] relations, distinctly: governors (code: — the verdict) and referencers
686
+ // (related: — pointers; coverage only, never drift, never eval freshness).
687
+ const { specOwners, specRelated } = await import('@spexcode/spec-core');
688
+ const { loadConfig } = await import('./lint.js');
689
+ const p0 = positionals(4)[0];
690
+ if (!p0) {
691
+ console.error('usage: spex spec owner <path> [--actionable]');
692
+ process.exit(2);
693
+ }
694
+ const p = stripRefSigil(p0);
695
+ const rel = p.startsWith(process.cwd()) ? p.slice(process.cwd().length + 1) : p;
696
+ const owners = specOwners(p);
697
+ const related = specRelated(p);
698
+ const maxOwners = loadConfig(process.cwd()).maxOwners;
699
+ // a selector-SCOPED governor (every claiming code: entry carries `#symbol` — [[code-anchor]]) still
700
+ // DISPLAYS, marked, but does not count toward the too-many-owners bound: it claims named units, not
701
+ // the whole file.
702
+ const whole = owners.filter((o) => !o.scoped);
703
+ const names = (xs) => xs.map((o) => `'${o.id}'${o.scoped ? ' (scoped)' : ''}`).join(', ');
704
+ const relLine = related.length ? `\n also referenced by ${names(related)} (related: coverage only — no drift, no eval freshness)` : '';
705
+ if (owners.length === 0 && related.length === 0) {
706
+ console.log(`${rel} — no spec claims this yet (uncovered). If your change is substantive, give it a home before it drifts.`);
707
+ }
708
+ else if (owners.length === 0) {
709
+ // related-only: lint's coverage is satisfied, so the per-edit hook stays silent (lint-consistent) —
710
+ // but a human asking gets the honest nuance about what does and does not track this file.
711
+ if (has('actionable'))
712
+ process.exit(0);
713
+ // @@@ both axes - "nothing tracks its drift" is a verdict about the WHOLE tracking model
714
+ // ([[governed-related]]: spec nodes AND eval scenarios), so it cannot be spoken from the spec axis
715
+ // alone — a scenario's code: anchor drives eval freshness with no spec claim involved. Derived, never
716
+ // asserted: a message that enumerates one axis cannot report what the other holds. The empty
717
+ // nodeCode fallback is exact HERE and only here — reaching this branch means no spec node code:-claims
718
+ // the file, so no scenario can anchor to it by inheriting its node's claim, only by an explicit one.
719
+ const { evalNodes, scenarioCodeAxis } = await import('@spexcode/spec-eval/scenarios');
720
+ const anchored = [];
721
+ for (const n of evalNodes(process.cwd())) {
722
+ for (const sc of n.scenarios) {
723
+ if (scenarioCodeAxis(sc.code).paths.includes(rel))
724
+ anchored.push(`'${n.id}' scenario '${sc.name}'`);
725
+ }
726
+ }
727
+ const tracking = anchored.length
728
+ ? `Its drift is tracked on the eval axis only: ${anchored.length} scenario${anchored.length === 1 ? '' : 's'} anchor${anchored.length === 1 ? 's' : ''} freshness to it (${anchored.join(', ')}), so changing it makes those measurements stale — but no spec body says what it should do`
729
+ : 'Nothing tracks its drift';
730
+ console.log(`${rel} — not governed (no code: claim), but referenced by ${names(related)} (related: coverage only). ${tracking}; if your change is substantive, consider giving it a governing home.`);
731
+ }
732
+ else if (whole.length <= maxOwners) {
733
+ // a sanely-owned file is NOT actionable: --actionable callers (the per-edit spec-of-file hook) stay
734
+ // silent here, so the annotation fires only on an OVER-owned or uncovered file — rare and worth acting on.
735
+ if (has('actionable'))
736
+ process.exit(0);
737
+ const named = names(owners);
738
+ const lead = owners.length === 1 ? `${rel} is governed by ${named} — ${owners[0].desc}` : `${rel} is governed by ${named} (shared, fine).`;
739
+ console.log(`${lead} Read/honor the spec; if your change shifts the intent, update the spec in the SAME commit.${relLine}`);
740
+ }
741
+ else {
742
+ const ids = names(owners);
743
+ console.log(`${rel} is governed whole-file by ${whole.length} specs (all claims: ${ids}) — more than one file should hold. This file does TOO MUCH: SPLIT it so each governor owns its own module (or merge the nodes if they're one concern, or give it a single foundation owner + relate the rest).${relLine}`);
744
+ }
745
+ }
746
+ else if (sub === 'lint') {
747
+ const { specLintReport, pendingTouchesGoverned, DRIFT_GUIDANCE } = await import('./lint.js');
748
+ const pending = flag('pending');
749
+ if (pending && process.env.SPEXCODE_GATE_SCOPE_ONLY === '1') {
750
+ const touches = await pendingTouchesGoverned(process.cwd(), pending);
751
+ if (!touches)
752
+ process.exit(76);
753
+ }
754
+ const report = await specLintReport(undefined, undefined, {
755
+ tip: pending || 'HEAD',
756
+ });
757
+ const { findings } = report;
758
+ const errors = findings.filter((f) => f.level === 'error');
759
+ if (has('json')) {
760
+ console.log(JSON.stringify(report, null, 2));
761
+ await flushExit(errors.length ? 1 : 0);
762
+ }
763
+ for (const f of findings)
764
+ console.error(` ${f.level === 'error' ? '✗' : '•'} ${f.rule}: ${f.msg}`);
765
+ console.error(`spex spec lint: ${errors.length} error(s), ${findings.length - errors.length} warning(s)`);
766
+ // drift teaches from the ONE `spex spec lint` (no flag). Unanchored drift stays advisory forever; the
767
+ // Blocking tier is anchor-drift ([[code-anchor]]) — CI/default lint judges HEAD; the local armed hook
768
+ // supplies --pending <real commit oid>; canonical pre-commit leaves the whole lint to that one run.
769
+ if (findings.some((f) => f.rule === 'drift' || f.rule === 'anchor-drift'))
770
+ console.error(`\n${DRIFT_GUIDANCE}`);
771
+ process.exit(errors.length ? 1 : 0);
772
+ }
773
+ else if (sub === 'ack') {
774
+ // An EMPTY stamp commit on top of HEAD, never an amend: driftFor (git.ts) quiets every drift commit
775
+ // REACHABLE from an ack, so a child stamp covers exactly what amending HEAD would — and it works where
776
+ // amend can't: on a trunk merge commit, re-authoring it after MERGE_HEAD is gone reads to main-guard as
777
+ // a direct trunk commit. The guard passes the stamp through its tree-unchanged gate instead. `--only`
778
+ // with no paths pins the commit to HEAD's tree even when the index is dirty — an ack must never sweep
779
+ // staged files along. git de-dupes adjacent trailers, so re-acking is harmless.
780
+ const { git } = await import('@spexcode/spec-core');
781
+ const nodes = positionals(4);
782
+ const reason = (flag('reason') ?? '').trim();
783
+ if (!nodes.length || !reason) {
784
+ console.error('usage: spex spec ack <node-id>… --reason "<why this change keeps each spec valid>"');
785
+ console.error(' --reason is required (it forces you to check before acking) and is recorded in the ack commit\'s message body — an ack that quiets an anchor hit is a strong claim, so its why must be durable.');
786
+ process.exit(2);
787
+ }
788
+ try {
789
+ git(['commit', '--only', '--allow-empty', '-m', `ack: Spec-OK ${nodes.join(', ')}`, '-m', reason,
790
+ ...nodes.flatMap((n) => ['--trailer', `Spec-OK: ${n}`])]);
791
+ console.log(`Spec-OK: ${nodes.join(', ')} → ${git(['rev-parse', '--short', 'HEAD']).trim()} (empty stamp commit; reason recorded in the commit body)`);
792
+ }
793
+ catch (e) {
794
+ console.error(`ack failed: ${e?.message ?? e}`);
795
+ process.exit(1);
796
+ }
797
+ }
798
+ else {
799
+ console.error(`spex spec: unknown verb '${sub}' — search | owner | lint | ack (spex help spec)`);
800
+ process.exit(2);
801
+ }
802
+ }
803
+ else if (cmd === 'init') {
804
+ // scaffold a repo to adopt SpexCode: copy the shipped DATA templates (seed spec tree + git hooks)
805
+ // into <targetDir> (default cwd). spex init [targetDir] --harness <ids> [--preset <tier>]
806
+ const { specInit } = await import('./init.js');
807
+ await specInit(positionals(3)[0], flag('preset'), flag('harness'));
808
+ }
809
+ else if (cmd === 'uninstall') {
810
+ // the surgical inverse of init: remove every SpexCode-generated artifact (harness shims/contract/trust, the
811
+ // .gitignore block, the global store, any plugin bundle) — NEVER the user's .spec/.plugins data or their own
812
+ // prose. Git hooks preserved unless --hooks. spex uninstall [targetDir] [--hooks]
813
+ const { uninstall } = await import('./uninstall.js');
814
+ uninstall(positionals(3)[0], { hooks: has('hooks') });
815
+ }
816
+ else if (cmd === 'eval') {
817
+ const sub = process.argv[3];
818
+ if (sub === undefined) {
819
+ console.log((await import('./help.js')).commandHelp('eval'));
820
+ }
821
+ else if (sub === 'ls' && flag('session') !== undefined) {
822
+ // the session EVAL read ([[session-eval]]'s interactive face as a CLI verb): the dashboard Eval tab's
823
+ // text twin. Renders the session's changed nodes with each DECLARED scenario at its CURRENT score
824
+ // (latest reading per scenario, worktree-rooted) — filed results keep the backend's newest-first order
825
+ // across ownership, the session's OWN measurements are ✦-marked, and blind spots follow. --export writes
826
+ // the self-contained HTML artifact instead.
827
+ const id = await resolveSelectorOrExit(flag('session'));
828
+ if (has('export'))
829
+ await evalExport(id);
830
+ const { clientEvals } = await import('./client.js');
831
+ const r = await clientEvals(id);
832
+ if (!r.ok) {
833
+ console.error(`no evals for ${id} (status ${r.status})`);
834
+ process.exit(1);
835
+ }
836
+ if (has('json')) {
837
+ console.log(JSON.stringify(r.model, null, 2));
838
+ await flushExit(0);
839
+ }
840
+ const m = r.model;
841
+ const own = m.items.filter((item) => item.inSession).length;
842
+ console.log(`eval session [${m.id}]`);
843
+ if (m.gates.length)
844
+ console.log(` gates : ${m.gates.map((g) => `${g.ok ? '✓' : '✗'} ${g.label} — ${g.detail}`).join(' · ')}`);
845
+ if (own)
846
+ console.log(` ✦ : ${own} scenario(s) measured by THIS session (unmarked rows = evals filed by other sessions)`);
847
+ if (!m.items.length)
848
+ console.log('\n no affected scenarios to evaluate yet');
849
+ let lastNode = null;
850
+ for (const e of m.items) {
851
+ if (e.node !== lastNode) {
852
+ console.log(`\n${e.node}`);
853
+ lastNode = e.node;
854
+ }
855
+ if (e.filterKind === 'blind') {
856
+ console.log(` ∅ unmeasured ${e.scenario} — declared, never measured (blind spot)`);
857
+ continue;
858
+ }
859
+ const verdict = e.verdict?.status === 'pass' ? '✓ pass' : e.verdict?.status === 'fail' ? '✗ fail' : '· unscored';
860
+ const stale = e.fresh ? '' : ` (stale: ${(e.staleAxes || []).join(',')})`;
861
+ console.log(` ${e.inSession ? '✦' : ' '} ${verdict}${stale} ${e.scenario} — ${e.ts}${e.evaluator ? ` · ${e.evaluator}` : ''}`);
862
+ }
863
+ }
864
+ else if (['add', 'ls', 'scenario', 'lint', 'ok', 'retract', 'clean'].includes(sub)) {
865
+ // node-scoped verbs — thin route; the logic lives in spec-eval.
866
+ const { runEval } = await import('@spexcode/spec-eval/cli');
867
+ await flushExit(await runEval(process.argv.slice(3)));
868
+ }
869
+ else {
870
+ console.error(`spex eval: unknown verb '${sub}' — add | ls | scenario ls/write | lint | ok | retract | clean (spex help eval)`);
871
+ if (!sub.startsWith('--'))
872
+ console.error(` (the old \`spex eval <SEL>\` session read is now \`spex eval ls --session <SEL>\` [--export])`); // dead-words-ok: signpost — one-version tombstone teaching the renamed spelling (0.4.0 removes it)
873
+ process.exit(2);
874
+ }
875
+ }
876
+ else if (cmd === 'evidence') {
877
+ if (process.argv[3] === undefined) {
878
+ console.log((await import('./help.js')).commandHelp('evidence'));
879
+ }
880
+ else {
881
+ const { runEvidence } = await import('@spexcode/spec-eval/cli');
882
+ await flushExit(await runEvidence(process.argv.slice(3)));
883
+ }
884
+ }
885
+ else if (cmd === 'issue') {
886
+ if (process.argv[3] === undefined) {
887
+ console.log((await import('./help.js')).commandHelp('issue'));
888
+ }
889
+ else {
890
+ const { runIssues } = await import('./issues-cli.js');
891
+ await flushExit(await runIssues(process.argv.slice(3)));
892
+ }
893
+ }
894
+ else if (cmd === 'remark') {
895
+ const sub = process.argv[3];
896
+ const m = sub === 'add' || sub === 'resolve' || sub === 'retract' ? await import('./issues-cli.js') : null;
897
+ if (sub === undefined) {
898
+ console.log((await import('./help.js')).commandHelp('remark'));
899
+ }
900
+ else if (sub === 'add') {
901
+ await flushExit(await m.runRemark(process.argv.slice(4)));
902
+ }
903
+ else if (sub === 'resolve') {
904
+ await flushExit(await m.runResolve(process.argv.slice(4)));
905
+ }
906
+ else if (sub === 'retract') {
907
+ await flushExit(await m.runRetract(process.argv.slice(4)));
908
+ }
909
+ else {
910
+ console.error(`spex remark: unknown verb '${sub}' — add | resolve | retract (spex help remark)`);
911
+ if (!sub.startsWith('--'))
912
+ console.error(' (the old bare `spex remark <host> --body …` write is now `spex remark add <host> --body …`)');
913
+ process.exit(2);
914
+ }
915
+ }
916
+ else if (cmd === 'materialize') {
917
+ const { materialize } = await import('./materialize.js');
918
+ try {
919
+ console.log(`materialized — content-hash ${materialize().contentHash}`);
920
+ }
921
+ catch (e) {
922
+ // a policy error (e.g. a missing/illegal `harnesses` set) is a user-facing verdict, not a crash — one
923
+ // line + the repair it already carries, never a stack trace.
924
+ console.error(`spex materialize: ${e.message}`);
925
+ process.exit(1);
926
+ }
927
+ }
928
+ else if (cmd === 'doctor') {
929
+ const { runDoctor } = await import('./doctor.js');
930
+ await flushExit(await runDoctor(process.argv.slice(3)));
931
+ }
932
+ else if (cmd === 'flat') {
933
+ const { runFlat } = await import('./flat.js');
934
+ await flushExit(await runFlat(process.argv.slice(3)));
935
+ }
936
+ else if (cmd === 'peer') {
937
+ const sub = process.argv[3];
938
+ if (sub === undefined) {
939
+ console.log((await import('./help.js')).commandHelp('peer'));
940
+ }
941
+ else if (sub === 'connect' || sub === 'disconnect') {
942
+ rejectUnknownFlags(`spex peer ${sub}`, 4, []);
943
+ const address = positionals(4);
944
+ if (address.length !== 1) {
945
+ console.error(`usage: spex peer ${sub} <SSH-ADDRESS>`);
946
+ process.exit(2);
947
+ }
948
+ const { peerControlOrThrow } = await import('./machine-peer.js');
949
+ try {
950
+ const peer = await peerControlOrThrow({ op: sub, sshAddress: address[0] });
951
+ if (!Array.isArray(peer))
952
+ console.log(`${sub === 'connect' ? 'connected' : 'disconnected'} ${peer.sshAddress} (${peer.machineId})`);
953
+ }
954
+ catch (error) {
955
+ console.error(`spex peer ${sub}: ${error.message}`);
956
+ process.exit(1);
957
+ }
958
+ }
959
+ else if (sub === 'ls') {
960
+ rejectUnknownFlags('spex peer ls', 4, ['json']);
961
+ const { peerControlOrThrow } = await import('./machine-peer.js');
962
+ try {
963
+ const peers = await peerControlOrThrow({ op: 'list' });
964
+ if (!Array.isArray(peers))
965
+ throw new Error('peer service returned an invalid list');
966
+ if (has('json'))
967
+ console.log(JSON.stringify(peers, null, 2));
968
+ else
969
+ for (const peer of peers)
970
+ console.log(`${peer.state.padEnd(10)} ${peer.sshAddress} ${peer.machineId} ${peer.owner ? 'owner' : 'reverse'}`);
971
+ }
972
+ catch (error) {
973
+ console.error(`spex peer ls: ${error.message}`);
974
+ process.exit(1);
975
+ }
976
+ }
977
+ else {
978
+ console.error(`spex peer: unknown verb '${sub}' — connect | ls | disconnect (spex help peer)`);
979
+ process.exit(2);
980
+ }
981
+ }
982
+ else if (cmd === 'session') {
983
+ const sub = process.argv[3];
984
+ if (sub === undefined) {
985
+ console.log((await import('./help.js')).commandHelp('session'));
986
+ }
987
+ else if (SESSION_SIGNPOSTS[sub]) {
988
+ signpost(`spex session ${sub}`, SESSION_SIGNPOSTS[sub]);
989
+ }
990
+ else if (sub === 'new') {
991
+ // spex session new "<prompt>" (prompt = first positional or --prompt, or --prompt-file
992
+ // <path>|- so a long multi-paragraph prompt never fights shell quoting — [[prompt-file]]).
993
+ // createSession POSTs to the running backend so the launch runs in the backend's process (auth env + cap);
994
+ // it falls back to an in-process launch only when no backend answers.
995
+ if (has('node')) {
996
+ console.error('spex session new: --node was removed — put a [[<id>]] mention in the prompt — the first mention binds');
997
+ process.exit(2);
998
+ }
999
+ const newPositionals = positionals(4);
1000
+ const peerAnchor = parseSessionPeerAnchor('new', newPositionals);
1001
+ rejectUnknownBackendFlags('spex session new', 4, ['prompt', 'prompt-file', 'launcher', 'name', 'base', 'ssh']);
1002
+ if (peerAnchor && newPositionals.length > 2)
1003
+ sessionPeerAnchorUsage('new', '--ssh accepts one full-id anchor and one inline prompt at most');
1004
+ const { createSession, ownSessionId, withPeerSenderHint } = await import('./sessions.js');
1005
+ const promptFile = flag('prompt-file');
1006
+ const explicitPrompt = flag('prompt');
1007
+ if (peerAnchor && explicitPrompt !== undefined && newPositionals.length !== 1)
1008
+ sessionPeerAnchorUsage('new', 'give the prompt either after the full-id anchor or via --prompt, not both');
1009
+ const inline = explicitPrompt ?? newPositionals[peerAnchor ? 1 : 0];
1010
+ let prompt = inline ?? '';
1011
+ if (promptFile !== undefined) {
1012
+ // fail-loud exclusive: never silently pick one of two prompt sources.
1013
+ if (inline !== undefined) {
1014
+ console.error('spex session new: give the prompt either inline or via --prompt-file, not both');
1015
+ process.exit(2);
1016
+ }
1017
+ const { readFileSync } = await import('node:fs');
1018
+ try {
1019
+ prompt = promptFile === '-' ? readFileSync(0, 'utf8') : readFileSync(promptFile, 'utf8');
1020
+ }
1021
+ catch (e) {
1022
+ console.error(`spex session new: --prompt-file ${promptFile}: ${e instanceof Error ? e.message : e}`);
1023
+ process.exit(2);
1024
+ }
1025
+ if (!prompt.trim()) {
1026
+ console.error(`spex session new: --prompt-file ${promptFile === '-' ? 'stdin' : promptFile} is empty — refusing a promptless launch`);
1027
+ process.exit(2);
1028
+ }
1029
+ }
1030
+ let peerPrompt = prompt;
1031
+ if (peerAnchor) {
1032
+ const senderId = ownSessionId();
1033
+ if (senderId) {
1034
+ const { readPeerMachineId } = await import('./machine-peer.js');
1035
+ peerPrompt = withPeerSenderHint(prompt, { id: senderId, label: null }, peerAnchor.sshAddress, readPeerMachineId());
1036
+ }
1037
+ }
1038
+ const created = peerAnchor
1039
+ ? await (await import('./client.js')).clientCreateThroughPeer(peerAnchor.sshAddress, peerAnchor.sessionId, {
1040
+ prompt: peerPrompt, launcher: flag('launcher') ?? undefined, name: flag('name') ?? undefined, base: flag('base') ?? undefined,
1041
+ })
1042
+ : await createSession(prompt, flag('launcher') ?? undefined, flag('name') ?? undefined, flag('base') ?? undefined);
1043
+ let watchEstablished = false;
1044
+ if (!peerAnchor && created.parent && created.parent === ownSessionId()) {
1045
+ try {
1046
+ const { subscribeSessionWatch } = await import('./sessions.js');
1047
+ await subscribeSessionWatch(created.parent, [created.id], 'parent');
1048
+ watchEstablished = true;
1049
+ }
1050
+ catch (error) {
1051
+ console.error(`spex session new: child ${created.id} was created, but its managed watch was not established: ${error instanceof Error ? error.message : String(error)}`);
1052
+ }
1053
+ }
1054
+ console.log(JSON.stringify(created, null, 2));
1055
+ const help = await import('./help.js');
1056
+ console.error(peerAnchor
1057
+ ? help.peerSessionLaunchReceipt(created.id, peerAnchor.sshAddress)
1058
+ : help.sessionLaunchReceipt(created.id, watchEstablished));
1059
+ }
1060
+ else if (sub === 'ls') {
1061
+ // pretty list of living sessions + states. `spex session ls [SEL...] [--children[=PARENT-SEL]] [--status a,b] [--json]`
1062
+ // the board comes from the backend (so it shows the sessions of whatever SPEXCODE_API_URL points at,
1063
+ // incl. a remote machine); selectSessions/formatTable are pure presentation, applied client-side.
1064
+ const { ownSessionId, resolveSession, selectChildren, selectSessions, formatTable } = await import('./sessions.js');
1065
+ const { clientListSessions, clientListSessionsThroughPeer, clientSessionClosure } = await import('./client.js');
1066
+ // The backend's default projection excludes cold archives. --all and an explicit selector request the
1067
+ // history projection so an operator can still inspect or unarchive one deliberately.
1068
+ const selectors = positionals(4);
1069
+ rejectUnknownBackendFlags('spex session ls', 4, ['status', 'all', 'json', 'ssh', 'children'], ['children']);
1070
+ const peerAnchor = parseSessionPeerAnchor('ls', selectors);
1071
+ const children = childrenScopeOption();
1072
+ if (peerAnchor && selectors.length !== 1)
1073
+ sessionPeerAnchorUsage('ls', '--ssh accepts exactly one full-id project anchor, not session selectors');
1074
+ if (peerAnchor && has('all'))
1075
+ sessionPeerAnchorUsage('ls', '--all is unavailable through a machine peer');
1076
+ if (peerAnchor && children === null)
1077
+ sessionPeerAnchorUsage('ls', '--children over a machine peer requires --children=<remote-parent-SEL>');
1078
+ const visible = peerAnchor
1079
+ ? await clientListSessionsThroughPeer(peerAnchor.sshAddress, peerAnchor.sessionId)
1080
+ : await clientListSessions(has('all') || selectors.length > 0);
1081
+ let scoped = visible;
1082
+ let scope = { kind: 'sessions' };
1083
+ if (children !== undefined) {
1084
+ let parent;
1085
+ if (children === null) {
1086
+ parent = ownSessionId() || '';
1087
+ if (!parent) {
1088
+ console.error('spex session ls: --children needs a governed caller session; use --children=<PARENT-SEL> from a human shell');
1089
+ process.exit(2);
1090
+ }
1091
+ }
1092
+ else {
1093
+ const resolved = resolveSession(children, visible);
1094
+ if ('ok' in resolved)
1095
+ parent = resolved.ok.id;
1096
+ else if ('ambiguous' in resolved) {
1097
+ console.error(`spex session ls: parent selector ${children} is ambiguous: ${resolved.ambiguous.map((session) => session.id.slice(0, 8)).join(', ')}`);
1098
+ process.exit(2);
1099
+ }
1100
+ else {
1101
+ const pointed = [...new Set(visible.flatMap((session) => session.parent && (session.parent === children || session.parent.startsWith(children)) ? [session.parent] : []))];
1102
+ if (pointed.length === 1)
1103
+ parent = pointed[0];
1104
+ else if (pointed.length > 1) {
1105
+ console.error(`spex session ls: parent selector ${children} is ambiguous: ${pointed.map((id) => id.slice(0, 8)).join(', ')}`);
1106
+ process.exit(2);
1107
+ }
1108
+ else {
1109
+ console.error(`spex session ls: no session or child-parent pointer matches ${children}`);
1110
+ process.exit(2);
1111
+ }
1112
+ }
1113
+ }
1114
+ scoped = selectChildren(visible, parent);
1115
+ scope = { kind: 'children', parent };
1116
+ }
1117
+ const selected = selectSessions(scoped, peerAnchor ? [] : selectors);
1118
+ const picked = flag('status') ? selected.filter((session) => flag('status').split(',').includes(session.status)) : selected;
1119
+ if (!peerAnchor && children === undefined && selectors.length === 1 && !selected.length && idLikeSelector(selectors[0])) {
1120
+ const closure = await clientSessionClosure(selectors[0]);
1121
+ if (closure) {
1122
+ const result = { id: closure.id, status: 'closed', closedAt: closure.closedAt };
1123
+ console.log(has('json') ? JSON.stringify([result], null, 2) : `${closure.id.slice(0, 8)}: closed at ${closure.closedAt}`);
1124
+ }
1125
+ else {
1126
+ console.error(`spex session ls: ${selectors[0]} was not found in this project's live, archive, or terminal-close history`);
1127
+ process.exit(2);
1128
+ }
1129
+ }
1130
+ else {
1131
+ console.log(has('json') ? JSON.stringify(picked, null, 2) : formatTable(picked, true, scope));
1132
+ }
1133
+ }
1134
+ else if (sub === 'resources') {
1135
+ rejectUnknownBackendFlags('spex session resources', 4, ['json']);
1136
+ const { clientResources } = await import('./client.js');
1137
+ const report = await clientResources();
1138
+ if (has('json'))
1139
+ console.log(JSON.stringify(report, null, 2));
1140
+ else
1141
+ console.log((await import('./host-resources.js')).formatResourceReport(report));
1142
+ }
1143
+ else if (sub === 'files') {
1144
+ rejectUnknownFlags('spex session files', 4, []);
1145
+ const [verb, path, extra] = positionals(4);
1146
+ if (extra || !verb || (verb !== 'ls' && !path) || (verb === 'ls' && path)) {
1147
+ console.error('usage: spex session files add <path> | ls | retract <path>');
1148
+ process.exit(2);
1149
+ }
1150
+ const { ownSessionId, withSessionRecordLockSync } = await import('./sessions.js');
1151
+ const id = ownSessionId();
1152
+ if (!id) {
1153
+ console.error('spex session files: no governed caller session — run this from the agent session that produced the file');
1154
+ process.exit(2);
1155
+ }
1156
+ const files = await import('./session-files.js');
1157
+ if (verb === 'ls') {
1158
+ for (const file of files.listSessionFiles(id))
1159
+ console.log(file);
1160
+ }
1161
+ else if (verb === 'add') {
1162
+ const result = files.addSessionFile(id, path, withSessionRecordLockSync);
1163
+ console.log(result.added ? `posted ${result.path}` : `already posted ${result.path}`);
1164
+ }
1165
+ else if (verb === 'retract') {
1166
+ const result = files.retractSessionFile(id, path, withSessionRecordLockSync);
1167
+ if (!result.removed) {
1168
+ console.error(`spex session files retract: path is not posted: ${result.path}`);
1169
+ process.exit(2);
1170
+ }
1171
+ console.log(`retracted ${result.path}`);
1172
+ }
1173
+ else {
1174
+ console.error(`spex session files: unknown verb '${verb}' — add | ls | retract (spex help session)`);
1175
+ process.exit(2);
1176
+ }
1177
+ }
1178
+ else if (sub === 'web') {
1179
+ rejectUnknownFlags('spex session web', 4, []);
1180
+ const [verb, url, extra] = positionals(4);
1181
+ if (extra || !verb || (verb !== 'ls' && !url) || (verb === 'ls' && url)) {
1182
+ console.error('usage: spex session web add <url> | ls | retract <url>');
1183
+ process.exit(2);
1184
+ }
1185
+ const { ownSessionId, withSessionRecordLockSync } = await import('./sessions.js');
1186
+ const id = ownSessionId();
1187
+ if (!id) {
1188
+ console.error('spex session web: no governed caller session — run this from the agent session that started the local service');
1189
+ process.exit(2);
1190
+ }
1191
+ const web = await import('./session-web.js');
1192
+ if (verb === 'ls') {
1193
+ for (const entry of web.listSessionWebs(id))
1194
+ console.log(entry.url);
1195
+ }
1196
+ else if (verb === 'add') {
1197
+ const result = web.addSessionWeb(id, url, withSessionRecordLockSync);
1198
+ console.log(result.added ? `posted ${result.url}` : `already posted ${result.url}`);
1199
+ }
1200
+ else if (verb === 'retract') {
1201
+ const result = web.retractSessionWeb(id, url, withSessionRecordLockSync);
1202
+ if (!result.removed) {
1203
+ console.error(`spex session web retract: URL is not posted: ${result.url}`);
1204
+ process.exit(2);
1205
+ }
1206
+ console.log(`retracted ${result.url}`);
1207
+ }
1208
+ else {
1209
+ console.error(`spex session web: unknown verb '${verb}' — add | ls | retract (spex help session)`);
1210
+ process.exit(2);
1211
+ }
1212
+ }
1213
+ else if (sub === 'watch') {
1214
+ const [verb, ...rest] = positionals(4);
1215
+ const { ownSessionId, subscribeSessionWatch, listSessionWatches, cancelSessionWatch } = await import('./sessions.js');
1216
+ const watcher = ownSessionId();
1217
+ if (verb === 'stream') {
1218
+ const kit = await followKit(rest, 'spex session watch stream');
1219
+ await kit.follow((line) => console.log(line), {
1220
+ statuses: flag('status')?.split(','),
1221
+ includeIdle: has('idle'),
1222
+ as: flag('as'),
1223
+ intervalMs: (Number(flag('interval')) || 1) * 1000,
1224
+ });
1225
+ }
1226
+ else if (verb === 'list') {
1227
+ if (!watcher) {
1228
+ console.error('spex session watch list: no governed caller session — this caller has no durable watch relation');
1229
+ process.exit(2);
1230
+ }
1231
+ for (const watch of listSessionWatches(watcher))
1232
+ console.log(`${watch.target}\t${watch.createdAt}`);
1233
+ }
1234
+ else if (verb === 'cancel') {
1235
+ if (!watcher) {
1236
+ console.error(`spex session watch cancel: no governed caller session — run \`spex session wait ${rest.join(' ')}\` in the background instead`);
1237
+ process.exit(2);
1238
+ }
1239
+ const targets = await localWatchTargetsOrExit(rest, 'spex session watch cancel');
1240
+ console.log(`cancelled ${cancelSessionWatch(watcher, targets)} watch${targets.length === 1 ? '' : 'es'}`);
1241
+ }
1242
+ else {
1243
+ const selectors = [verb, ...rest].filter(Boolean);
1244
+ if (!watcher) {
1245
+ if (!selectors.length) {
1246
+ console.error('usage: spex session watch <SEL...>');
1247
+ process.exit(2);
1248
+ }
1249
+ console.error(`spex session watch: no governed caller session — run \`spex session wait ${selectors.join(' ')}\` in the BACKGROUND to receive the next actionable transition`);
1250
+ }
1251
+ else {
1252
+ const targets = await localWatchTargetsOrExit(selectors, 'spex session watch');
1253
+ const result = await subscribeSessionWatch(watcher, targets);
1254
+ console.log(`watching ${result.watched.join(' ')}`);
1255
+ }
1256
+ }
1257
+ }
1258
+ else if (sub === 'wait') {
1259
+ const selectors = positionals(4);
1260
+ const kit = await followKit(selectors, 'spex session wait');
1261
+ const named = selectors.join(' ') || 'your own log';
1262
+ // point-of-use turn-freeze warning ([[session-follow]]): a managed agent that runs this wait in the FOREGROUND
1263
+ // freezes its whole turn until the target produces an edge — a warning that used to live only in help
1264
+ // prose, now said where it matters. Foreground vs background is invisible from here, so the hint prints
1265
+ // for ANY managed-agent shell (harmless in a background transcript), on stderr, and changes nothing else.
1266
+ if (kit.watcher)
1267
+ console.error(`spex session wait: heads-up (managed agent ${kit.watcher.slice(0, 8)}) — this command BLOCKS until it OBSERVES ${named} transition from non-actionable into an actionable status, or a message arrives for you (edge-triggered: an already-actionable current state does NOT return it — to just read the state now, use \`spex session ls\`/\`review\`); run it in the BACKGROUND or it freezes your whole turn (its exit is your wake-up). Proceeding.`);
1268
+ const timeoutSec = Number(flag('timeout')) || 1200;
1269
+ const r = await kit.follow(() => { }, {
1270
+ includeIdle: has('idle'),
1271
+ intervalMs: (Number(flag('interval')) || 1) * 1000,
1272
+ take: true,
1273
+ timeoutMs: timeoutSec * 1000,
1274
+ // the arrival state and each observed transition narrate on stderr AS THEY HAPPEN, so a backgrounded
1275
+ // wait's transcript is the state sequence itself; stdout stays the one machine verdict.
1276
+ onObserved: (sid, st, was) => console.error(was
1277
+ ? `spex session wait: observed ${sid.slice(0, 8)} ${was} → ${st}`
1278
+ : `spex session wait: ${sid.slice(0, 8)} current status ${st} — recorded as the path start; returns on the next non-actionable→actionable transition`),
1279
+ });
1280
+ // the observed status path is the stdout verdict: read the LAST token as the status reached. Printing the
1281
+ // whole path (not just the final status) is the point — a manager sees what the wait lived through
1282
+ // (e.g. review→working→close-pending across a merge dispatch), not a bare word out of context.
1283
+ if ('reached' in r) {
1284
+ console.log(r.path.join('→'));
1285
+ process.exit(0);
1286
+ }
1287
+ if ('mail' in r) {
1288
+ console.error(`spex session wait: message from ${r.mail.from ?? 'a human'} — ${r.mail.text}`);
1289
+ console.log('message');
1290
+ process.exit(0);
1291
+ }
1292
+ if ('gone' in r) {
1293
+ console.error(`spex session wait: session ${r.gone} is gone — its store dir no longer exists`);
1294
+ process.exit(2);
1295
+ }
1296
+ console.error(`spex session wait: timeout — observed no non-actionable→actionable transition on ${named} within ${timeoutSec}s (status path: ${r.path.join('→') || 'never sighted'})`);
1297
+ process.exit(1);
1298
+ }
1299
+ else if (sub === 'review') {
1300
+ const first = positionals(4)[0];
1301
+ if (first === 'proof')
1302
+ signpost('spex review proof', 'spex eval ls --session <SEL> --export'); // dead-words-ok: signpost — one-version tombstone teaching the renamed spelling (0.4.0 removes it)
1303
+ const { clientReview } = await import('./client.js');
1304
+ if (!first) {
1305
+ console.error('usage: spex session review <SEL> (id | id-prefix | node | branch)');
1306
+ process.exit(2);
1307
+ }
1308
+ const session = await resolveSessionOrExit(first);
1309
+ const r = await clientReview(session.id);
1310
+ if (!r) {
1311
+ console.error(`no such session ${session.id}`);
1312
+ process.exit(1);
1313
+ }
1314
+ if (has('json')) {
1315
+ console.log(JSON.stringify(r, null, 2));
1316
+ }
1317
+ else {
1318
+ const g = r.gates;
1319
+ console.log(`review ${session.title} [${r.id}]`);
1320
+ console.log(` ahead of main : ${r.ahead} commit(s)`);
1321
+ console.log(` uncommitted : ${r.dirtyNonRuntime} non-runtime file(s)`);
1322
+ console.log(` proposal : ${r.proposal.kind ?? '—'}${r.proposal.note ? ` — ${r.proposal.note}` : ''}`);
1323
+ console.log(' gates:');
1324
+ console.log(` conflicts w/ main : ${g.conflictsWithMain ? 'YES' : 'no'}`);
1325
+ console.log(` lint : ${g.lint.errorCount} error(s), ${g.lint.warningCount} warning(s)`);
1326
+ // measured loss is REPORTED, never graded — and a projection that isn't ready says so instead of
1327
+ // printing four zeros that would read as a clean session.
1328
+ console.log(` evals : ${g.evals.phase === 'ready'
1329
+ ? `${g.evals.freshPass} fresh pass, ${g.evals.freshFail} fresh fail, ${g.evals.needReview} need review, ${g.evals.blind} blind`
1330
+ : `not measured yet (${g.evals.phase})`}`);
1331
+ console.log(` diff (merge-base, ${r.diff.length} file(s)):`);
1332
+ for (const f of r.diff)
1333
+ console.log(` ${f.status.padEnd(12)} +${f.additions} -${f.deletions} ${f.path}`);
1334
+ }
1335
+ }
1336
+ else if (sub === 'merge') {
1337
+ const { clientMerge } = await import('./client.js');
1338
+ const sel = positionals(4)[0];
1339
+ if (!sel) {
1340
+ console.error('usage: spex session merge <SEL> (id | id-prefix | node | branch)');
1341
+ process.exit(2);
1342
+ }
1343
+ const id = await resolveSelectorOrExit(sel);
1344
+ const r = await clientMerge(id);
1345
+ if (r.dispatched)
1346
+ console.log(`merge dispatched to ${id} — its agent is landing the merge`);
1347
+ else
1348
+ console.error(`merge dispatch failed: ${r.reason}`);
1349
+ process.exit(r.dispatched ? 0 : 1);
1350
+ }
1351
+ else if (sub === 'done' || sub === 'park' || sub === 'ask') {
1352
+ const { runSessionDeclaration } = await import('./session-declarations.js');
1353
+ await runSessionDeclaration(sub, process.argv);
1354
+ }
1355
+ else {
1356
+ // `c` (client.ts) backs the read/control subs that route through the backend. Lazily imported.
1357
+ const sendArgs = sub === 'send' ? parseSessionSendArgs(process.argv.slice(4)) : null;
1358
+ const targetArgs = sub === 'show' || sub === 'close' ? parseSessionTargetArgs(sub, process.argv.slice(4)) : null;
1359
+ const c = await import('./client.js');
1360
+ const id = sendArgs?.selector ?? targetArgs?.selector ?? process.argv[4];
1361
+ if (sub === 'resume') {
1362
+ // bring the agent back up (relaunch ONLY if confirmed offline, the backend owns it); demotes a working
1363
+ // `active` to idle but leaves a standing declaration/proposal untouched (see sessions.ts resumeSession()).
1364
+ // The RESUME GUARD refuses a relaunch on a LIVE/unproven agent (that would kill a live worker) — `--force`
1365
+ // overrides for a genuinely wedged process. A following prompt is what actually re-drives it.
1366
+ const full = await resolveSelectorOrExit(id);
1367
+ const r = await c.clientResume(full, process.argv.includes('--force'));
1368
+ if (r.ok)
1369
+ console.log(r.info ? `${full} -> ${r.info}` : `${full} -> resumed`);
1370
+ else {
1371
+ console.error(`spex session resume: ${r.error || `no such session ${full}`}`);
1372
+ process.exit(2);
1373
+ }
1374
+ }
1375
+ else if (sub === 'stop') {
1376
+ // the SOFT stop: kill the agent's tmux + socket but KEEP the worktree, so the session goes offline and
1377
+ // can be resumed (`session resume`). Distinct from `close`, which removes the worktree.
1378
+ const full = await resolveSelectorOrExit(id);
1379
+ const stopped = await c.clientStop(full);
1380
+ if (!stopped) {
1381
+ console.error(`spex session stop: no such session ${full} (no stop transition was committed)`);
1382
+ process.exit(1);
1383
+ }
1384
+ console.log(`stopped ${full} (worktree kept — resumable)`);
1385
+ }
1386
+ else if (sub === 'interrupt') {
1387
+ const full = await resolveSelectorOrExit(id);
1388
+ const r = await c.clientInterrupt(full);
1389
+ console.log(r.ok ? `interrupted ${full}` : `interrupt failed: ${r.error}`);
1390
+ process.exit(r.ok ? 0 : 1);
1391
+ }
1392
+ else if (sub === 'archive' || sub === 'unarchive') {
1393
+ // ARCHIVING/legacy unarchive ([[archive]]) — archive exact-stops before filing; unarchive signposts to
1394
+ // resume and recreates the same conversation.
1395
+ const on = sub === 'archive';
1396
+ const full = await resolveSelectorOrExit(id);
1397
+ const ok = await c.clientArchive(full, on);
1398
+ console.log(!ok ? `no such session ${full}` : `${on ? 'archived' : 'resumed'} ${full}`);
1399
+ }
1400
+ else if (sub === 'close') {
1401
+ const full = targetArgs?.sshAddress
1402
+ ? (FULL_SESSION_ID.test(id ?? '') ? id : sessionTargetUsage('close', '--ssh requires a full session id, not a selector'))
1403
+ : await resolveSelectorOrExit(id);
1404
+ const closed = targetArgs?.sshAddress
1405
+ ? await c.clientCloseThroughPeer(targetArgs.sshAddress, full)
1406
+ : await c.clientClose(full);
1407
+ if (!closed) {
1408
+ console.error(`spex session close: no such session ${full} (record remains; no close was committed)`);
1409
+ process.exit(1);
1410
+ }
1411
+ console.log(`closed ${full}`);
1412
+ }
1413
+ else if (sub === 'quarantine') {
1414
+ rejectUnknownBackendFlags('spex session quarantine', 4, ['adapter', 'thread', 'tmux', 'worktree', 'branch', 'restore']);
1415
+ if (!id) {
1416
+ console.error('usage: spex session quarantine <ID> --adapter <harness> [--thread <native-id>] --tmux <session-id> --worktree <absent-path> --branch <absent-branch> (--thread is adapter-native; omit it for Claude)');
1417
+ process.exit(2);
1418
+ }
1419
+ if (has('restore')) {
1420
+ // Quarantine addresses an unreadable row which selector resolution intentionally excludes. Both the
1421
+ // move and its reverse therefore take the literal exact id, with the backend proving record state.
1422
+ const restored = await c.clientRestoreQuarantine(id);
1423
+ console.log(`restored quarantined record ${restored.id} from ${restored.bundle}`);
1424
+ }
1425
+ else {
1426
+ const adapter = flag('adapter'), tmux = flag('tmux'), worktree = flag('worktree'), branch = flag('branch');
1427
+ if (!adapter || !tmux || !worktree || !branch) {
1428
+ console.error('usage: spex session quarantine <ID> --adapter <harness> [--thread <native-id>] --tmux <session-id> --worktree <absent-path> --branch <absent-branch> (--thread is adapter-native; omit it for Claude)');
1429
+ process.exit(2);
1430
+ }
1431
+ const quarantined = await c.clientQuarantine(id, { adapter, thread: flag('thread') ?? null, tmux, worktree, branch });
1432
+ console.log(`quarantined ${quarantined.id} -> ${quarantined.bundle}`);
1433
+ }
1434
+ }
1435
+ else if (sub === 'reparent') {
1436
+ rejectUnknownBackendFlags('spex session reparent', 4, ['to']);
1437
+ const children = positionals(4);
1438
+ const to = flag('to');
1439
+ if (!children.length || !to) {
1440
+ console.error('usage: spex session reparent <child-SEL...> --to <parent-SEL>');
1441
+ process.exit(2);
1442
+ }
1443
+ const childIds = [...new Set(await Promise.all(children.map(resolveSelectorOrExit)))];
1444
+ const parent = await resolveSelectorOrExit(to);
1445
+ const { reparentSessions } = await import('./session-reparent.js');
1446
+ const result = await reparentSessions(childIds, parent);
1447
+ console.log(`reparented ${result.children.join(', ')} -> ${result.parent}`);
1448
+ }
1449
+ else if (sub === 'send') {
1450
+ if (!sendArgs)
1451
+ sessionSendUsage('arguments were not parsed');
1452
+ const full = sendArgs.sshAddress
1453
+ ? (FULL_SESSION_ID.test(id ?? '')
1454
+ ? id
1455
+ : sessionSendUsage('--ssh requires a full session id, not a selector'))
1456
+ : await resolveSelectorOrExit(id);
1457
+ if (sendArgs.kind === 'keys') {
1458
+ // the LAST-RESORT face of send: forward raw nav-mode keystrokes (tmux send-keys, NEVER the prompt
1459
+ // socket) — how a manager drives a worker wedged in an interactive TUI dialog the prompt channel
1460
+ // can't reach (a select menu wanting one Enter/arrow). UNSTABLE, and able to confirm dangerous
1461
+ // dialogs — try plain `session send` text FIRST; reach for --keys only when text provably can't
1462
+ // land. Tokens = named keys, single chars, C-/M-/S- combos; whitespace-separated, delivered as ONE
1463
+ // ordered batch ([[nav-mode-key-ordering]]). Fail-loud: nothing delivered exits non-zero.
1464
+ if (await c.clientSendRawKeys(full, sendArgs.keys)) {
1465
+ console.log(`sent ${sendArgs.keys.length} key${sendArgs.keys.length === 1 ? '' : 's'} -> ${full}`);
1466
+ process.exit(0);
1467
+ }
1468
+ console.error(`spex session send --keys: nothing delivered to ${full} (offline, unknown session, or no valid key token)`);
1469
+ process.exit(1);
1470
+ }
1471
+ // A send is accepted at its timeline append, except a proven-unreachable transport attached to a live
1472
+ // registered agent: that stranded combination refuses before it can add unclaimable queue debt.
1473
+ // BIDIRECTIONAL: stamp the SENDER (this send process's OWN session — the only process that knows it, via
1474
+ // ownSessionId from CLAUDE_CODE_SESSION_ID) + a one-line reply hint into the delivered
1475
+ // message, so the recipient can reply over the SAME send. The sender's row (hence its display label) is
1476
+ // resolved through the shared resolver; a human in a plain shell has no session id → bare message, no
1477
+ // hint (see [[agent-reply-channel]]).
1478
+ const s = await import('./sessions.js');
1479
+ const senderId = s.ownSessionId();
1480
+ let sender = null;
1481
+ if (senderId) {
1482
+ const sr = await c.resolveClientSession(senderId);
1483
+ // name the sender by the board HEADLINE (sessionHeadline — the live self-summary the recipient sees on
1484
+ // the board), NOT the stable sessionLabel that stops at the bare prompt-truncation title.
1485
+ sender = 'ok' in sr ? { id: sr.ok.id, label: s.sessionHeadline(sr.ok) } : { id: senderId, label: null };
1486
+ }
1487
+ let text = sendArgs.text;
1488
+ let from = senderId ?? undefined;
1489
+ if (sendArgs.sshAddress && sender) {
1490
+ const { peerSenderRef, readPeerMachineId } = await import('./machine-peer.js');
1491
+ const machineId = readPeerMachineId();
1492
+ text = s.withPeerSenderHint(text, sender, sendArgs.sshAddress, machineId);
1493
+ from = peerSenderRef(machineId, sender.id);
1494
+ }
1495
+ else if (!sendArgs.sshAddress) {
1496
+ text = s.withSenderHint(text, sender);
1497
+ }
1498
+ const r = sendArgs.sshAddress
1499
+ ? await c.clientSendThroughPeer(sendArgs.sshAddress, full, text, from)
1500
+ : await c.clientSend(full, text, from);
1501
+ if (r.ok) {
1502
+ console.log('sent');
1503
+ process.exit(0);
1504
+ }
1505
+ console.error(`dispatch failed: ${r.error}`);
1506
+ process.exit(1);
1507
+ }
1508
+ else if (sub === 'show') {
1509
+ // the session RECORD as one per-id read (status · node · branch · launcher · the full originating
1510
+ // prompt); --capture swaps in the LIVE PANE face of the same read. The pane contract is unchanged from
1511
+ // the verb it absorbed — fail and empty stay DISTINCT: a real empty pane prints nothing and exits 0;
1512
+ // unknown id exits 2, offline / capture-error exit 1, each with a named reason.
1513
+ const full = targetArgs?.sshAddress
1514
+ ? (FULL_SESSION_ID.test(id ?? '') ? id : sessionTargetUsage('show', '--ssh requires a full session id, not a selector'))
1515
+ : await resolveSelectorOrExit(id);
1516
+ if (has('capture')) {
1517
+ const r = await c.clientCapture(full);
1518
+ if (r.ok) {
1519
+ process.stdout.write(r.pane);
1520
+ }
1521
+ else {
1522
+ console.error(`spex session show --capture: ${r.reason}`);
1523
+ process.exit(r.status === 404 ? 2 : 1);
1524
+ }
1525
+ }
1526
+ else {
1527
+ const r = targetArgs?.sshAddress
1528
+ ? await c.clientShowThroughPeer(targetArgs.sshAddress, full)
1529
+ : await c.clientShow(full);
1530
+ if (!r.ok) {
1531
+ console.error(`spex session show: no such session ${full}`);
1532
+ process.exit(2);
1533
+ }
1534
+ if (has('json')) {
1535
+ console.log(JSON.stringify(r.session, null, 2));
1536
+ }
1537
+ else {
1538
+ const x = r.session;
1539
+ console.log(`${x.title} [${x.id}]`);
1540
+ console.log(` status : ${x.status} (lifecycle ${x.lifecycle} · liveness ${x.liveness})`);
1541
+ console.log(` node : ${x.node ?? '—'}`);
1542
+ console.log(` branch : ${x.branch ?? '—'}`);
1543
+ console.log(` launcher : ${x.launcher ?? '—'} (harness ${x.harness})`);
1544
+ console.log(` worktree : ${x.path}`);
1545
+ console.log(` created : ${new Date(x.created).toISOString()}`);
1546
+ if (x.note)
1547
+ console.log(` note : ${x.note}`);
1548
+ if (x.proposal)
1549
+ console.log(` proposal : ${x.proposal}`);
1550
+ console.log(x.prompt ? ` prompt |\n${x.prompt.replace(/\n$/, '').split('\n').map((l) => ` ${l}`).join('\n')}` : ' prompt : (none recorded)');
1551
+ }
1552
+ }
1553
+ }
1554
+ else if (sub === 'rename') {
1555
+ // set the session's display-name override — the right-click rename ([[session-rename]]) as a verb, so an
1556
+ // agent manager can fix a label without the GUI. An EXPLICIT "" clears back to the derived label; a
1557
+ // MISSING argument is a usage error, never a silent clear. Unknown session → the endpoint's 404, loud.
1558
+ const full = await resolveSelectorOrExit(id);
1559
+ const name = process.argv[5];
1560
+ if (name === undefined) {
1561
+ console.error('usage: spex session rename <SEL> "<name>" (an explicit "" clears the override)');
1562
+ process.exit(2);
1563
+ }
1564
+ if (await c.clientRename(full, name))
1565
+ console.log(name.trim() ? `${full} -> renamed "${name.trim()}"` : `${full} -> name cleared (derived label restored)`);
1566
+ else {
1567
+ console.error(`spex session rename: no such session ${full}`);
1568
+ process.exit(2);
1569
+ }
1570
+ }
1571
+ else if (sub === 'attach') {
1572
+ // the HUMAN escape hatch (attach.ts, [[session-attach]]): foreground `tmux attach` into the worker's
1573
+ // real session. Guards fail loud BEFORE resolving: local-only (the tmux server is the backend
1574
+ // machine's) and terminal-only (an agent must never block its turn on it — capture/send).
1575
+ const { assertLocalBackend, attachSession } = await import('./attach.js');
1576
+ await assertLocalBackend();
1577
+ process.exit(await attachSession(await resolveSelectorOrExit(id)));
1578
+ }
1579
+ else {
1580
+ console.error(`spex session: unknown verb '${sub}' — new | ls | files | web | show | watch | wait | review | merge | reparent | send | interrupt | rename | resume | stop | close | attach | done | park | ask (spex help session)`);
1581
+ process.exit(2);
1582
+ }
1583
+ }
1584
+ }
1585
+ else if (cmd === 'internal') {
1586
+ const sub = process.argv[3];
1587
+ if (sub === 'peer-accept' || sub === 'peer-drop') {
1588
+ const encoded = process.argv[4];
1589
+ if (!encoded) {
1590
+ console.error(`usage: spex internal ${sub} <base64url-json>`);
1591
+ process.exit(2);
1592
+ }
1593
+ let body;
1594
+ try {
1595
+ const parsed = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
1596
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1597
+ throw new Error('not an object');
1598
+ body = parsed;
1599
+ }
1600
+ catch {
1601
+ console.error(`spex internal ${sub}: invalid base64url JSON`);
1602
+ process.exit(2);
1603
+ }
1604
+ const { peerRpc } = await import('./machine-peer.js');
1605
+ const request = sub === 'peer-accept' ? { ...body, op: 'accept' } : { ...body, op: 'drop' };
1606
+ try {
1607
+ console.log(JSON.stringify(await peerRpc(request)));
1608
+ }
1609
+ catch (error) {
1610
+ console.error(`spex internal ${sub}: ${error.message}`);
1611
+ process.exit(1);
1612
+ }
1613
+ }
1614
+ else if (sub === 'trunk') {
1615
+ // print the resolved source-of-truth branch (layout.ts mainBranch(): config override → the main
1616
+ // checkout's current branch → 'main'). The pre-commit main-guard captures this so it blocks direct
1617
+ // commits on whatever the repo's trunk is actually named, never a hardcoded 'main'. One value, one
1618
+ // line; GET /api/settings exposes the same resolution (`.layout`).
1619
+ const { mainBranch } = await import('@spexcode/spec-core');
1620
+ console.log(mainBranch());
1621
+ }
1622
+ else if (sub === 'spec-governors') {
1623
+ // Stable machine projection for spec-aware hooks: one real code: governor per row, with the live spec
1624
+ // path the block reason can point at. Empty stdout means ungoverned (including related-only).
1625
+ const file = process.argv[4];
1626
+ if (!file) {
1627
+ console.error('usage: spex internal spec-governors <path>');
1628
+ process.exit(2);
1629
+ }
1630
+ const { specOwners, loadSpecsLite } = await import('@spexcode/spec-core');
1631
+ const paths = new Map(loadSpecsLite().map((node) => [node.id, node.path]));
1632
+ for (const owner of specOwners(file)) {
1633
+ const path = paths.get(owner.id);
1634
+ if (!path)
1635
+ throw new Error(`governor '${owner.id}' has no live spec path`);
1636
+ console.log(`${owner.id}\t${path}`);
1637
+ }
1638
+ }
1639
+ else if (sub === 'shared-runtime-spawn') {
1640
+ const [cwd, logFile, pidFile, receiptFile, command] = process.argv.slice(4, 9);
1641
+ const args = process.argv.slice(9);
1642
+ if (!cwd || !logFile || !pidFile || !receiptFile || !command) {
1643
+ console.error('usage: spex internal shared-runtime-spawn <cwd> <log> <pid-file> <receipt-file> <command> [args...]');
1644
+ process.exit(2);
1645
+ }
1646
+ const { spawnDetachedRuntime } = await import('./runtime-ownership.js');
1647
+ const env = { ...process.env };
1648
+ delete env.SPEXCODE_SESSION_ID;
1649
+ const runtime = await spawnDetachedRuntime({ cwd, logFile, pidFile, receiptFile, command, args, env });
1650
+ console.log(runtime.pid);
1651
+ }
1652
+ else if (sub === 'codex-generation-current' || sub === 'codex-generation-session') {
1653
+ // Both launch spellings resolve ONE endpoint and print it as shell assignments. Starting a root is the same
1654
+ // detached spawn either way — a session identity must never ride into the shared, project-scoped daemon.
1655
+ const resume = sub === 'codex-generation-session';
1656
+ const [root, ...rest] = process.argv.slice(4);
1657
+ const [sessionId, threadId, sessionCommand] = rest;
1658
+ const command = resume ? sessionCommand : rest[0];
1659
+ if (!root || !command || (resume && (!sessionId || !threadId))) {
1660
+ console.error(resume
1661
+ ? 'usage: spex internal codex-generation-session <runtime-root> <session-id> <thread-id> <codex-command>'
1662
+ : 'usage: spex internal codex-generation-current <runtime-root> <codex-command>');
1663
+ process.exit(2);
1664
+ }
1665
+ const { ensureCodexCurrentGeneration, resolveCodexGenerationForResume } = await import('./codex-runtime-generations.js');
1666
+ const { spawnDetachedRuntime } = await import('./runtime-ownership.js');
1667
+ const { sessionIdentityEnvVars } = await import('./harness.js');
1668
+ const env = { ...process.env };
1669
+ for (const key of sessionIdentityEnvVars())
1670
+ delete env[key];
1671
+ const start = async (candidate) => {
1672
+ await spawnDetachedRuntime({ cwd: root, logFile: candidate.logFile, pidFile: candidate.pidFile, receiptFile: candidate.receiptFile,
1673
+ command, args: ['app-server', '--listen', `unix://${candidate.socketPath}`], env });
1674
+ };
1675
+ const endpoint = resume
1676
+ ? await resolveCodexGenerationForResume(root, sessionId, threadId, start)
1677
+ : await ensureCodexCurrentGeneration(root, start);
1678
+ if (!endpoint) {
1679
+ console.error(`no exact Codex generation binding for session ${sessionId} thread ${threadId}`);
1680
+ process.exit(1);
1681
+ }
1682
+ const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1683
+ console.log(`sock=${quote(endpoint.socketPath)}; pid=${quote(endpoint.pidFile)}; receipt=${quote(endpoint.receiptFile)}; log=${quote(endpoint.logFile)}; export SPEXCODE_CODEX_GENERATION=${quote(endpoint.id)}`);
1684
+ }
1685
+ else if (sub === 'codex-launch') {
1686
+ // BACKEND-owned codex thread. On the shared per-project app-server: thread/start { cwd = this worktree }
1687
+ // (codex loads that worktree's config/hooks/AGENTS.md), fire the launch prompt as the FIRST turn —
1688
+ // materializing the rollout — then stage the id + exact-payload proof for the session lifecycle owner and
1689
+ // print the thread id. The launch script then `resume`s it in the visible TUI.
1690
+ const { codexStartThread, codexTurn, waitForCodexRollout, codexBinary, codexSupportsBypassHookTrust, codexLauncherThreadPolicy } = await import('./harness.js');
1691
+ const { stageHarnessLaunchProof } = await import('./sessions.js');
1692
+ const sock = process.argv[4], cwd = process.argv[5];
1693
+ const prompt = process.argv.slice(6).join(' ');
1694
+ if (!sock || !cwd) {
1695
+ console.error('usage: spex internal codex-launch <sock> <cwd> [prompt...]');
1696
+ process.exit(2);
1697
+ }
1698
+ // On the bypass-trust path (the codex install supports the flag → materialize skipped writeCodexTrust's hash),
1699
+ // the thread the BACKEND owns must carry `bypass_hook_trust` in thread/start's config so the app-server fires
1700
+ // the worktree's local hooks — mirror materialize's capability decision so the two stay in lockstep.
1701
+ const launcherCmd = process.env.SPEXCODE_CODEX_CMD || 'codex';
1702
+ const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(launcherCmd));
1703
+ // The governed record id rides into the thread's own shell environment (shell_environment_policy.set), so
1704
+ // every command this thread spawns knows which session it is — the codex equivalent of the launch-injected
1705
+ // id claude gets. codex-launch is exactly where both ids are known ([[harness-adapter]]).
1706
+ const ownId = process.env.SPEXCODE_SESSION_ID?.trim();
1707
+ const r = await codexStartThread(sock, cwd, bypassHookTrust, ownId ? { SPEXCODE_SESSION_ID: ownId } : undefined, codexLauncherThreadPolicy(launcherCmd));
1708
+ if (!r.ok) {
1709
+ console.error(r.error);
1710
+ process.exit(1);
1711
+ }
1712
+ if (prompt) {
1713
+ const t = await codexTurn(sock, r.threadId, prompt, cwd);
1714
+ if (!t.ok) {
1715
+ console.error(t.error);
1716
+ process.exit(1);
1717
+ }
1718
+ // The visible TUI resumes this thread from its ON-DISK rollout; a freshly-spawned app-server acks the turn
1719
+ // but persists the rollout a few seconds LATE (verified live: the SAME thread's file lands at ~2-4s, not
1720
+ // lost). WAIT for it to land BEFORE storing the id / printing it, else FAIL LOUD — never store a
1721
+ // non-resumable harness_session_id (that permanently wedges every reopen). The 15s budget exceeds launch.sh's
1722
+ // fast-fail threshold, so a real failure exits past it and the retry loop won't spray duplicate-prompt threads.
1723
+ if (!await waitForCodexRollout(r.threadId, 20000)) {
1724
+ console.error(`codex thread ${r.threadId} started but persisted no rollout within 20s — app-server not ready; not storing a non-resumable id`);
1725
+ process.exit(1);
1726
+ }
1727
+ }
1728
+ const sid = process.env.SPEXCODE_SESSION_ID;
1729
+ if (sid)
1730
+ stageHarnessLaunchProof(sid, r.threadId, prompt);
1731
+ console.log(r.threadId);
1732
+ }
1733
+ else if (sub === 'opencode-capture') {
1734
+ // opencode MINTS its own session id (no launch flag pins it), so the generated plugin's FIRST event calls
1735
+ // this to store that id as harness_session_id on the governed record (SPEXCODE_SESSION_ID from the launch
1736
+ // env, inherited by the opencode process → plugin). That is what lets reopen() resume the SAME
1737
+ // conversation (`--session <id>`). A missing record/env is a clean no-op — a plugin loaded outside a
1738
+ // governed launch has nothing to mark.
1739
+ const { markHarnessSessionId } = await import('./sessions.js');
1740
+ const ocid = process.argv[4];
1741
+ if (!ocid) {
1742
+ console.error('usage: spex internal opencode-capture <opencode-session-id>');
1743
+ process.exit(2);
1744
+ }
1745
+ const sid = process.env.SPEXCODE_SESSION_ID;
1746
+ console.log(sid && markHarnessSessionId(sid, ocid) ? `captured ${ocid}` : 'noop (no governed session record)');
1747
+ }
1748
+ else if (sub === 'claude-headless-run') {
1749
+ const id = process.argv[4], runtimeDir = process.argv[5], claudeCmd = process.argv[6];
1750
+ const divider = process.argv[7];
1751
+ if (!id || !runtimeDir || !claudeCmd || divider !== '--') {
1752
+ console.error('usage: spex internal claude-headless-run <session-id> <runtime-dir> <claude-cmd> -- [--session-id <id> <prompt> | --resume <id>]');
1753
+ process.exit(2);
1754
+ }
1755
+ const { runClaudeHeadlessController } = await import('./claude-headless.js');
1756
+ await runClaudeHeadlessController(id, runtimeDir, claudeCmd, process.argv.slice(8));
1757
+ }
1758
+ else if (sub === 'pi-headless-run') {
1759
+ const id = process.argv[4], runtimeDir = process.argv[5], piCmd = process.argv[6];
1760
+ const divider = process.argv[7];
1761
+ if (!id || !runtimeDir || !piCmd || divider !== '--') {
1762
+ console.error('usage: spex internal pi-headless-run <session-id> <runtime-dir> <pi-cmd> -- [--session-id <id> <prompt> | --session <id>]');
1763
+ process.exit(2);
1764
+ }
1765
+ const { runPiHeadlessController } = await import('./pi-headless.js');
1766
+ await runPiHeadlessController(id, runtimeDir, piCmd, process.argv.slice(8));
1767
+ }
1768
+ else if (sub === 'commit-surgery') {
1769
+ // the pre-commit footprint anchor ([[commit-surgery]]): unconditional materialize + staged-index repair
1770
+ // (strip our sentinel block from staged blobs, unstage HEAD-untracked generated artifacts). Called only
1771
+ // by the planted pre-commit hook; repairs and proceeds, never rejects. Exit non-zero only on an internal
1772
+ // error — the hook treats that as advisory (warn + continue), CI lint remains the real gate.
1773
+ const { commitSurgery } = await import('./commit-surgery.js');
1774
+ commitSurgery();
1775
+ }
1776
+ else if (sub === 'refresh-footprint') {
1777
+ // the post-checkout / post-merge freshness anchor ([[commit-surgery]]): a quiet materialize after a git
1778
+ // state transition (the only events that can move the materialize's inputs — .spec/.plugins arrive by commit,
1779
+ // merge, or checkout). Best-effort and silent on success; hooks call it fire-and-forget.
1780
+ const { materialize } = await import('./materialize.js');
1781
+ try {
1782
+ materialize();
1783
+ }
1784
+ catch (e) {
1785
+ console.error(`spexcode: footprint refresh failed (${e.message})`);
1786
+ process.exit(1);
1787
+ }
1788
+ }
1789
+ else if (sub === 'codex-turn') {
1790
+ // fire a follow-up turn on an OWNED thread over the per-project socket (the delivery channel, exposed for
1791
+ // tests / scripts). steer-vs-start is chosen live from the thread read.
1792
+ const { codexTurn } = await import('./harness.js');
1793
+ const sock = process.argv[4], tid = process.argv[5], text = process.argv.slice(6).join(' ');
1794
+ if (!sock || !tid || !text) {
1795
+ console.error('usage: spex internal codex-turn <sock> <threadId> <text...>');
1796
+ process.exit(2);
1797
+ }
1798
+ const r = await codexTurn(sock, tid, text);
1799
+ if (r.ok) {
1800
+ console.log('ok');
1801
+ }
1802
+ else {
1803
+ console.error(r.error);
1804
+ process.exit(1);
1805
+ }
1806
+ }
1807
+ else if (sub === 'check-staged') {
1808
+ // the pre-commit hook's eval backstop: a staged stray evidence blob or malformed eval.md rejects the
1809
+ // commit. Logic lives in spec-eval; the hook shims here.
1810
+ const { checkStaged } = await import('@spexcode/spec-eval/cli');
1811
+ process.exit(checkStaged());
1812
+ }
1813
+ else if (sub === 'session-state') {
1814
+ // a lifecycle hook authors the session's state: active|awaiting|parked|error|asking
1815
+ // [--propose] [--note] [--session] — the machine face of the typeable done/park/ask declarations.
1816
+ const { s, sess, mark, noRecord, noteEcho } = await stateKit();
1817
+ const st = process.argv[4];
1818
+ const ok = mark(() => s.markState(st, { proposal: flag('propose'), note: flag('note'), sessionId: sess }));
1819
+ console.log(ok.ok ? `state -> ${st}${noteEcho(flag('note'))}` : ok.reason ?? noRecord());
1820
+ }
1821
+ else if (sub === 'session-fail') {
1822
+ // StopFailure is one native source for the shared active-only turn-failure CAS. A declaration or explicit
1823
+ // stop that landed first is authoritative, just as it is for Codex notifications and headless exits.
1824
+ const { s, sess, mark } = await stateKit();
1825
+ const failed = mark(() => s.markTurnFailure(sess, 'claude turn failed'));
1826
+ console.log(failed.ok ? 'marked error' : failed.reason ?? 'noop (session is not live active)');
1827
+ }
1828
+ else if (sub === 'session-turn-fail') {
1829
+ // Headless adapters report an ephemeral turn's non-zero exit through this one shared CAS. A declaration
1830
+ // that landed before teardown wins, so a late child close can never erase an agent-authored state.
1831
+ const sessionId = process.argv[4], harness = process.argv[5], exitCode = process.argv[6];
1832
+ if (!sessionId || !harness || !exitCode) {
1833
+ console.error('usage: spex internal session-turn-fail <session-id> <harness> <exit-code|signal>');
1834
+ process.exit(2);
1835
+ }
1836
+ const { markHeadlessTurnFailure } = await import('./sessions.js');
1837
+ console.log(markHeadlessTurnFailure(sessionId, harness, exitCode) ? `marked error (${harness} ${exitCode})` : 'noop (session is not live active)');
1838
+ }
1839
+ else if (sub === 'session-idle') {
1840
+ // the Notification(idle_prompt) hook marks its session (--session from the payload) idle when claude waits
1841
+ // at its prompt. INFERRED, so guarded active-only: it no-ops unless the current status is exactly `active`,
1842
+ // never clobbering a deliberate awaiting/asking/parked/error declaration. Distinct from `session ask`
1843
+ // (the agent deliberately asking the human) — idle is the undeclared stop the Stop gate missed.
1844
+ const { s, sess, mark } = await stateKit();
1845
+ const idled = mark(() => s.markIdle(sess));
1846
+ console.log(idled.ok ? 'idle' : idled.reason ?? 'noop (no session record, or not active)');
1847
+ }
1848
+ else if (sub === 'commit-gate') {
1849
+ // the Stop gate's deterministic commit check (from cwd = the worktree): exit 0 if the node branch is
1850
+ // ready to declare done, else print the reason and exit 1. Takes the PROPOSAL being judged — `merge`
1851
+ // additionally requires commits ahead of main, `nothing` only a clean tree (see mergeReadiness). Uses
1852
+ // git() so the hook's exported GIT_DIR/GIT_INDEX_FILE don't misdirect repo discovery (see git.ts).
1853
+ const { s } = await stateKit();
1854
+ const kind = positionals(4)[0] === 'nothing' ? 'nothing' : 'merge';
1855
+ const r = s.mergeReadiness(kind);
1856
+ if (r.ready) {
1857
+ console.log('ready');
1858
+ process.exit(0);
1859
+ }
1860
+ console.log(r.reason);
1861
+ process.exit(1);
1862
+ }
1863
+ else if (sub === 'hook-prompt') {
1864
+ // The internal render seam shared by hook handlers and GuidanceCatalog. The hooks call it only on branches
1865
+ // that already emit model-facing text; their hot no-op paths remain pure shell.
1866
+ const name = positionals(4)[0];
1867
+ if (!name) {
1868
+ console.error('usage: spex internal hook-prompt <name> [--variant <name>] [--path <path>] [--owner <owners>] [--details <text>] [--cli <command>] [--propose <name>] [--reason <text>] [--count <n>] [--ids <ids>]');
1869
+ process.exit(2);
1870
+ }
1871
+ const { HookPromptCatalog } = await import('./hook-prompts.js');
1872
+ try {
1873
+ console.log(new HookPromptCatalog().render(name, {
1874
+ variant: flag('variant') ?? '',
1875
+ path: flag('path') ?? '',
1876
+ owner: flag('owner') ?? '',
1877
+ details: flag('details') ?? '',
1878
+ cli: flag('cli') ?? '',
1879
+ proposal: flag('propose') ?? '',
1880
+ reason: flag('reason') ?? '',
1881
+ count: flag('count') ?? '',
1882
+ ids: flag('ids') ?? '',
1883
+ }));
1884
+ }
1885
+ catch (error) {
1886
+ console.error(`spex internal hook-prompt: ${error instanceof Error ? error.message : String(error)}`);
1887
+ process.exit(2);
1888
+ }
1889
+ }
1890
+ else if (sub === 'nudge') {
1891
+ // the post-merge hook prints the (toggle-aware) issue nudge for a merged node — never typed.
1892
+ const { nudge } = await import('./localIssues.js');
1893
+ const text = nudge(positionals(4)[0] || '');
1894
+ if (text)
1895
+ console.log(text);
1896
+ }
1897
+ else {
1898
+ const { commandHelp } = await import('./help.js');
1899
+ console.error(commandHelp('internal'));
1900
+ process.exit(2);
1901
+ }
1902
+ }
1903
+ else {
1904
+ const { publicCommands } = await import('./help.js');
1905
+ const suggestion = nearestPublicCommand(cmd, publicCommands());
1906
+ console.error(`spex: unknown command '${cmd}'${suggestion ? ` — try: spex ${suggestion}` : ''} (try: spex help)`);
1907
+ process.exit(2);
1908
+ }