spexcode 0.6.1 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +99 -149
  2. package/bin/spex.mjs +4 -0
  3. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
  4. package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
  5. package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
  6. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
  7. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
  8. package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
  9. package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
  10. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
  11. package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
  12. package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
  13. package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
  14. package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
  15. package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
  16. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
  17. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
  18. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
  19. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
  20. package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
  21. package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
  22. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
  23. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
  24. package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
  25. package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
  26. package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
  27. package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
  28. package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
  29. package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
  30. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
  31. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
  32. package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
  33. package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
  34. package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
  35. package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
  36. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
  37. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
  38. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
  39. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
  40. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
  41. package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
  42. package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
  43. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
  44. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
  45. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
  46. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
  47. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
  48. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
  49. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
  50. package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
  51. package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +115 -22
  52. package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
  53. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
  54. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +325 -0
  55. package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
  56. package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
  57. package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
  58. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
  59. package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
  60. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
  61. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
  62. package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
  63. package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
  64. package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
  65. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
  66. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
  67. package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
  68. package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
  69. package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
  70. package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
  71. package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
  72. package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
  73. package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
  74. package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
  75. package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
  76. package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
  77. package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
  78. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
  79. package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
  80. package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
  81. package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
  82. package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
  83. package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
  84. package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
  85. package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
  86. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
  87. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
  88. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
  89. package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
  90. package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
  91. package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
  92. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
  93. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
  94. package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
  95. package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
  96. package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
  97. package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
  98. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
  99. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
  100. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
  101. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
  102. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
  103. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
  104. package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
  105. package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
  106. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
  107. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
  108. package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
  109. package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
  110. package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
  111. package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
  112. package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
  113. package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
  114. package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
  115. package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
  116. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
  117. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
  118. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
  119. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
  120. package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
  121. package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
  122. package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
  123. package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
  124. package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
  125. package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
  126. package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
  127. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
  128. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
  129. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
  130. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
  131. package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
  132. package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
  133. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
  134. package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
  135. package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
  136. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
  137. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
  138. package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
  139. package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
  140. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
  141. package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
  142. package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
  143. package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
  144. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
  145. package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
  146. package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
  147. package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
  148. package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
  149. package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
  150. package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
  151. package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
  152. package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
  153. package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
  154. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
  155. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
  156. package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
  157. package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
  158. package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
  159. package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
  160. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
  161. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
  162. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
  163. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +27 -7
  164. package/node_modules/@spexcode/spec-cli/package.json +47 -0
  165. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  166. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
  167. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  168. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  169. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +22 -0
  170. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  171. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  172. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
  173. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
  174. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
  175. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
  176. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  177. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  178. package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
  179. package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
  180. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  181. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  182. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  183. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  184. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  185. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  186. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  187. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  188. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  189. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  190. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  191. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  192. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
  193. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  194. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  195. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  196. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  197. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  198. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  199. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  200. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  201. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  202. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  203. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  204. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  205. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  206. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  207. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  208. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  209. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  210. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  211. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  212. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  213. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  214. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  215. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  216. package/node_modules/@spexcode/spec-core/package.json +28 -0
  217. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  218. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  219. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  220. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  221. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  222. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  223. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  224. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  225. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  226. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  227. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  228. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  229. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  230. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  231. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  232. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  233. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  234. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  235. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  236. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  237. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  238. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  239. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  240. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  241. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  242. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  243. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  244. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  245. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  246. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  247. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  248. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  249. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  250. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  251. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  252. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  253. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  254. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  255. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  256. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  257. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  258. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  259. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  260. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  261. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  262. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  263. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  264. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  265. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  266. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  267. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  268. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  269. package/package.json +33 -19
  270. package/spec-cli/bin/spex.mjs +0 -69
  271. package/spec-cli/src/anchors.ts +0 -706
  272. package/spec-cli/src/attach.ts +0 -43
  273. package/spec-cli/src/claude-headless.ts +0 -321
  274. package/spec-cli/src/cli.ts +0 -1115
  275. package/spec-cli/src/client.ts +0 -315
  276. package/spec-cli/src/cockpit.ts +0 -43
  277. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  278. package/spec-cli/src/commit-surgery.ts +0 -68
  279. package/spec-cli/src/contract-filter.ts +0 -202
  280. package/spec-cli/src/delivery-queue.ts +0 -107
  281. package/spec-cli/src/doctor.ts +0 -516
  282. package/spec-cli/src/file-write.ts +0 -22
  283. package/spec-cli/src/gateway-auth.ts +0 -181
  284. package/spec-cli/src/gateway-hub.ts +0 -356
  285. package/spec-cli/src/gateway.ts +0 -368
  286. package/spec-cli/src/git.ts +0 -2180
  287. package/spec-cli/src/graph.ts +0 -221
  288. package/spec-cli/src/graphCache.ts +0 -641
  289. package/spec-cli/src/graphDelta.ts +0 -77
  290. package/spec-cli/src/graphStream.ts +0 -1019
  291. package/spec-cli/src/harness-select.ts +0 -77
  292. package/spec-cli/src/harness.ts +0 -2747
  293. package/spec-cli/src/headless-controller.ts +0 -63
  294. package/spec-cli/src/help.ts +0 -480
  295. package/spec-cli/src/host-resources.ts +0 -707
  296. package/spec-cli/src/host.ts +0 -611
  297. package/spec-cli/src/identity-presets.d.ts +0 -13
  298. package/spec-cli/src/identity-presets.js +0 -128
  299. package/spec-cli/src/index.ts +0 -730
  300. package/spec-cli/src/init.ts +0 -258
  301. package/spec-cli/src/issues-cli.ts +0 -276
  302. package/spec-cli/src/issues.ts +0 -237
  303. package/spec-cli/src/layout.ts +0 -557
  304. package/spec-cli/src/lint.ts +0 -458
  305. package/spec-cli/src/localIssues.ts +0 -538
  306. package/spec-cli/src/materialize.ts +0 -412
  307. package/spec-cli/src/mentions.ts +0 -81
  308. package/spec-cli/src/opencode-headless.ts +0 -205
  309. package/spec-cli/src/pi-headless.ts +0 -153
  310. package/spec-cli/src/plugin-harness.ts +0 -157
  311. package/spec-cli/src/process-identity.ts +0 -207
  312. package/spec-cli/src/project-identity.ts +0 -73
  313. package/spec-cli/src/project-store.ts +0 -17
  314. package/spec-cli/src/pty-bridge.ts +0 -396
  315. package/spec-cli/src/pty-helper.mjs +0 -96
  316. package/spec-cli/src/pty-native-helper.mjs +0 -22
  317. package/spec-cli/src/ranker.ts +0 -191
  318. package/spec-cli/src/reaper.ts +0 -125
  319. package/spec-cli/src/resilience.ts +0 -41
  320. package/spec-cli/src/reviewSnapshot.ts +0 -28
  321. package/spec-cli/src/reviews.ts +0 -405
  322. package/spec-cli/src/runtime-guard.ts +0 -35
  323. package/spec-cli/src/runtime-ownership.ts +0 -88
  324. package/spec-cli/src/search.bench.mjs +0 -80
  325. package/spec-cli/src/search.ts +0 -63
  326. package/spec-cli/src/session-cursors.ts +0 -91
  327. package/spec-cli/src/session-follow.ts +0 -182
  328. package/spec-cli/src/session-timeline.ts +0 -98
  329. package/spec-cli/src/sessions.ts +0 -3403
  330. package/spec-cli/src/sh.ts +0 -3
  331. package/spec-cli/src/slash-commands.ts +0 -318
  332. package/spec-cli/src/source-files.ts +0 -104
  333. package/spec-cli/src/specs.ts +0 -498
  334. package/spec-cli/src/supervise.ts +0 -245
  335. package/spec-cli/src/tree.ts +0 -118
  336. package/spec-cli/src/tsx-bin.ts +0 -18
  337. package/spec-cli/src/uninstall.ts +0 -171
  338. package/spec-cli/src/uploads.ts +0 -253
  339. package/spec-cli/src/worktree-sources.ts +0 -42
  340. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  341. package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -28
  342. package/spec-cli/templates/spec/project/.plugins/core/spec.md +0 -23
  343. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -32
  344. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -9
  345. package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -15
  346. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -18
  347. package/spec-dashboard/dist/assets/App-F9uaAVcH.js +0 -2
  348. package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
  349. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  350. package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
  351. package/spec-dashboard/dist/assets/IssuesPage-CuKLFhH3.js +0 -1
  352. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  353. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  354. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  355. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  356. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  357. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  358. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  359. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  360. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  361. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  404. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  405. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  406. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  407. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  408. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  409. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  410. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  411. package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
  412. package/spec-dashboard/dist/assets/Modal-CQgYymmr.js +0 -1
  413. package/spec-dashboard/dist/assets/PageScroll-hT7UTLvD.js +0 -1
  414. package/spec-dashboard/dist/assets/ProjectsPage-CtXxakF9.js +0 -1
  415. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  416. package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
  417. package/spec-dashboard/dist/assets/SessionWindow-CixDi4PI.js +0 -1
  418. package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
  419. package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
  420. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  421. package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
  422. package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
  423. package/spec-dashboard/dist/assets/index-DrVao0Ep.js +0 -41
  424. package/spec-dashboard/dist/assets/launch-BBH02b1v.js +0 -276
  425. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  426. package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
  427. package/spec-dashboard/dist/index.html +0 -29
  428. package/spec-dashboard/src/reviewFilters.js +0 -322
  429. package/spec-dashboard/src/reviewQuery.js +0 -174
  430. package/spec-dashboard/src/session.js +0 -199
  431. package/spec-eval/src/cache.ts +0 -57
  432. package/spec-eval/src/cli.ts +0 -889
  433. package/spec-eval/src/evaltab.ts +0 -262
  434. package/spec-eval/src/filing.ts +0 -39
  435. package/spec-eval/src/freshness.ts +0 -557
  436. package/spec-eval/src/humanok.ts +0 -28
  437. package/spec-eval/src/scenariofresh.ts +0 -244
  438. package/spec-eval/src/scenarios.ts +0 -672
  439. package/spec-eval/src/sessioneval.ts +0 -2226
  440. package/spec-eval/src/sidecar.ts +0 -93
  441. package/spec-eval/src/timeline.ts +0 -54
  442. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  443. package/spec-forge/src/cache.ts +0 -66
  444. package/spec-forge/src/cli.ts +0 -99
  445. package/spec-forge/src/drivers/github.ts +0 -132
  446. package/spec-forge/src/drivers/gitlab.ts +0 -153
  447. package/spec-forge/src/drivers.ts +0 -73
  448. package/spec-forge/src/links.ts +0 -72
  449. package/spec-forge/src/needs-eval.ts +0 -29
  450. package/spec-forge/src/port.ts +0 -36
  451. package/spec-forge/src/resident.ts +0 -46
  452. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  453. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  454. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  455. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  456. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  457. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  458. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  459. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  460. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  480. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -1,1115 +0,0 @@
1
- export {} // make this a module so top-level await is allowed
2
- // static import is fine here: mentions.ts is dependency-free at module level, and stripRefSigil is needed
3
- // by several verbs (spec owner, graph, issue/eval node args) — a CLI reference arg tolerates an optional @/[[ ]]
4
- // sigil ([[mentions]]).
5
- import { stripRefSigil } from './mentions.js'
6
-
7
- const cmd = process.argv[2]
8
-
9
- const DAEMON_DEPENDENCIES = ['hono', '@hono/node-server', '@hono/node-ws', 'node-pty']
10
-
11
- async function assertDaemonDependencies(command: 'spex serve' | 'spex dashboard'): Promise<void> {
12
- const { createRequire } = await import('node:module')
13
- const resolve = createRequire(import.meta.url).resolve
14
- const missing = DAEMON_DEPENDENCIES.filter((dependency) => {
15
- try { resolve(dependency); return false } catch { return true }
16
- })
17
- if (!missing.length) return
18
- console.error(`${command}: missing optional daemon dependencies: ${missing.join(', ')}`)
19
- console.error(`Install them with: npm install ${missing.join(' ')}`)
20
- process.exit(1)
21
- }
22
-
23
- // Registered before any await so a fatal top-level error lands here. Errors we OWN — BackendError, the
24
- // loud malformed-config ConfigError, the --api/--port UsageError, the write-guard GuardError — are
25
- // matched BY NAME (to avoid importing them) and rendered as a one-line `spex: <message>` (a user's
26
- // config typo or a refused cross-project write must read as their situation, not a SpexCode stack dump);
27
- // anything else prints in full so a real bug keeps its trace. A synchronous throw inside an awaited call
28
- // (loadConfig on a malformed spexcode.json) surfaces as uncaughtException, not unhandledRejection, so BOTH
29
- // paths route through the same printer.
30
- function fatal(e: unknown): never {
31
- if (e instanceof Error && ['BackendError', 'ConfigError', 'UsageError', 'GuardError'].includes(e.name)) console.error(`spex: ${e.message}`)
32
- else console.error(e)
33
- process.exit(1)
34
- }
35
- process.on('unhandledRejection', fatal)
36
- process.on('uncaughtException', fatal)
37
-
38
- // tiny flag reader: --key value (and bare positionals)
39
- function flag(name: string): string | undefined {
40
- const i = process.argv.indexOf(`--${name}`)
41
- return i >= 0 ? process.argv[i + 1] : undefined
42
- }
43
-
44
- // Exit AFTER stdout has flushed. process.exit() force-quits without draining buffered pipe writes, so a
45
- // large piped dump (`spex issue ls --json | …`, graph --json, review --json) is silently cut off at the
46
- // pipe buffer (~64KB). The empty write's callback fires once every prior queued chunk has drained; the
47
- // returned promise never resolves (process.exit ends the process inside the callback), so
48
- // `await flushExit(code)` halts execution here exactly like process.exit did — safe to drop in on any
49
- // unbounded-output verb. EPIPE (a reader that closed early — `| head`, `| jq` exiting) can never drain, so
50
- // we ALSO exit on the stream error rather than hang: the truncation is the reader's choice then, not ours.
51
- function flushExit(code = 0): Promise<never> {
52
- return new Promise<never>(() => {
53
- const done = () => process.exit(code)
54
- process.stdout.on('error', done)
55
- process.stdout.write('', done)
56
- })
57
- }
58
- const has = (name: string) => process.argv.includes(`--${name}`)
59
- // bare positionals after argv index `from`, skipping flags and their values (selectors for ls/watch).
60
- const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--prompt-file', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api', '--api-port', '--host', '--preset', '--limit', '--session', '--depth', '--focus', '--keys', '--allow-stop', '--allow-resume', '--ttl-ms', '--wait-ms', '--adapter', '--thread', '--tmux', '--worktree', '--branch'])
61
- function positionals(from: number): string[] {
62
- const out: string[] = []
63
- for (let i = from; i < process.argv.length; i++) {
64
- const t = process.argv[i]
65
- if (t.startsWith('--')) { if (VALUE_FLAGS.has(t)) i++; continue }
66
- out.push(t)
67
- }
68
- return out
69
- }
70
-
71
- function rejectUnknownFlags(command: string, from: number, allowed: readonly string[]): void {
72
- const known = new Set(allowed.map((name) => `--${name}`))
73
- for (let i = from; i < process.argv.length; i++) {
74
- const token = process.argv[i]
75
- if (!token.startsWith('--')) continue
76
- if (!known.has(token)) {
77
- console.error(`${command}: unknown flag ${token}`)
78
- process.exit(2)
79
- }
80
- if (VALUE_FLAGS.has(token)) i++
81
- }
82
- }
83
-
84
- const SIGNPOSTS: Record<string, string> = {
85
- search: 'spex spec search <query>',
86
- owner: 'spex spec owner <path>',
87
- lint: 'spex spec lint',
88
- ack: 'spex spec ack <node>… --reason "<why>"',
89
- tree: 'spex graph',
90
- board: 'spex graph --json',
91
- blob: 'spex evidence put|get',
92
- 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',
93
- forge: 'spex issue links [--pending] [--store <host>] (--host is now --store)',
94
- new: 'spex session new',
95
- ls: 'spex session ls',
96
- watch: 'spex session watch',
97
- wait: 'spex session wait',
98
- review: 'spex session review',
99
- merge: 'spex session merge',
100
- send: 'spex session send',
101
- reopen: 'spex session resume',
102
- done: 'spex session done',
103
- park: 'spex session park',
104
- ask: 'spex session ask',
105
- exit: 'spex session stop',
106
- close: 'spex session close',
107
- capture: 'spex session show <SEL> --capture',
108
- attach: 'spex session attach',
109
- rename: 'spex session rename',
110
- prompt: 'spex session show <SEL>',
111
- rawkey: 'spex session send <SEL> --keys "<keys>"',
112
- resolve: 'spex remark resolve <ref>',
113
- retract: 'spex remark retract <ref>',
114
- }
115
- const SESSION_SIGNPOSTS: Record<string, string> = {
116
- rawkey: 'spex session send <SEL> --keys "<keys>"',
117
- exit: 'spex session stop <SEL>',
118
- reopen: 'spex session resume <SEL> [--force]',
119
- capture: 'spex session show <SEL> --capture',
120
- prompt: 'spex session show <SEL>',
121
- state: 'spex internal session-state',
122
- fail: 'spex internal session-fail',
123
- idle: 'spex internal session-idle',
124
- 'commit-gate': 'spex internal commit-gate',
125
- }
126
- function signpost(oldSpelling: string, newSpelling: string): never {
127
- console.error(`spex: \`${oldSpelling}\` was removed in v0.3.0 — use: ${newSpelling} (map: spex help)`)
128
- process.exit(2)
129
- }
130
- if (cmd !== undefined && SIGNPOSTS[cmd]) signpost(`spex ${cmd}`, SIGNPOSTS[cmd])
131
-
132
- // @@@ followKit - the ONE seam where a CLI follow verb ([[session-follow]]) binds to what it follows.
133
- // Resolution reads the LOCAL store, never a backend: following is reading a file, so it must answer with no
134
- // `spex serve` running, and asking for the derived board would cost exactly the tmux/rendezvous probes a follow
135
- // exists to eliminate. No selector = follow every session, re-enumerated each tick so one that launches
136
- // mid-follow joins the feed; an explicit selector resolves once to a fixed set.
137
- type FollowRest = Omit<import('./session-follow.js').FollowOpts, 'targets' | 'self' | 'row'>
138
- async function followKit(selectors: string[], verb: string): Promise<{
139
- watcher: string | null
140
- follow: (emit: (line: string) => void, opts: FollowRest) => Promise<import('./session-follow.js').FollowOutcome>
141
- }> {
142
- const { followSessions } = await import('./session-follow.js')
143
- const { localCachedSessions } = await import('./client.js')
144
- const { fromRaw, ownSessionId, selectSessions, toSession } = await import('./sessions.js')
145
- const { listSessionIds, readPublicRecordEntry } = await import('./layout.js')
146
- const real = selectors.filter((sel) => sel && sel !== '@all')
147
- let picked: string[] = []
148
- if (real.length) {
149
- picked = selectSessions(localCachedSessions(true), real).map((s) => s.id)
150
- if (!picked.length) { console.error(`${verb}: no such session: ${real.join(' ')}`); process.exit(2) }
151
- }
152
- const targets = real.length ? () => picked : () => listSessionIds()
153
- // A feed line needs a session's NAME, which is record data; its STATUS comes from the log event, never from
154
- // a fresh sample of the mutable record — that resample is what made the old poll lose moves.
155
- const row = (id: string, status: import('./sessions.js').DisplayStatus, note: string | null) => {
156
- const entry = readPublicRecordEntry(id)
157
- if (entry.kind !== 'ok') return null
158
- return { ...toSession(fromRaw(entry.raw), status, 'unknown'), note }
159
- }
160
- const watcher = ownSessionId()
161
- return { watcher, follow: (emit, opts) => followSessions(emit, { ...opts, targets, self: watcher, row }) }
162
- }
163
-
164
- async function localWatchTargetsOrExit(selectors: string[], verb: string): Promise<string[]> {
165
- if (!selectors.length) { console.error(`usage: ${verb} <SEL...>`); process.exit(2) }
166
- const { localCachedSessions } = await import('./client.js')
167
- const { selectSessions } = await import('./sessions.js')
168
- const targets = selectSessions(localCachedSessions(true), selectors).map((session) => session.id)
169
- if (!targets.length) { console.error(`${verb}: no such local session: ${selectors.join(' ')}`); process.exit(2) }
170
- return targets
171
- }
172
-
173
- async function resolveSelectorOrExit(selector: string): Promise<string> {
174
- if (!selector) { console.error('spex: missing session selector (id | id-prefix | node | branch | . for self)'); process.exit(2) }
175
- const { resolveClientSession } = await import('./client.js')
176
- const { sessionLabel } = await import('./sessions.js')
177
- const r = await resolveClientSession(selector)
178
- if ('ok' in r) return r.ok.id
179
- if ('none' in r) { console.error(`spex: no such session: ${selector}`); process.exit(2) }
180
- console.error(`spex: ambiguous selector "${selector}" matches ${r.ambiguous.length} sessions — be more specific:`)
181
- for (const s of r.ambiguous) console.error(` ${s.id.slice(0, 8)} ${sessionLabel(s)}`)
182
- process.exit(2)
183
- }
184
-
185
- // the [[session-eval]] EXPORT artifact behind `spex eval ls --session <SEL> --export`: fetch the
186
- // backend-rendered self-contained HTML, write it (--out, else a tmp file) or open it (--open). Never returns.
187
- async function evalExport(id: string): Promise<never> {
188
- const { clientEvalExport } = await import('./client.js')
189
- const r = await clientEvalExport(id)
190
- if (!r.ok) { console.error(`no export for ${id} (status ${r.status})`); process.exit(1) }
191
- const { writeFileSync } = await import('node:fs')
192
- const { join } = await import('node:path')
193
- const { tmpdir } = await import('node:os')
194
- const out = flag('out') ?? join(tmpdir(), `spexcode-eval-${id.slice(0, 8)}.html`)
195
- writeFileSync(out, r.body)
196
- if (has('open')) {
197
- const { spawn } = await import('node:child_process')
198
- const opener = process.platform === 'darwin' ? 'open' : 'xdg-open'
199
- try { spawn(opener, [out], { detached: true, stdio: 'ignore' }).unref(); console.log(`opened ${out}`) }
200
- catch { console.log(`wrote ${out} — couldn't auto-open, open it in a browser`) }
201
- } else console.log(out)
202
- process.exit(0)
203
- }
204
-
205
- // appended to a done/ask/block declaration: the note is durable conversation history even though the
206
- // CURRENT board projection correctly flips back to active on the next tool call.
207
- const DECLARED = ' — recorded; the human sees it in the dashboard. This declaration remains in the session timeline; your next tool call flips only the current graph state back to active (the mark-active hook, by design).'
208
- // appended ONLY to a propose-close declaration: a worktree about to be discarded may still own ephemeral things the agent started to test this change; nudge (not gate) it to reclaim them before the worktree goes, keyed on whether the thing should outlive the task — never on who started it (a deliberately long-running service / a production build is started-by-you yet must be left alone). Project-agnostic on purpose.
209
- // @@@ the sweep never includes THIS session - the nudge names sessions as sweepable, and it is read at the one moment a session is thinking about closing. Said loosely it reads as permission to close yourself, which deletes the worktree the reading agent is running in. So the target is scoped to sessions the agent spawned, and the exclusion is stated rather than implied.
210
- const CLOSE_CLEANUP = '\n\nBefore this worktree closes, check whether you left anything running that you started to test this change — a background process, a dev or preview server, a bound port, a throwaway session you spawned. If nothing depends on it anymore, shut it down, or it keeps running as an orphan. Leave anything meant to keep running: a service you deliberately stood up, a production build, anything other work relies on. What matters is whether it still needs to exist after this task, not whether you started it. If unsure, leave it. This sweep never includes THIS session: you have PROPOSED that the human close it, and closing your own session would delete the worktree you are running in. This is a reminder to check, not a required step.'
211
-
212
- async function stateKit() {
213
- const s = await import('./sessions.js')
214
- const l = await import('./layout.js')
215
- const { existsSync, writeFileSync } = await import('node:fs')
216
- // the agent-authored state writers resolve WHICH session by id: a `--session <id>` flag (the lifecycle
217
- // hooks pass it, parsed from the payload, since they no longer have a cwd `.session`) wins, else the
218
- // harness env var (ownSessionId — the agent's own `spex session …` carries the harness session id).
219
- const sess = flag('session')
220
- // @@@ no-record diagnosis ([[state]]) - the session store resolves from the CURRENT directory (runtimeRoot
221
- // ← the cwd's git common dir), so the classic declaration failure is a cd OUTSIDE the session's project —
222
- // and a bare "no session record" told the author none of that (field-reported). Name the actual cause and
223
- // route the fix; each branch is a distinct situation, distinguished by probing the same store the writer used.
224
- const noRecord = (): string => {
225
- // cwd probe FIRST: outside a git repo nothing below can resolve — not the store, and not even the env
226
- // id (ownSessionId's alias walk reads the store too) — so diagnose the cwd before touching either.
227
- let ids: string[] | null
228
- try { ids = l.listSessionIds() } catch { ids = null }
229
- if (ids === null) return `no session record — declarations resolve the session store from the CURRENT directory, and ${process.cwd()} is not inside a git repository. cd back into the session's worktree and re-declare.`
230
- const wid = sess || s.ownSessionId() // safe now: the store just resolved, so the alias walk cannot throw
231
- if (!wid) return 'no session record — no session id to write: this shell carries no harness session env (SPEXCODE_SESSION_ID / CLAUDE_CODE_SESSION_ID) and no --session was given. Pass --session <id> (spex session ls lists ids).'
232
- if (ids.length === 0) return `no session record for ${wid.slice(0, 8)} — declarations resolve the session store from the CURRENT directory, and the project at ${process.cwd()} has no sessions at all: you are in a different project's checkout. cd back into the session's worktree and re-declare.`
233
- return `no session record for ${wid.slice(0, 8)} — this project's store (resolved from ${process.cwd()}) holds ${ids.length} session(s) but not this one: a wrong --session id, or you are declaring from a different project's checkout. cd back into the session's worktree and re-declare, or pass a valid --session <id> (spex session ls).`
234
- }
235
- // a state writer from a non-repo cwd throws git's not-a-repo before it can return false — map exactly
236
- // that throw to the no-record path (noRecord re-probes and names the cwd); anything else stays loud.
237
- // A record that exists but CANNOT carry state (unreadable bytes, or a worktree that is gone) is its own
238
- // answer: the writer refused on purpose and already knows why, so that reason is what the caller prints —
239
- // never the no-record diagnosis, which would send the author chasing a wrong cwd ([[sessions-core]]).
240
- const mark = (fn: () => boolean): { ok: boolean; reason?: string } => {
241
- try { return { ok: fn() } }
242
- catch (e) {
243
- if (e instanceof s.SessionRecordUnusable) return { ok: false, reason: e.message }
244
- if (/not a git repository/i.test(String((e as any)?.stderr ?? e))) return { ok: false }
245
- throw e
246
- }
247
- }
248
- // Taught once per record: the CLI's explicit NOTE column is the only note display that cuts prose.
249
- const noteEcho = (note?: string): string => {
250
- if (!note) return ''
251
- const tableCut = s.displayWidth(note) > s.NOTE_BOARD_LIMIT
252
- if (!tableCut) return ''
253
- const wid = sess || s.ownSessionId()
254
- const rid = wid ? (l.readAliasedRawRecord(wid)?.session_id ?? wid) : null // sentinel lives in the RECORD's dir, so an aliased codex id lands on the same file
255
- if (rid) {
256
- const sentinel = l.sessionArtifactPath(rid, 'note-echo-taught')
257
- try {
258
- if (existsSync(sentinel)) return ''
259
- writeFileSync(sentinel, `${new Date().toISOString()}\n`) // only reached on a successful declaration (the echo rides the success branch)
260
- } catch { /* unreadable/unwritable store dir → fall through and teach again; never block the echo */ }
261
- }
262
- return `\nyour note is ${note.length} chars — the session table's NOTE column shows only the first ${s.NOTE_BOARD_LIMIT} display columns. the full text IS recorded, and readable via spex session review ${(wid || '<your-session>').slice(0, 8)} / spex session ls --json. (said once — later cut notes won't repeat this.)`
263
- }
264
- return { s, l, sess, noRecord, mark, noteEcho }
265
- }
266
-
267
- // a trailing --help/-h prints help and exits BEFORE any verb runs, so a help probe never fires a
268
- // streaming/mutating command. It prints THAT command's usage when an entry exists (the second layer
269
- // of the help journey — see help.ts); session noun-verb probes project the exact verb from the shared
270
- // drawer definition. Unknown tokens preserve the existing drawer/map fallback. (Removed spellings never
271
- // reach here — the signpost table above already exited.)
272
- if (cmd && cmd !== 'help' && (has('help') || process.argv.includes('-h'))) {
273
- const { commandHelp, overviewHelp } = await import('./help.js')
274
- console.log(commandHelp(cmd, cmd === 'session' ? process.argv[3] : undefined) ?? overviewHelp())
275
- process.exit(0)
276
- }
277
-
278
- if (cmd === 'serve') {
279
- // `spex serve [api]` runs the backend; `spex serve ui` serves the dashboard on top of a running backend —
280
- // two processes, two verbs in one operator drawer.
281
- const target = positionals(3)[0]
282
- if (target === 'ui') {
283
- await assertDaemonDependencies('spex serve')
284
- // the natural post-install UI: serve the bundled dashboard on its OWN port (loopback by default;
285
- // --host widens the bind for LAN/tailnet viewing), proxying /api + the terminal socket to a
286
- // separately-run `spex serve`. Replaces the dogfood-only `npm run web` (vite).
287
- const { serveDashboardLocal } = await import('./gateway.js')
288
- const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173)
289
- const apiPort = Number(flag('api-port') ?? process.env.PORT ?? 8787)
290
- const host = flag('host') ?? '127.0.0.1'
291
- if (!Number.isInteger(port) || !Number.isInteger(apiPort)) { console.error('spex serve ui: --port and --api-port must be integers'); process.exit(2) }
292
- serveDashboardLocal({ port, apiPort, host })
293
- } else if (target === undefined || target === 'api') {
294
- await assertDaemonDependencies('spex serve')
295
- // fail loud, not cryptic ([[platform-support]]): serve IS the entry to the session runtime, which needs a
296
- // POSIX host (tmux/bash/unix-sockets). On a non-POSIX host (native Windows) point at WSL2 and exit here,
297
- // before importing the supervisor spawns tsx into a downstream ENOENT.
298
- const { assertSessionRuntime } = await import('./runtime-guard.js')
299
- assertSessionRuntime()
300
- // the supervisor owns the public port and runs index.ts as a child for zero-downtime reloads; it
301
- // (not `tsx watch`) is what watches spec-cli/src, so the package `serve` script must NOT use --watch.
302
- // --port is sugar over the PORT env supervise.ts reads — set BEFORE importing so it takes effect. This
303
- // mirrors `spex serve ui --api-port`, so one host runs many projects (each `serve --port N` paired with
304
- // a `serve ui --api-port N`), cwd picking which project's .spec is served — no shared default collides.
305
- const portArg = flag('port')
306
- if (portArg !== undefined) {
307
- if (!Number.isInteger(Number(portArg))) { console.error('spex serve: --port must be an integer'); process.exit(2) }
308
- process.env.PORT = portArg
309
- }
310
- await import('./supervise.js')
311
- } else {
312
- console.error(`spex serve: unknown target '${target}' — spex serve [api] (the backend) | spex serve ui (the dashboard)`)
313
- process.exit(2)
314
- }
315
- } else if (cmd === 'dashboard') {
316
- await assertDaemonDependencies('spex dashboard')
317
- // the HOST-level dashboard ([[host-gateway]]): ONE gateway for every project this user serves. The
318
- // engine is [[gateway-hub]] (routing + [[gateway-auth]] authorization: admin scope implicit from
319
- // loopback until an admin password is set; per-project gates as configured); the host layer mounts the
320
- // instance-validated project registry, its SSE stream, the durable catalog, and the /projects
321
- // operations (register · init · doctor · start a backend) as the hub's admin extension. No --api-port
322
- // pairing: which backend a request reaches is named in its /p/:projectId path, resolved per request.
323
- // `spex serve ui` remains the explicit one-backend pairing; this verb is the zero-config many-project face.
324
- const { startHostDashboard } = await import('./host.js')
325
- const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173)
326
- const host = flag('host') ?? '127.0.0.1'
327
- if (!Number.isInteger(port)) { console.error('spex dashboard: --port must be an integer'); process.exit(2) }
328
- startHostDashboard({ port, host })
329
- } else if (cmd === undefined || cmd === 'help' || cmd === '--help' || cmd === '-h') {
330
- // `spex help <cmd>` drills into one command; bare help is the map. Both name the next layer down.
331
- const { commandHelp, overviewHelp } = await import('./help.js')
332
- const topic = positionals(3)[0]
333
- if (cmd === 'help' && topic) {
334
- if (SIGNPOSTS[topic]) signpost(`spex ${topic}`, SIGNPOSTS[topic])
335
- const h = commandHelp(topic)
336
- if (!h) { console.error(`spex help: no command '${topic}' — run \`spex help\` for the map`); process.exit(2) }
337
- console.log(h)
338
- } else console.log(overviewHelp())
339
- } else if (cmd === 'guide') {
340
- const { guideText } = await import('./guide.js')
341
- if (process.argv[3] === 'config') signpost('spex guide config', 'spex guide settings')
342
- const text = guideText(process.argv[3])
343
- if (text === null) {
344
- console.error(`spex guide: no topic '${process.argv[3]}'. Topics: spec, eval, settings, footprint. Run \`spex guide\` (no topic) for the setup workflow, \`spex help\` for the command map.`)
345
- process.exit(2)
346
- }
347
- console.log(text)
348
- } else if (cmd === 'graph') {
349
- if (flag('node') !== undefined) { console.error('spex graph: --node was renamed — use --focus <id>'); process.exit(2) }
350
- const { buildBoard } = await import('./graph.js')
351
- const focusRaw = flag('focus')
352
- const depthRaw = flag('depth')
353
- const depth = depthRaw === undefined ? undefined : Number(depthRaw)
354
- if (depth !== undefined && (!Number.isInteger(depth) || depth < 0)) { console.error('spex graph: --depth must be a non-negative integer'); process.exit(2) }
355
- const board = await buildBoard()
356
- if (has('json') && focusRaw === undefined && depth === undefined) {
357
- // interactive-only stderr hint: the JSON dump is machine food; a human at a tty gets pointed at
358
- // the readable twin. Piped/redirected stdout stays byte-identical (the hint never touches stdout).
359
- if (process.stdout.isTTY) console.error('(human-readable: spex graph)')
360
- console.log(JSON.stringify(board, null, 2))
361
- await flushExit(0)
362
- }
363
- const { renderTree, treeJson } = await import('./tree.js')
364
- const opts = { node: focusRaw && stripRefSigil(focusRaw), depth, color: process.stdout.isTTY && !process.env.NO_COLOR }
365
- try {
366
- // --json on a FOCUSED/pruned view is that view's JSON (the filtered subtree, badge counts precomputed);
367
- // --json bare is the full payload above — the filter chooses which representation you asked for.
368
- console.log(has('json') ? JSON.stringify(treeJson(board.nodes, opts), null, 2) : renderTree(board.nodes, opts))
369
- } catch (e: any) {
370
- console.error(`spex graph: ${e?.message ?? e}`)
371
- process.exit(2)
372
- }
373
- await flushExit(0)
374
- } else if (cmd === 'spec') {
375
- const sub = process.argv[3]
376
- if (sub === undefined) {
377
- console.log((await import('./help.js')).commandHelp('spec'))
378
- } else if (sub === 'search') {
379
- const { searchSpecs, nearestTitles } = await import('./search.js')
380
- const query = positionals(4).join(' ')
381
- if (!query.trim()) { console.error('usage: spex spec search <query> [--json] [--limit N]'); process.exit(2) }
382
- const limit = Number(flag('limit')) || 10
383
- 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)`) })
384
- // zero-result fail-loud + route-to-next-step: always the corpus-is-English fact (unconditional — no
385
- // language sniffing, no score threshold), plus the nearest titles when anything is even near (typo
386
- // recovery) and the browse-all pointer, so no query dead-ends.
387
- const NO_MATCH = (q: string) => {
388
- const near = nearestTitles(q, 3)
389
- return [
390
- `no spec node matches "${q}" (the corpus is English — if your query isn't, translate and retry)`,
391
- ...(near.length ? ['nearest titles:', ...near.map((t) => ` ${t.title} [${t.id}]`)] : []),
392
- 'browse all: spex graph',
393
- ].join('\n')
394
- }
395
- if (has('json')) {
396
- if (!results.length) console.error(NO_MATCH(query)) // stderr: the stdout JSON contract stays verbatim
397
- console.log(JSON.stringify(results)); await flushExit(0)
398
- }
399
- if (!results.length) { console.log(NO_MATCH(query)); process.exit(0) }
400
- results.forEach((r, i) => {
401
- console.log(`${String(i + 1).padStart(2)}. ${r.title} [${r.id}] · score ${r.score}`)
402
- console.log(` ${r.path}`)
403
- if (r.snippet) console.log(` ${r.snippet}`)
404
- })
405
- process.exit(0)
406
- } else if (sub === 'owner') {
407
- // BOTH [[governed-related]] relations, distinctly: governors (code: — the verdict) and referencers
408
- // (related: — pointers; coverage only, never drift, never eval freshness).
409
- const { specOwners, specRelated } = await import('./specs.js')
410
- const { loadConfig } = await import('./lint.js')
411
- const p0 = positionals(4)[0]
412
- if (!p0) { console.error('usage: spex spec owner <path> [--actionable]'); process.exit(2) }
413
- const p = stripRefSigil(p0)
414
- const rel = p.startsWith(process.cwd()) ? p.slice(process.cwd().length + 1) : p
415
- const owners = specOwners(p)
416
- const related = specRelated(p)
417
- const maxOwners = loadConfig(process.cwd()).maxOwners
418
- // a selector-SCOPED governor (every claiming code: entry carries `#symbol` — [[code-anchor]]) still
419
- // DISPLAYS, marked, but does not count toward the too-many-owners bound: it claims named units, not
420
- // the whole file.
421
- const whole = owners.filter((o) => !o.scoped)
422
- const names = (xs: { id: string; scoped?: boolean }[]) => xs.map((o) => `'${o.id}'${o.scoped ? ' (scoped)' : ''}`).join(', ')
423
- const relLine = related.length ? `\n also referenced by ${names(related)} (related: coverage only — no drift, no eval freshness)` : ''
424
- if (owners.length === 0 && related.length === 0) {
425
- console.log(`${rel} — no spec claims this yet (uncovered). If your change is substantive, give it a home before it drifts.`)
426
- } else if (owners.length === 0) {
427
- // related-only: lint's coverage is satisfied, so the per-edit hook stays silent (lint-consistent) —
428
- // but a human asking gets the honest nuance: nothing tracks this file's drift.
429
- if (has('actionable')) process.exit(0)
430
- console.log(`${rel} — not governed (no code: claim), but referenced by ${names(related)} (related: coverage only). Nothing tracks its drift; if your change is substantive, consider giving it a governing home.`)
431
- } else if (whole.length <= maxOwners) {
432
- // a sanely-owned file is NOT actionable: --actionable callers (the per-edit spec-of-file hook) stay
433
- // silent here, so the annotation fires only on an OVER-owned or uncovered file — rare and worth acting on.
434
- if (has('actionable')) process.exit(0)
435
- const named = names(owners)
436
- const lead = owners.length === 1 ? `${rel} is governed by ${named} — ${owners[0].desc}` : `${rel} is governed by ${named} (shared, fine).`
437
- console.log(`${lead} Read/honor the spec; if your change shifts the intent, update the spec in the SAME commit.${relLine}`)
438
- } else {
439
- const ids = names(owners)
440
- 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}`)
441
- }
442
- } else if (sub === 'lint') {
443
- const { specLint, pendingTouchesGoverned, DRIFT_GUIDANCE } = await import('./lint.js')
444
- const pending = flag('pending')
445
- if (pending && process.env.SPEXCODE_GATE_SCOPE_ONLY === '1') {
446
- const touches = await pendingTouchesGoverned(process.cwd(), pending)
447
- if (!touches) process.exit(76)
448
- }
449
- const findings = await specLint(undefined, undefined, {
450
- tip: pending || 'HEAD',
451
- })
452
- const errors = findings.filter((f) => f.level === 'error')
453
- for (const f of findings) console.error(` ${f.level === 'error' ? '✗' : '•'} ${f.rule}: ${f.msg}`)
454
- console.error(`spex spec lint: ${errors.length} error(s), ${findings.length - errors.length} warning(s)`)
455
- // drift teaches from the ONE `spex spec lint` (no flag). Unanchored drift stays advisory forever; the
456
- // Blocking tier is anchor-drift ([[code-anchor]]) — CI/default lint judges HEAD; the local armed hook
457
- // supplies --pending <real commit oid>; canonical pre-commit leaves the whole lint to that one run.
458
- if (findings.some((f) => f.rule === 'drift' || f.rule === 'anchor-drift')) console.error(`\n${DRIFT_GUIDANCE}`)
459
- process.exit(errors.length ? 1 : 0)
460
- } else if (sub === 'ack') {
461
- // An EMPTY stamp commit on top of HEAD, never an amend: driftFor (git.ts) quiets every drift commit
462
- // REACHABLE from an ack, so a child stamp covers exactly what amending HEAD would — and it works where
463
- // amend can't: on a trunk merge commit, re-authoring it after MERGE_HEAD is gone reads to main-guard as
464
- // a direct trunk commit. The guard passes the stamp through its tree-unchanged gate instead. `--only`
465
- // with no paths pins the commit to HEAD's tree even when the index is dirty — an ack must never sweep
466
- // staged files along. git de-dupes adjacent trailers, so re-acking is harmless.
467
- const { git } = await import('./git.js')
468
- const nodes = positionals(4)
469
- const reason = (flag('reason') ?? '').trim()
470
- if (!nodes.length || !reason) {
471
- console.error('usage: spex spec ack <node-id>… --reason "<why this change keeps each spec valid>"')
472
- 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.')
473
- process.exit(2)
474
- }
475
- try {
476
- git(['commit', '--only', '--allow-empty', '-m', `ack: Spec-OK ${nodes.join(', ')}`, '-m', reason,
477
- ...nodes.flatMap((n) => ['--trailer', `Spec-OK: ${n}`])])
478
- console.log(`Spec-OK: ${nodes.join(', ')} → ${git(['rev-parse', '--short', 'HEAD']).trim()} (empty stamp commit; reason recorded in the commit body)`)
479
- } catch (e: any) {
480
- console.error(`ack failed: ${e?.message ?? e}`); process.exit(1)
481
- }
482
- } else {
483
- console.error(`spex spec: unknown verb '${sub}' — search | owner | lint | ack (spex help spec)`)
484
- process.exit(2)
485
- }
486
- } else if (cmd === 'init') {
487
- // scaffold a repo to adopt SpexCode: copy the shipped DATA templates (seed spec tree + git hooks)
488
- // into <targetDir> (default cwd). spex init [targetDir] --harness <ids> [--preset <tier>]
489
- const { specInit } = await import('./init.js')
490
- await specInit(positionals(3)[0], flag('preset'), flag('harness'))
491
- } else if (cmd === 'uninstall') {
492
- // the surgical inverse of init: remove every SpexCode-generated artifact (harness shims/contract/trust, the
493
- // .gitignore block, the global store, any plugin bundle) — NEVER the user's .spec/.plugins data or their own
494
- // prose. Git hooks preserved unless --hooks. spex uninstall [targetDir] [--hooks]
495
- const { uninstall } = await import('./uninstall.js')
496
- uninstall(positionals(3)[0], { hooks: has('hooks') })
497
- } else if (cmd === 'eval') {
498
- const sub = process.argv[3]
499
- if (sub === undefined) {
500
- console.log((await import('./help.js')).commandHelp('eval'))
501
- } else if (sub === 'ls' && flag('session') !== undefined) {
502
- // the session EVAL read ([[session-eval]]'s interactive face as a CLI verb): the dashboard Eval tab's
503
- // text twin. Renders the session's changed nodes with each DECLARED scenario at its CURRENT score
504
- // (latest reading per scenario, worktree-rooted) — filed results keep the backend's newest-first order
505
- // across ownership, the session's OWN measurements are ✦-marked, and blind spots follow. --export writes
506
- // the self-contained HTML artifact instead.
507
- const id = await resolveSelectorOrExit(flag('session')!)
508
- if (has('export')) await evalExport(id)
509
- const { clientEvals } = await import('./client.js')
510
- const r = await clientEvals(id)
511
- if (!r.ok) { console.error(`no evals for ${id} (status ${r.status})`); process.exit(1) }
512
- if (has('json')) { console.log(JSON.stringify(r.model, null, 2)); await flushExit(0) }
513
- const m = r.model
514
- const own = m.items.filter((item) => item.inSession).length
515
- console.log(`eval session [${m.id}]`)
516
- console.log(` gates : ${m.gates.map((g) => `${g.ok ? '✓' : '✗'} ${g.label} — ${g.detail}`).join(' · ')}`)
517
- if (own) console.log(` ✦ : ${own} scenario(s) measured by THIS session (unmarked rows = evals filed by other sessions)`)
518
- if (!m.items.length) console.log('\n no affected scenarios to evaluate yet')
519
- let lastNode: string | null = null
520
- for (const e of m.items) {
521
- if (e.node !== lastNode) {
522
- console.log(`\n${e.node}`)
523
- lastNode = e.node
524
- }
525
- if (e.filterKind === 'blind') {
526
- console.log(` ∅ unmeasured ${e.scenario} — declared, never measured (blind spot)`)
527
- continue
528
- }
529
- const verdict = e.verdict?.status === 'pass' ? '✓ pass' : e.verdict?.status === 'fail' ? '✗ fail' : '· unscored'
530
- const stale = e.fresh ? '' : ` (stale: ${(e.staleAxes || []).join(',')})`
531
- console.log(` ${e.inSession ? '✦' : ' '} ${verdict}${stale} ${e.scenario} — ${e.ts}${e.evaluator ? ` · ${e.evaluator}` : ''}`)
532
- }
533
- } else if (['add', 'ls', 'scenario', 'lint', 'ok', 'retract', 'clean'].includes(sub)) {
534
- // node-scoped verbs — thin route; the logic lives in spec-eval.
535
- const { runEval } = await import('../../spec-eval/src/cli.js')
536
- await flushExit(await runEval(process.argv.slice(3)))
537
- } else {
538
- console.error(`spex eval: unknown verb '${sub}' — add | ls | scenario ls/write | lint | ok | retract | clean (spex help eval)`)
539
- if (!sub.startsWith('--')) 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)
540
- process.exit(2)
541
- }
542
- } else if (cmd === 'evidence') {
543
- if (process.argv[3] === undefined) {
544
- console.log((await import('./help.js')).commandHelp('evidence'))
545
- } else {
546
- const { runEvidence } = await import('../../spec-eval/src/cli.js')
547
- await flushExit(await runEvidence(process.argv.slice(3)))
548
- }
549
- } else if (cmd === 'issue') {
550
- if (process.argv[3] === undefined) {
551
- console.log((await import('./help.js')).commandHelp('issue'))
552
- } else {
553
- const { runIssues } = await import('./issues-cli.js')
554
- await flushExit(await runIssues(process.argv.slice(3)))
555
- }
556
- } else if (cmd === 'remark') {
557
- const sub = process.argv[3]
558
- const m = sub === 'add' || sub === 'resolve' || sub === 'retract' ? await import('./issues-cli.js') : null
559
- if (sub === undefined) {
560
- console.log((await import('./help.js')).commandHelp('remark'))
561
- } else if (sub === 'add') {
562
- await flushExit(await m!.runRemark(process.argv.slice(4)))
563
- } else if (sub === 'resolve') {
564
- await flushExit(await m!.runResolve(process.argv.slice(4)))
565
- } else if (sub === 'retract') {
566
- await flushExit(await m!.runRetract(process.argv.slice(4)))
567
- } else {
568
- console.error(`spex remark: unknown verb '${sub}' — add | resolve | retract (spex help remark)`)
569
- if (!sub.startsWith('--')) console.error(' (the old bare `spex remark <host> --body …` write is now `spex remark add <host> --body …`)')
570
- process.exit(2)
571
- }
572
- } else if (cmd === 'materialize') {
573
- const { materialize } = await import('./materialize.js')
574
- try {
575
- console.log(`materialized — content-hash ${materialize().contentHash}`)
576
- } catch (e) {
577
- // a policy error (e.g. a missing/illegal `harnesses` set) is a user-facing verdict, not a crash — one
578
- // line + the repair it already carries, never a stack trace.
579
- console.error(`spex materialize: ${(e as Error).message}`)
580
- process.exit(1)
581
- }
582
- } else if (cmd === 'doctor') {
583
- const { runDoctor } = await import('./doctor.js')
584
- await flushExit(await runDoctor(process.argv.slice(3)))
585
- } else if (cmd === 'session') {
586
- const sub = process.argv[3]
587
- if (sub === undefined) {
588
- console.log((await import('./help.js')).commandHelp('session'))
589
- } else if (SESSION_SIGNPOSTS[sub]) {
590
- signpost(`spex session ${sub}`, SESSION_SIGNPOSTS[sub])
591
- } else if (sub === 'new') {
592
- // spex session new "<prompt>" (prompt = first positional or --prompt, or --prompt-file
593
- // <path>|- so a long multi-paragraph prompt never fights shell quoting — [[prompt-file]]).
594
- // createSession POSTs to the running backend so the launch runs in the backend's process (auth env + cap);
595
- // it falls back to an in-process launch only when no backend answers.
596
- if (has('node')) {
597
- console.error('spex session new: --node was removed — put a [[<id>]] mention in the prompt — the first mention binds')
598
- process.exit(2)
599
- }
600
- rejectUnknownFlags('spex session new', 4, ['prompt', 'prompt-file', 'launcher', 'api', 'port'])
601
- const { createSession } = await import('./sessions.js')
602
- const promptFile = flag('prompt-file')
603
- const inline = flag('prompt') ?? positionals(4)[0]
604
- let prompt = inline ?? ''
605
- if (promptFile !== undefined) {
606
- // fail-loud exclusive: never silently pick one of two prompt sources.
607
- if (inline !== undefined) { console.error('spex session new: give the prompt either inline or via --prompt-file, not both'); process.exit(2) }
608
- const { readFileSync } = await import('node:fs')
609
- try { prompt = promptFile === '-' ? readFileSync(0, 'utf8') : readFileSync(promptFile, 'utf8') }
610
- catch (e) { console.error(`spex session new: --prompt-file ${promptFile}: ${e instanceof Error ? e.message : e}`); process.exit(2) }
611
- if (!prompt.trim()) { console.error(`spex session new: --prompt-file ${promptFile === '-' ? 'stdin' : promptFile} is empty — refusing a promptless launch`); process.exit(2) }
612
- }
613
- const created = await createSession(prompt, flag('launcher') ?? undefined)
614
- const { ownSessionId, subscribeSessionWatch } = await import('./sessions.js')
615
- let watchEstablished = false
616
- if (created.parent && created.parent === ownSessionId()) {
617
- try {
618
- await subscribeSessionWatch(created.parent, [created.id])
619
- watchEstablished = true
620
- } catch (error) {
621
- console.error(`spex session new: child ${created.id} was created, but its managed watch was not established: ${error instanceof Error ? error.message : String(error)}`)
622
- }
623
- }
624
- console.log(JSON.stringify(created, null, 2))
625
- console.error((await import('./help.js')).sessionLaunchReceipt(created.id, watchEstablished))
626
- } else if (sub === 'ls') {
627
- // pretty list of living sessions + states. `spex session ls [SEL...] [--status a,b] [--json]`
628
- // the board comes from the backend (so it shows the sessions of whatever SPEXCODE_API_URL points at,
629
- // incl. a remote machine); selectSessions/formatTable are pure presentation, applied client-side.
630
- const { selectSessions, formatTable } = await import('./sessions.js')
631
- const { clientListSessions } = await import('./client.js')
632
- // The backend's default projection excludes cold archives. --all and an explicit selector request the
633
- // history projection so an operator can still inspect or unarchive one deliberately.
634
- const selectors = positionals(4)
635
- const all = await clientListSessions(has('all') || selectors.length > 0)
636
- const visible = all
637
- const picked = selectSessions(visible, selectors, flag('status')?.split(','))
638
- console.log(has('json') ? JSON.stringify(picked, null, 2) : formatTable(picked))
639
- } else if (sub === 'resources') {
640
- rejectUnknownFlags('spex session resources', 4, ['json', 'api', 'port'])
641
- const { clientResources } = await import('./client.js')
642
- const report = await clientResources()
643
- if (has('json')) console.log(JSON.stringify(report, null, 2))
644
- else console.log((await import('./host-resources.js')).formatResourceReport(report))
645
- } else if (sub === 'watch') {
646
- const [verb, ...rest] = positionals(4)
647
- const { ownSessionId, subscribeSessionWatch, listSessionWatches, cancelSessionWatch } = await import('./sessions.js')
648
- const watcher = ownSessionId()
649
- if (verb === 'stream') {
650
- const kit = await followKit(rest, 'spex session watch stream')
651
- await kit.follow((line) => console.log(line), {
652
- statuses: flag('status')?.split(','),
653
- includeIdle: has('idle'),
654
- as: flag('as'),
655
- intervalMs: (Number(flag('interval')) || 1) * 1000,
656
- })
657
- } else if (verb === 'list') {
658
- if (!watcher) { console.error('spex session watch list: no governed caller session — this caller has no durable watch relation'); process.exit(2) }
659
- for (const watch of listSessionWatches(watcher)) console.log(`${watch.target}\t${watch.createdAt}`)
660
- } else if (verb === 'cancel') {
661
- if (!watcher) { console.error(`spex session watch cancel: no governed caller session — run \`spex session wait ${rest.join(' ')}\` in the background instead`); process.exit(2) }
662
- const targets = await localWatchTargetsOrExit(rest, 'spex session watch cancel')
663
- console.log(`cancelled ${cancelSessionWatch(watcher, targets)} watch${targets.length === 1 ? '' : 'es'}`)
664
- } else {
665
- const selectors = [verb, ...rest].filter(Boolean)
666
- if (!watcher) {
667
- if (!selectors.length) { console.error('usage: spex session watch <SEL...>'); process.exit(2) }
668
- console.error(`spex session watch: no governed caller session — run \`spex session wait ${selectors.join(' ')}\` in the BACKGROUND to receive the next actionable transition`)
669
- } else {
670
- const targets = await localWatchTargetsOrExit(selectors, 'spex session watch')
671
- const result = await subscribeSessionWatch(watcher, targets)
672
- console.log(`watching ${result.watched.join(' ')}`)
673
- }
674
- }
675
- } else if (sub === 'wait') {
676
- const selectors = positionals(4)
677
- const kit = await followKit(selectors, 'spex session wait')
678
- const named = selectors.join(' ') || 'your own log'
679
- // point-of-use turn-freeze warning ([[session-follow]]): a managed agent that runs this wait in the FOREGROUND
680
- // freezes its whole turn until the target produces an edge — a warning that used to live only in help
681
- // prose, now said where it matters. Foreground vs background is invisible from here, so the hint prints
682
- // for ANY managed-agent shell (harmless in a background transcript), on stderr, and changes nothing else.
683
- if (kit.watcher) 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.`)
684
- const timeoutSec = Number(flag('timeout')) || 1200
685
- const r = await kit.follow(() => {}, {
686
- includeIdle: has('idle'),
687
- intervalMs: (Number(flag('interval')) || 1) * 1000,
688
- take: true,
689
- timeoutMs: timeoutSec * 1000,
690
- // the arrival state and each observed transition narrate on stderr AS THEY HAPPEN, so a backgrounded
691
- // wait's transcript is the state sequence itself; stdout stays the one machine verdict.
692
- onObserved: (sid, st, was) => console.error(was
693
- ? `spex session wait: observed ${sid.slice(0, 8)} ${was} → ${st}`
694
- : `spex session wait: ${sid.slice(0, 8)} current status ${st} — recorded as the path start; returns on the next non-actionable→actionable transition`),
695
- })
696
- // the observed status path is the stdout verdict: read the LAST token as the status reached. Printing the
697
- // whole path (not just the final status) is the point — a manager sees what the wait lived through
698
- // (e.g. review→working→close-pending across a merge dispatch), not a bare word out of context.
699
- if ('reached' in r) { console.log(r.path.join('→')); process.exit(0) }
700
- if ('mail' in r) {
701
- console.error(`spex session wait: message from ${r.mail.from ?? 'a human'} — ${r.mail.text}`)
702
- console.log('message')
703
- process.exit(0)
704
- }
705
- if ('gone' in r) { console.error(`spex session wait: session ${r.gone} is gone — its store dir no longer exists`); process.exit(2) }
706
- console.error(`spex session wait: timeout — observed no non-actionable→actionable transition on ${named} within ${timeoutSec}s (status path: ${r.path.join('→') || 'never sighted'})`)
707
- process.exit(1)
708
- } else if (sub === 'review') {
709
- const first = positionals(4)[0]
710
- if (first === 'proof') 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)
711
- const { clientReview } = await import('./client.js')
712
- if (!first) { console.error('usage: spex session review <SEL> (id | id-prefix | node | branch)'); process.exit(2) }
713
- const id = await resolveSelectorOrExit(first)
714
- const r = await clientReview(id)
715
- if (!r) { console.error(`no such session ${id}`); process.exit(1) }
716
- if (has('json')) { console.log(JSON.stringify(r, null, 2)) }
717
- else {
718
- const g = r.gates
719
- console.log(`review ${r.label} [${r.id}]`)
720
- console.log(` ahead of main : ${r.ahead} commit(s)`)
721
- console.log(` uncommitted : ${r.dirtyNonRuntime} non-runtime file(s)`)
722
- console.log(` proposal : ${r.proposal.kind ?? '—'}${r.proposal.note ? ` — ${r.proposal.note}` : ''}`)
723
- console.log(' gates:')
724
- console.log(` conflicts w/ main : ${g.conflictsWithMain ? 'YES' : 'no'}`)
725
- console.log(` lint : ${g.lint.errorCount} error(s), ${g.lint.warningCount} warning(s)`)
726
- // measured loss is REPORTED, never graded — and a projection that isn't ready says so instead of
727
- // printing four zeros that would read as a clean session.
728
- console.log(` evals : ${g.evals.phase === 'ready'
729
- ? `${g.evals.freshPass} fresh pass, ${g.evals.freshFail} fresh fail, ${g.evals.needReview} need review, ${g.evals.blind} blind`
730
- : `not measured yet (${g.evals.phase})`}`)
731
- console.log(` diff (merge-base, ${r.diff.length} file(s)):`)
732
- for (const f of r.diff) console.log(` ${f.status.padEnd(12)} +${f.additions} -${f.deletions} ${f.path}`)
733
- }
734
- } else if (sub === 'merge') {
735
- const { clientMerge } = await import('./client.js')
736
- const sel = positionals(4)[0]
737
- if (!sel) { console.error('usage: spex session merge <SEL> (id | id-prefix | node | branch)'); process.exit(2) }
738
- const id = await resolveSelectorOrExit(sel)
739
- const r = await clientMerge(id)
740
- if (r.dispatched) console.log(`merge dispatched to ${id} — its agent is landing the merge`)
741
- else console.error(`merge dispatch failed: ${r.reason}`)
742
- process.exit(r.dispatched ? 0 : 1)
743
- } else {
744
- // `s` (sessions.ts) backs the state PRODUCERS that stay local (done/park/ask write the global record by
745
- // session_id) and the stateKit shared with `spex internal session-*`. `c` (client.ts) backs the
746
- // read/control subs that route through the backend. Lazily imported.
747
- const c = await import('./client.js')
748
- const id = process.argv[4]
749
- if (sub === 'resume') {
750
- // bring the agent back up (relaunch ONLY if confirmed offline, the backend owns it); demotes a working
751
- // `active` to idle but leaves a standing declaration/proposal untouched (see sessions.ts resumeSession()).
752
- // The RESUME GUARD refuses a relaunch on a LIVE/unproven agent (that would kill a live worker) — `--force`
753
- // overrides for a genuinely wedged process. A following prompt is what actually re-drives it.
754
- const full = await resolveSelectorOrExit(id)
755
- const r = await c.clientResume(full, process.argv.includes('--force'))
756
- if (r.ok) console.log(r.info ? `${full} -> ${r.info}` : `${full} -> resumed`)
757
- else { console.error(`spex session resume: ${r.error || `no such session ${full}`}`); process.exit(2) }
758
- } else if (sub === 'done') {
759
- // sugar for awaiting; --propose merge|nothing|close, optional --note
760
- const { s, sess, mark, noRecord, noteEcho } = await stateKit()
761
- const p = (flag('propose') as any) || 'nothing'
762
- let closeNote = p === 'close' ? CLOSE_CLEANUP : ''
763
- if (p === 'close') {
764
- // the DATA half of the close nudge ([[local-issues]] closeoutNudge): the still-open local issues this
765
- // session touched, listed by id — empty/OFF/no-identity prints nothing. Loud on failure but never
766
- // gating: the declaration must land whatever the issue store is doing.
767
- try { closeNote += (await import('./localIssues.js')).closeoutNudge(sess ?? s.ownSessionId()) }
768
- catch (e) { console.error(`issue closeout check failed (declaration unaffected): ${e instanceof Error ? e.message : e}`) }
769
- }
770
- const done = mark(() => s.markDone(p, sess, flag('note')))
771
- console.log(done.ok ? `done (${p})${DECLARED}${noteEcho(flag('note'))}${closeNote}` : done.reason ?? noRecord())
772
- } else if (sub === 'park') {
773
- // sugar: the agent is waiting on a background task; it will self-resume (NOT idle/awaiting)
774
- const { s, sess, mark, noRecord, noteEcho } = await stateKit()
775
- const parked = mark(() => s.markState('parked', { note: flag('note'), sessionId: sess }))
776
- console.log(parked.ok ? `parked${DECLARED}${noteEcho(flag('note'))}` : parked.reason ?? noRecord())
777
- } else if (sub === 'ask') {
778
- // the agent DELIBERATELY declares it is pausing to ask the human a question (like `done`/`park`, an
779
- // authored state — NOT guarded active-only). The --note carries the question. Distinct from `park`
780
- // (waiting on a background task, self-resumes): an asking agent resumes only when the human replies.
781
- const { s, sess, mark, noRecord, noteEcho } = await stateKit()
782
- const asked = mark(() => s.markState('asking', { note: flag('note'), sessionId: sess }))
783
- console.log(asked.ok ? `asking${DECLARED}${noteEcho(flag('note'))}` : asked.reason ?? noRecord())
784
- } else if (sub === 'stop') {
785
- // the SOFT stop: kill the agent's tmux + socket but KEEP the worktree, so the session goes offline and
786
- // can be resumed (`session resume`). Distinct from `close`, which removes the worktree.
787
- const full = await resolveSelectorOrExit(id)
788
- const stopped = await c.clientStop(full)
789
- if (!stopped) { console.error(`spex session stop: no such session ${full} (no stop transition was committed)`); process.exit(1) }
790
- console.log(`stopped ${full} (worktree kept — resumable)`)
791
- } else if (sub === 'interrupt') {
792
- const full = await resolveSelectorOrExit(id)
793
- const r = await c.clientInterrupt(full)
794
- console.log(r.ok ? `interrupted ${full}` : `interrupt failed: ${r.error}`)
795
- process.exit(r.ok ? 0 : 1)
796
- } else if (sub === 'archive' || sub === 'unarchive') {
797
- // ARCHIVING/legacy unarchive ([[archive]]) — archive exact-stops before filing; unarchive signposts to
798
- // resume and recreates the same conversation.
799
- const on = sub === 'archive'
800
- const full = await resolveSelectorOrExit(id)
801
- const ok = await c.clientArchive(full, on)
802
- console.log(!ok ? `no such session ${full}` : `${on ? 'archived' : 'resumed'} ${full}`)
803
- } else if (sub === 'close') {
804
- const full = await resolveSelectorOrExit(id)
805
- const closed = await c.clientClose(full)
806
- if (!closed) { console.error(`spex session close: no such session ${full} (record remains; no close was committed)`); process.exit(1) }
807
- console.log(`closed ${full}`)
808
- } else if (sub === 'quarantine') {
809
- rejectUnknownFlags('spex session quarantine', 4, ['adapter', 'thread', 'tmux', 'worktree', 'branch', 'restore', 'api', 'port'])
810
- if (!id) { 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)') ; process.exit(2) }
811
- if (has('restore')) {
812
- // Quarantine addresses an unreadable row which selector resolution intentionally excludes. Both the
813
- // move and its reverse therefore take the literal exact id, with the backend proving record state.
814
- const restored = await c.clientRestoreQuarantine(id)
815
- console.log(`restored quarantined record ${restored.id} from ${restored.bundle}`)
816
- } else {
817
- const adapter = flag('adapter'), tmux = flag('tmux'), worktree = flag('worktree'), branch = flag('branch')
818
- if (!adapter || !tmux || !worktree || !branch) {
819
- 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)')
820
- process.exit(2)
821
- }
822
- const quarantined = await c.clientQuarantine(id, { adapter, thread: flag('thread') ?? null, tmux, worktree, branch })
823
- console.log(`quarantined ${quarantined.id} -> ${quarantined.bundle}`)
824
- }
825
- } else if (sub === 'send') {
826
- const full = await resolveSelectorOrExit(id)
827
- if (has('keys')) {
828
- // the LAST-RESORT face of send: forward raw nav-mode keystrokes (tmux send-keys, NEVER the prompt
829
- // socket) — how a manager drives a worker wedged in an interactive TUI dialog the prompt channel
830
- // can't reach (a select menu wanting one Enter/arrow). UNSTABLE, and able to confirm dangerous
831
- // dialogs — try plain `session send` text FIRST; reach for --keys only when text provably can't
832
- // land. Tokens = named keys, single chars, C-/M-/S- combos; whitespace-separated, delivered as ONE
833
- // ordered batch ([[nav-mode-key-ordering]]). Fail-loud: nothing delivered exits non-zero.
834
- const keys = (flag('keys') ?? '').split(/\s+/).filter(Boolean)
835
- if (keys.length === 0) { console.error('usage: spex session send <SEL> --keys "<keys>" (e.g. "Up Up Enter", "C-r", single chars — last resort; try a plain send first)'); process.exit(2) }
836
- if (await c.clientSendRawKeys(full, keys)) { console.log(`sent ${keys.length} key${keys.length === 1 ? '' : 's'} -> ${full}`); process.exit(0) }
837
- console.error(`spex session send --keys: nothing delivered to ${full} (offline, unknown session, or no valid key token)`)
838
- process.exit(1)
839
- }
840
- // The backend decides send success at the timeline append. A dead adapter poke only delays context
841
- // injection, while a refused record write prints the reason and exits non-zero.
842
- // BIDIRECTIONAL: stamp the SENDER (this send process's OWN session — the only process that knows it, via
843
- // ownSessionId from CLAUDE_CODE_SESSION_ID) + a one-line reply hint into the delivered
844
- // message, so the recipient can reply over the SAME send. The sender's row (hence its display label) is
845
- // resolved through the shared resolver; a human in a plain shell has no session id → bare message, no
846
- // hint (see [[agent-reply-channel]]).
847
- const s = await import('./sessions.js')
848
- const senderId = s.ownSessionId()
849
- let sender = null
850
- if (senderId) {
851
- const sr = await c.resolveClientSession(senderId)
852
- // name the sender by the board HEADLINE (sessionHeadline — the live self-summary the recipient sees on
853
- // the board), NOT the stable sessionLabel that stops at the bare prompt-truncation title.
854
- sender = 'ok' in sr ? { id: sr.ok.id, label: s.sessionHeadline(sr.ok) } : { id: senderId, label: null }
855
- }
856
- const r = await c.clientSend(full, s.withSenderHint(process.argv[5] ?? '', sender), senderId ?? undefined)
857
- console.log(r.ok ? 'sent' : `dispatch failed: ${r.error}`)
858
- process.exit(r.ok ? 0 : 1)
859
- } else if (sub === 'show') {
860
- // the session RECORD as one per-id read (status · node · branch · launcher · the full originating
861
- // prompt); --capture swaps in the LIVE PANE face of the same read. The pane contract is unchanged from
862
- // the verb it absorbed — fail and empty stay DISTINCT: a real empty pane prints nothing and exits 0;
863
- // unknown id exits 2, offline / capture-error exit 1, each with a named reason.
864
- const full = await resolveSelectorOrExit(id)
865
- if (has('capture')) {
866
- const r = await c.clientCapture(full)
867
- if (r.ok) { process.stdout.write(r.pane) }
868
- else { console.error(`spex session show --capture: ${r.reason}`); process.exit(r.status === 404 ? 2 : 1) }
869
- } else {
870
- const r = await c.clientShow(full)
871
- if (!r.ok) { console.error(`spex session show: no such session ${full}`); process.exit(2) }
872
- if (has('json')) { console.log(JSON.stringify(r.session, null, 2)) }
873
- else {
874
- const x = r.session
875
- console.log(`${x.label} [${x.id}]`)
876
- console.log(` status : ${x.status} (lifecycle ${x.lifecycle} · liveness ${x.liveness})`)
877
- console.log(` node : ${x.node ?? '—'}`)
878
- console.log(` branch : ${x.branch ?? '—'}`)
879
- console.log(` launcher : ${x.launcher ?? '—'} (harness ${x.harness})`)
880
- console.log(` worktree : ${x.path}`)
881
- console.log(` created : ${new Date(x.created).toISOString()}`)
882
- if (x.note) console.log(` note : ${x.note}`)
883
- if (x.proposal) console.log(` proposal : ${x.proposal}`)
884
- console.log(x.prompt ? ` prompt |\n${x.prompt.replace(/\n$/, '').split('\n').map((l) => ` ${l}`).join('\n')}` : ' prompt : (none recorded)')
885
- }
886
- }
887
- } else if (sub === 'rename') {
888
- // set the session's display-name override — the right-click rename ([[session-rename]]) as a verb, so an
889
- // agent manager can fix a label without the GUI. An EXPLICIT "" clears back to the derived label; a
890
- // MISSING argument is a usage error, never a silent clear. Unknown session → the endpoint's 404, loud.
891
- const full = await resolveSelectorOrExit(id)
892
- const name = process.argv[5]
893
- if (name === undefined) { console.error('usage: spex session rename <SEL> "<name>" (an explicit "" clears the override)'); process.exit(2) }
894
- if (await c.clientRename(full, name)) console.log(name.trim() ? `${full} -> renamed "${name.trim()}"` : `${full} -> name cleared (derived label restored)`)
895
- else { console.error(`spex session rename: no such session ${full}`); process.exit(2) }
896
- } else if (sub === 'attach') {
897
- // the HUMAN escape hatch (attach.ts, [[session-attach]]): foreground `tmux attach` into the worker's
898
- // real session. Guards fail loud BEFORE resolving: local-only (the tmux server is the backend
899
- // machine's) and terminal-only (an agent must never block its turn on it — capture/send).
900
- const { assertLocalBackend, attachSession } = await import('./attach.js')
901
- await assertLocalBackend()
902
- process.exit(await attachSession(await resolveSelectorOrExit(id)))
903
- } else {
904
- console.error(`spex session: unknown verb '${sub}' — new | ls | show | watch | wait | review | merge | send | interrupt | rename | resume | stop | close | attach | done | park | ask (spex help session)`)
905
- process.exit(2)
906
- }
907
- }
908
- } else if (cmd === 'internal') {
909
- const sub = process.argv[3]
910
- if (sub === 'trunk') {
911
- // print the resolved source-of-truth branch (layout.ts mainBranch(): config override → the main
912
- // checkout's current branch → 'main'). The pre-commit main-guard captures this so it blocks direct
913
- // commits on whatever the repo's trunk is actually named, never a hardcoded 'main'. One value, one
914
- // line; GET /api/settings exposes the same resolution (`.layout`).
915
- const { mainBranch } = await import('./layout.js')
916
- console.log(mainBranch())
917
- } else if (sub === 'spec-governors') {
918
- // Stable machine projection for spec-aware hooks: one real code: governor per row, with the live spec
919
- // path the block reason can point at. Empty stdout means ungoverned (including related-only).
920
- const file = process.argv[4]
921
- if (!file) { console.error('usage: spex internal spec-governors <path>'); process.exit(2) }
922
- const { specOwners, loadSpecsLite } = await import('./specs.js')
923
- const paths = new Map(loadSpecsLite().map((node) => [node.id, node.path]))
924
- for (const owner of specOwners(file)) {
925
- const path = paths.get(owner.id)
926
- if (!path) throw new Error(`governor '${owner.id}' has no live spec path`)
927
- console.log(`${owner.id}\t${path}`)
928
- }
929
- } else if (sub === 'shared-runtime-spawn') {
930
- const [cwd, logFile, pidFile, receiptFile, command] = process.argv.slice(4, 9)
931
- const args = process.argv.slice(9)
932
- if (!cwd || !logFile || !pidFile || !receiptFile || !command) {
933
- console.error('usage: spex internal shared-runtime-spawn <cwd> <log> <pid-file> <receipt-file> <command> [args...]')
934
- process.exit(2)
935
- }
936
- const { spawnDetachedRuntime } = await import('./runtime-ownership.js')
937
- const env = { ...process.env }
938
- delete env.SPEXCODE_SESSION_ID
939
- const runtime = await spawnDetachedRuntime({ cwd, logFile, pidFile, receiptFile, command, args, env })
940
- console.log(runtime.pid)
941
- } else if (sub === 'codex-generation-current' || sub === 'codex-generation-session') {
942
- // Both launch spellings resolve ONE endpoint and print it as shell assignments. Starting a root is the same
943
- // detached spawn either way — a session identity must never ride into the shared, project-scoped daemon.
944
- const resume = sub === 'codex-generation-session'
945
- const [root, ...rest] = process.argv.slice(4)
946
- const [sessionId, threadId, sessionCommand] = rest
947
- const command = resume ? sessionCommand : rest[0]
948
- if (!root || !command || (resume && (!sessionId || !threadId))) {
949
- console.error(resume
950
- ? 'usage: spex internal codex-generation-session <runtime-root> <session-id> <thread-id> <codex-command>'
951
- : 'usage: spex internal codex-generation-current <runtime-root> <codex-command>')
952
- process.exit(2)
953
- }
954
- const { ensureCodexCurrentGeneration, resolveCodexGenerationForResume } = await import('./codex-runtime-generations.js')
955
- const { spawnDetachedRuntime } = await import('./runtime-ownership.js')
956
- const { sessionIdentityEnvVars } = await import('./harness.js')
957
- const env = { ...process.env }
958
- for (const key of sessionIdentityEnvVars()) delete env[key]
959
- const start = async (candidate: { logFile: string; pidFile: string; receiptFile: string; socketPath: string }) => {
960
- await spawnDetachedRuntime({ cwd: root, logFile: candidate.logFile, pidFile: candidate.pidFile, receiptFile: candidate.receiptFile,
961
- command, args: ['app-server', '--listen', `unix://${candidate.socketPath}`], env })
962
- }
963
- const endpoint = resume
964
- ? await resolveCodexGenerationForResume(root, sessionId!, threadId!, start)
965
- : await ensureCodexCurrentGeneration(root, start)
966
- if (!endpoint) { console.error(`no exact Codex generation binding for session ${sessionId} thread ${threadId}`); process.exit(1) }
967
- const quote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`
968
- 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)}`)
969
- } else if (sub === 'codex-launch') {
970
- // BACKEND-owned codex thread. On the shared per-project app-server: thread/start { cwd = this worktree }
971
- // (codex loads that worktree's config/hooks/AGENTS.md), store the new id on the governed record (keyed by
972
- // SPEXCODE_SESSION_ID), fire the launch prompt as the FIRST turn — materializing the rollout — and print the
973
- // thread id. The launch script then `resume`s it in the visible TUI.
974
- const { codexStartThread, codexTurn, waitForCodexRollout, codexBinary, codexSupportsBypassHookTrust } = await import('./harness.js')
975
- const { markHarnessSessionId } = await import('./sessions.js')
976
- const sock = process.argv[4], cwd = process.argv[5]
977
- const prompt = process.argv.slice(6).join(' ')
978
- if (!sock || !cwd) { console.error('usage: spex internal codex-launch <sock> <cwd> [prompt...]'); process.exit(2) }
979
- // On the bypass-trust path (the codex install supports the flag → materialize skipped writeCodexTrust's hash),
980
- // the thread the BACKEND owns must carry `bypass_hook_trust` in thread/start's config so the app-server fires
981
- // the worktree's local hooks — mirror materialize's capability decision so the two stay in lockstep.
982
- const bypassHookTrust = codexSupportsBypassHookTrust(codexBinary(process.env.SPEXCODE_CODEX_CMD || 'codex'))
983
- // The governed record id rides into the thread's own shell environment (shell_environment_policy.set), so
984
- // every command this thread spawns knows which session it is — the codex equivalent of the launch-injected
985
- // id claude gets. codex-launch is exactly where both ids are known ([[harness-adapter]]).
986
- const ownId = process.env.SPEXCODE_SESSION_ID?.trim()
987
- const r = await codexStartThread(sock, cwd, bypassHookTrust, ownId ? { SPEXCODE_SESSION_ID: ownId } : undefined)
988
- if (!r.ok) { console.error(r.error); process.exit(1) }
989
- if (prompt) {
990
- const t = await codexTurn(sock, r.threadId, prompt, cwd)
991
- if (!t.ok) { console.error(t.error); process.exit(1) }
992
- // The visible TUI resumes this thread from its ON-DISK rollout; a freshly-spawned app-server acks the turn
993
- // but persists the rollout a few seconds LATE (verified live: the SAME thread's file lands at ~2-4s, not
994
- // lost). WAIT for it to land BEFORE storing the id / printing it, else FAIL LOUD — never store a
995
- // non-resumable harness_session_id (that permanently wedges every reopen). The 15s budget exceeds launch.sh's
996
- // fast-fail threshold, so a real failure exits past it and the retry loop won't spray duplicate-prompt threads.
997
- if (!await waitForCodexRollout(r.threadId, 20000)) {
998
- console.error(`codex thread ${r.threadId} started but persisted no rollout within 20s — app-server not ready; not storing a non-resumable id`)
999
- process.exit(1)
1000
- }
1001
- }
1002
- const sid = process.env.SPEXCODE_SESSION_ID
1003
- if (sid) markHarnessSessionId(sid, r.threadId)
1004
- console.log(r.threadId)
1005
- } else if (sub === 'opencode-capture') {
1006
- // opencode MINTS its own session id (no launch flag pins it), so the generated plugin's FIRST event calls
1007
- // this to store that id as harness_session_id on the governed record (SPEXCODE_SESSION_ID from the launch
1008
- // env, inherited by the opencode process → plugin). That is what lets reopen() resume the SAME
1009
- // conversation (`--session <id>`). A missing record/env is a clean no-op — a plugin loaded outside a
1010
- // governed launch has nothing to mark.
1011
- const { markHarnessSessionId } = await import('./sessions.js')
1012
- const ocid = process.argv[4]
1013
- if (!ocid) { console.error('usage: spex internal opencode-capture <opencode-session-id>'); process.exit(2) }
1014
- const sid = process.env.SPEXCODE_SESSION_ID
1015
- console.log(sid && markHarnessSessionId(sid, ocid) ? `captured ${ocid}` : 'noop (no governed session record)')
1016
- } else if (sub === 'claude-headless-run') {
1017
- const id = process.argv[4], runtimeDir = process.argv[5], claudeCmd = process.argv[6]
1018
- const divider = process.argv[7]
1019
- if (!id || !runtimeDir || !claudeCmd || divider !== '--') {
1020
- console.error('usage: spex internal claude-headless-run <session-id> <runtime-dir> <claude-cmd> -- [--session-id <id> <prompt> | --resume <id>]')
1021
- process.exit(2)
1022
- }
1023
- const { runClaudeHeadlessController } = await import('./claude-headless.js')
1024
- await runClaudeHeadlessController(id, runtimeDir, claudeCmd, process.argv.slice(8))
1025
- } else if (sub === 'pi-headless-run') {
1026
- const id = process.argv[4], runtimeDir = process.argv[5], piCmd = process.argv[6]
1027
- const divider = process.argv[7]
1028
- if (!id || !runtimeDir || !piCmd || divider !== '--') {
1029
- console.error('usage: spex internal pi-headless-run <session-id> <runtime-dir> <pi-cmd> -- [--session-id <id> <prompt> | --session <id>]')
1030
- process.exit(2)
1031
- }
1032
- const { runPiHeadlessController } = await import('./pi-headless.js')
1033
- await runPiHeadlessController(id, runtimeDir, piCmd, process.argv.slice(8))
1034
- } else if (sub === 'commit-surgery') {
1035
- // the pre-commit footprint anchor ([[commit-surgery]]): unconditional materialize + staged-index repair
1036
- // (strip our sentinel block from staged blobs, unstage HEAD-untracked generated artifacts). Called only
1037
- // by the planted pre-commit hook; repairs and proceeds, never rejects. Exit non-zero only on an internal
1038
- // error — the hook treats that as advisory (warn + continue), CI lint remains the real gate.
1039
- const { commitSurgery } = await import('./commit-surgery.js')
1040
- commitSurgery()
1041
- } else if (sub === 'refresh-footprint') {
1042
- // the post-checkout / post-merge freshness anchor ([[commit-surgery]]): a quiet materialize after a git
1043
- // state transition (the only events that can move the materialize's inputs — .spec/.plugins arrive by commit,
1044
- // merge, or checkout). Best-effort and silent on success; hooks call it fire-and-forget.
1045
- const { materialize } = await import('./materialize.js')
1046
- try { materialize() } catch (e) { console.error(`spexcode: footprint refresh failed (${(e as Error).message})`); process.exit(1) }
1047
- } else if (sub === 'codex-turn') {
1048
- // fire a follow-up turn on an OWNED thread over the per-project socket (the delivery channel, exposed for
1049
- // tests / scripts). steer-vs-start is chosen live from the thread read.
1050
- const { codexTurn } = await import('./harness.js')
1051
- const sock = process.argv[4], tid = process.argv[5], text = process.argv.slice(6).join(' ')
1052
- if (!sock || !tid || !text) { console.error('usage: spex internal codex-turn <sock> <threadId> <text...>'); process.exit(2) }
1053
- const r = await codexTurn(sock, tid, text)
1054
- if (r.ok) { console.log('ok') } else { console.error(r.error); process.exit(1) }
1055
- } else if (sub === 'check-staged') {
1056
- // the pre-commit hook's eval backstop: a staged stray evidence blob or malformed eval.md rejects the
1057
- // commit. Logic lives in spec-eval; the hook shims here.
1058
- const { checkStaged } = await import('../../spec-eval/src/cli.js')
1059
- process.exit(checkStaged())
1060
- } else if (sub === 'session-state') {
1061
- // a lifecycle hook authors the session's state: active|awaiting|parked|error|asking
1062
- // [--propose] [--note] [--session] — the machine face of the typeable done/park/ask declarations.
1063
- const { s, sess, mark, noRecord, noteEcho } = await stateKit()
1064
- const st = process.argv[4] as any
1065
- const ok = mark(() => s.markState(st, { proposal: flag('propose') as any, note: flag('note'), sessionId: sess }))
1066
- console.log(ok.ok ? `state -> ${st}${noteEcho(flag('note'))}` : ok.reason ?? noRecord())
1067
- } else if (sub === 'session-fail') {
1068
- // StopFailure is one native source for the shared active-only turn-failure CAS. A declaration or explicit
1069
- // stop that landed first is authoritative, just as it is for Codex notifications and headless exits.
1070
- const { s, sess, mark } = await stateKit()
1071
- const failed = mark(() => s.markTurnFailure(sess, 'claude turn failed'))
1072
- console.log(failed.ok ? 'marked error' : failed.reason ?? 'noop (session is not live active)')
1073
- } else if (sub === 'session-turn-fail') {
1074
- // Headless adapters report an ephemeral turn's non-zero exit through this one shared CAS. A declaration
1075
- // that landed before teardown wins, so a late child close can never erase an agent-authored state.
1076
- const sessionId = process.argv[4], harness = process.argv[5], exitCode = process.argv[6]
1077
- if (!sessionId || !harness || !exitCode) {
1078
- console.error('usage: spex internal session-turn-fail <session-id> <harness> <exit-code|signal>')
1079
- process.exit(2)
1080
- }
1081
- const { markHeadlessTurnFailure } = await import('./sessions.js')
1082
- console.log(markHeadlessTurnFailure(sessionId, harness, exitCode) ? `marked error (${harness} ${exitCode})` : 'noop (session is not live active)')
1083
- } else if (sub === 'session-idle') {
1084
- // the Notification(idle_prompt) hook marks its session (--session from the payload) idle when claude waits
1085
- // at its prompt. INFERRED, so guarded active-only: it no-ops unless the current status is exactly `active`,
1086
- // never clobbering a deliberate awaiting/asking/parked/error declaration. Distinct from `session ask`
1087
- // (the agent deliberately asking the human) — idle is the undeclared stop the Stop gate missed.
1088
- const { s, sess, mark } = await stateKit()
1089
- const idled = mark(() => s.markIdle(sess))
1090
- console.log(idled.ok ? 'idle' : idled.reason ?? 'noop (no session record, or not active)')
1091
- } else if (sub === 'commit-gate') {
1092
- // the Stop gate's deterministic commit check (from cwd = the worktree): exit 0 if the node branch is
1093
- // ready to declare done, else print the reason and exit 1. Takes the PROPOSAL being judged — `merge`
1094
- // additionally requires commits ahead of main, `nothing` only a clean tree (see mergeReadiness). Uses
1095
- // git() so the hook's exported GIT_DIR/GIT_INDEX_FILE don't misdirect repo discovery (see git.ts).
1096
- const { s } = await stateKit()
1097
- const kind = positionals(4)[0] === 'nothing' ? 'nothing' : 'merge'
1098
- const r = s.mergeReadiness(kind)
1099
- if (r.ready) { console.log('ready'); process.exit(0) }
1100
- console.log(r.reason)
1101
- process.exit(1)
1102
- } else if (sub === 'nudge') {
1103
- // the post-merge hook prints the (toggle-aware) issue nudge for a merged node — never typed.
1104
- const { nudge } = await import('./localIssues.js')
1105
- const text = nudge(positionals(4)[0] || '')
1106
- if (text) console.log(text)
1107
- } else {
1108
- const { commandHelp } = await import('./help.js')
1109
- console.error(commandHelp('internal'))
1110
- process.exit(2)
1111
- }
1112
- } else {
1113
- console.error(`spex: unknown command '${cmd}' (try: spex help)`)
1114
- process.exit(2)
1115
- }