spexcode 0.6.7 → 0.7.0-next.0

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 (247) hide show
  1. package/README.md +12 -7
  2. package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
  3. package/node_modules/@spexcode/session-application/dist/index.js +35 -0
  4. package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
  5. package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
  6. package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
  7. package/node_modules/@spexcode/session-application/dist/production.js +438 -0
  8. package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
  9. package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
  10. package/node_modules/@spexcode/session-application/package.json +29 -0
  11. package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
  12. package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
  13. package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
  14. package/node_modules/@spexcode/session-events/dist/index.js +245 -0
  15. package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
  16. package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
  17. package/node_modules/@spexcode/session-events/package.json +24 -0
  18. package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
  19. package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
  20. package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
  21. package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
  22. package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
  23. package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
  24. package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
  25. package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
  26. package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
  27. package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
  28. package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
  29. package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
  30. package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
  31. package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
  32. package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
  33. package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
  34. package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
  35. package/node_modules/@spexcode/session-runtime/package.json +24 -0
  36. package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
  37. package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
  38. package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
  39. package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
  40. package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
  41. package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +31 -0
  42. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +135 -0
  43. package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
  44. package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
  45. package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
  46. package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
  47. package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
  48. package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
  49. package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
  50. package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
  51. package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
  52. package/node_modules/@spexcode/session-topology/package.json +24 -0
  53. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +175 -21
  54. package/node_modules/@spexcode/spec-cli/dist/cli.js +121 -59
  55. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
  56. package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
  57. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
  58. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
  59. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
  60. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
  61. package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
  62. package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
  63. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
  64. package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
  65. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
  66. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
  67. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
  68. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
  69. package/node_modules/@spexcode/spec-cli/dist/guide.js +29 -18
  70. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
  71. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +24 -6
  72. package/node_modules/@spexcode/spec-cli/dist/harness.js +510 -72
  73. package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
  74. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
  75. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
  76. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
  77. package/node_modules/@spexcode/spec-cli/dist/host.js +98 -1
  78. package/node_modules/@spexcode/spec-cli/dist/index.js +294 -36
  79. package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
  80. package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
  81. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
  82. package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
  83. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
  84. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +46 -15
  85. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
  86. package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
  87. package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
  88. package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
  89. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
  90. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
  91. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
  92. package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
  93. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
  94. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
  95. package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
  96. package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
  97. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
  98. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
  99. package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
  100. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
  101. package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
  102. package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
  103. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
  104. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
  105. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
  106. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
  107. package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
  108. package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
  109. package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
  110. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +112 -15
  111. package/node_modules/@spexcode/spec-cli/dist/sessions.js +1553 -764
  112. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
  113. package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
  114. package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
  115. package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
  116. package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
  117. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
  118. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
  119. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
  120. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
  121. package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
  122. package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
  123. package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
  124. package/node_modules/@spexcode/spec-cli/package.json +8 -6
  125. package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
  126. package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
  127. package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
  128. package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
  129. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
  130. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
  131. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  132. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
  133. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
  134. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
  135. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
  136. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
  137. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
  138. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
  139. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  140. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
  141. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
  142. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
  143. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
  144. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
  145. package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
  146. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  147. package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
  148. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -1
  149. package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
  150. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
  151. package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
  152. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
  153. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
  154. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
  155. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
  156. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
  157. package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
  158. package/node_modules/@spexcode/spec-core/package.json +4 -1
  159. package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
  160. package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
  161. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
  162. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
  163. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
  164. package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
  165. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
  166. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
  167. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
  168. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  169. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  170. package/node_modules/@spexcode/transcript/dist/frames.d.ts +44 -0
  171. package/node_modules/@spexcode/transcript/dist/frames.js +85 -0
  172. package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
  173. package/node_modules/@spexcode/transcript/dist/index.js +5 -0
  174. package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
  175. package/node_modules/@spexcode/transcript/dist/live.js +65 -0
  176. package/node_modules/@spexcode/transcript/dist/parsers.d.ts +47 -0
  177. package/node_modules/@spexcode/transcript/dist/parsers.js +285 -0
  178. package/node_modules/@spexcode/transcript/dist/readers.d.ts +10 -0
  179. package/node_modules/@spexcode/transcript/dist/readers.js +296 -0
  180. package/node_modules/@spexcode/transcript/dist/turns.d.ts +42 -0
  181. package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
  182. package/node_modules/@spexcode/transcript/package.json +30 -0
  183. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  184. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  185. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  186. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  187. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  188. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  189. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  190. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  191. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  192. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  193. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  194. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  195. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  196. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  197. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  198. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  199. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  200. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  201. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  202. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  203. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  204. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  205. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  206. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  207. package/node_modules/smol-toml/LICENSE +24 -0
  208. package/node_modules/smol-toml/README.md +418 -0
  209. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  210. package/node_modules/smol-toml/dist/date.js +127 -0
  211. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  212. package/node_modules/smol-toml/dist/error.js +63 -0
  213. package/node_modules/smol-toml/dist/extract.js +69 -0
  214. package/node_modules/smol-toml/dist/index.cjs +734 -0
  215. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  216. package/node_modules/smol-toml/dist/index.js +33 -0
  217. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  218. package/node_modules/smol-toml/dist/parse.js +149 -0
  219. package/node_modules/smol-toml/dist/primitive.js +238 -0
  220. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  221. package/node_modules/smol-toml/dist/stringify.js +181 -0
  222. package/node_modules/smol-toml/dist/struct.js +179 -0
  223. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  224. package/node_modules/smol-toml/dist/util.js +89 -0
  225. package/node_modules/smol-toml/package.json +68 -0
  226. package/package.json +9 -3
  227. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
  228. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
  229. package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
  230. package/node_modules/@spexcode/session-core/dist/index.js +0 -5
  231. package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
  232. package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
  233. package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
  234. package/node_modules/@spexcode/session-core/dist/message.js +0 -53
  235. package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
  236. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
  237. package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
  238. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
  239. package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
  240. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
  241. package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
  242. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -26
  243. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
  244. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
  245. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
  246. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
  247. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
@@ -16,27 +16,23 @@
16
16
  # session-worktree creation, and the pre-commit/post-checkout/post-merge hooks). .plugins edits are
17
17
  # git-transactional: they take effect at the commit/checkout/merge that carries them, like any other source.
18
18
  set -u
19
- # args: `<harness> <Event>`. A harness id as $1 (claude|codex|opencode|pi|zcode|plugin) is consumed; otherwise we keep
20
- # $1 as the event and default the harness to claude — so a stale shim still written as `dispatch.sh <Event>`
21
- # keeps working. `plugin` is the bundle form ([[plugin-harness]]), `opencode` the generated event-bus plugin
19
+ # args: `<harness> <Event>`. The harness id is explicit. `plugin` is the bundle form ([[plugin-harness]]),
20
+ # `opencode` the generated event-bus plugin
22
21
  # ([[opencode-harness]]), `pi` the generated extension ([[pi-harness]]), and `zcode` the native adapter: all four
23
22
  # carry Claude-shaped payloads (Claude tool names + file_path), so they join the claude branch in harness.sh via
24
23
  # the default case — no parse arm of their own.
25
24
  harness=claude
26
- case "${1:-}" in claude|codex|opencode|pi|zcode|plugin) harness="$1"; shift ;; esac
25
+ case "${1:-}" in claude|codex|opencode|pi|zcode|plugin) harness="$1"; shift ;; *)
26
+ printf 'dispatch.sh: missing or unknown harness id\n' >&2
27
+ exit 64
28
+ ;; esac
27
29
  event="${1:?usage: dispatch.sh <harness> <Event>}"
28
30
  export SPEXCODE_HARNESS="$harness"
29
31
  # the harness.sh path (the adapter's shell mirror) — sibling of this script; hook handlers source it, and we
30
32
  # source it here too for hp_runtime_dir (the per-project store dir).
31
33
  hook_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
- tool_root="$(cd "$hook_root/.." && pwd)"
33
34
  export SPEXCODE_HARNESS_LIB="$hook_root/harness.sh"
34
35
  . "$SPEXCODE_HARNESS_LIB"
35
- if [ -n "${SPEX:-}" ]; then
36
- read -r -a spex_cmd <<< "$SPEX"
37
- else
38
- spex_cmd=("$tool_root/bin/spex.mjs")
39
- fi
40
36
  proj="${CLAUDE_PROJECT_DIR:-$PWD}"
41
37
  # the manifest lives in THIS tree's materialize slot of the GLOBAL per-project store (mirrors layout.treeSlotDir),
42
38
  # NOT the worktree — and per tree, so a dispatch can only read the manifest of the tree it fires in
@@ -46,8 +42,7 @@ rt="$(cd "$proj" 2>/dev/null && hp_runtime_dir)" || rt=""
46
42
  slot="$(cd "$proj" 2>/dev/null && hp_tree_dir)" || slot=""
47
43
 
48
44
  # A project transport can outlive the tree that installed it. The current tree's last successful materialize
49
- # is the authority for whether its events are active. Before the v1 marker, an absent allowlist is the legacy
50
- # shape; afterwards absence means this tree never successfully selected a harness and dispatch stays inert.
45
+ # is the authority for whether its events are active. A tree without a published selection is inert.
51
46
  allowed="$slot/harnesses"
52
47
  if [ -f "$allowed" ]; then
53
48
  grep -Fxq "$harness" "$allowed" || exit 0
@@ -56,17 +51,8 @@ elif [ -f "$rt/harness-selection-v1" ]; then
56
51
  fi
57
52
 
58
53
  # --- dispatch ---------------------------------------------------------------------------------------------
59
- if [ -n "${SPEX_HOOK_MANIFEST:-}" ]; then
60
- manifest="$SPEX_HOOK_MANIFEST"
61
- else
62
- # migration window: a tree last materialized by a pre-slot toolchain has no slot until its next git-native
63
- # anchor — fall back to the legacy global manifest (its exact pre-migration behavior) so no hook (the
64
- # Stop gate included) silently no-ops. The legacy file is never written again; the next anchor plants the
65
- # slot and this branch goes dead.
66
- manifest="$slot/hooks-manifest"
67
- [ -f "$manifest" ] || manifest="$rt/hooks-manifest"
68
- fi
69
- [ -f "$manifest" ] || exit 0 # no manifest yet (materialize never ran) → nothing to dispatch
54
+ manifest="${SPEX_HOOK_MANIFEST:-$slot/hooks-manifest}"
55
+ [ -f "$manifest" ] || { printf 'dispatch.sh: current tree has no hook manifest\n' >&2; exit 78; }
70
56
  input="$(cat 2>/dev/null || true)" # capture stdin ONCE; each handler gets its own copy
71
57
  err="/tmp/.spex-hook-$$.err" # per-dispatch (pid-unique) stderr capture; no cross-session race
72
58
  cleanup() { rm -f "$err"; }
@@ -78,14 +64,6 @@ rc=0
78
64
  while IFS=$'\t' read -r ev order block script; do
79
65
  [ "$ev" = "$event" ] || continue
80
66
  handler="$proj/$script"
81
- # A seeded core hook is tracked project source, so package replacement cannot safely overwrite it. These
82
- # byte-exact default revisions compose an ask note into JSON with sed; route only them to the package
83
- # implementation. `cmp` makes a user-modified hook ineligible without a platform-specific hash utility.
84
- if [ "$script" = '.spec/project/.plugins/core/mark-active/mark-active.sh' ] &&
85
- { cmp -s "$handler" "$hook_root/compat/mark-active-sed-v0.fixture" ||
86
- cmp -s "$handler" "$hook_root/compat/mark-active-0.5.2-eef1.fixture"; }; then
87
- handler="$tool_root/templates/spec/project/.plugins/core/mark-active/mark-active.sh"
88
- fi
89
67
  out="$(printf '%s' "$input" | bash "$handler" 2>"$err")"; code=$?
90
68
  [ -n "$out" ] && printf '%s' "$out"
91
69
  if [ "$block" = "true" ] && { [ "$code" = "2" ] || printf '%s' "$out" | grep -q '"decision"[[:space:]]*:[[:space:]]*"block"'; }; then
@@ -101,5 +79,15 @@ while IFS=$'\t' read -r ev order block script; do
101
79
  fi
102
80
  rc=2
103
81
  fi
82
+ # FAIL LOUD. A non-blocking handler's failure used to vanish completely: its exit code was dropped and its
83
+ # stderr was overwritten by the next handler and deleted on exit, so a lifecycle hook that could not write
84
+ # left NO trace anywhere — the board kept whatever state it last held and the reader had to guess whether
85
+ # the hook had run at all. That silence is what let a whole fleet's mark-active and stop-gate die unnoticed.
86
+ # Reporting is all this does: a non-blocking hook must not change the dispatch verdict, so `rc` stays the
87
+ # blocking handlers' to set, and a noisy hook can never turn into a gate.
88
+ if [ "$code" != 0 ] && [ "$block" != "true" ]; then
89
+ printf 'dispatch.sh: %s handler %s exited %s\n' "$event" "$script" "$code" >&2
90
+ [ -s "$err" ] && cat "$err" >&2
91
+ fi
104
92
  done < "$manifest"
105
93
  exit "$rc"
@@ -84,8 +84,8 @@ hp_session_id() {
84
84
  codex) hp_field "$1" session_id ;;
85
85
  *) pid=$(hp_field "$1" session_id)
86
86
  if [ -n "$pid" ] && [ -n "$SPEXCODE_SESSION_ID" ] && [ "$pid" != "$SPEXCODE_SESSION_ID" ] \
87
- && [ ! -e "$(hp_store_dir "$pid")/session.json" ] \
88
- && [ -e "$(hp_store_dir "$SPEXCODE_SESSION_ID")/session.json" ]; then
87
+ && [ ! -e "$(hp_store_dir "$pid")/runtime.json" ] \
88
+ && [ -e "$(hp_store_dir "$SPEXCODE_SESSION_ID")/runtime.json" ]; then
89
89
  printf '%s' "$SPEXCODE_SESSION_ID"
90
90
  else
91
91
  printf '%s' "${pid:-$SPEXCODE_SESSION_ID}"
@@ -139,19 +139,19 @@ hp_tree_dir() {
139
139
  # payload session_id on codex: the codex THREAD id, NOT the SpexCode record id the dir is keyed by. So when no
140
140
  # record sits at <id> directly, find the one record that captured this id as `harness_session_id` (the backend
141
141
  # stored it at thread/start, before the first tool turn).
142
- # A grep over the few session.json files — no jq on the hot path; the trailing quote anchors the value so a
142
+ # A grep over the few runtime.json files — no jq on the hot path; the trailing quote anchors the value so a
143
143
  # thread id can't match a longer one as a prefix. Direct hit wins; a miss with no alias echoes the direct path
144
144
  # unchanged, so the caller's `[ -e "$rec" ]` still no-ops gracefully. Mirrors layout.ts `readAliasedRawRecord`.
145
145
  hp_store_dir() {
146
146
  local rd; rd=$(hp_runtime_dir) || return 1
147
147
  local direct="$rd/sessions/$1"
148
- if [ -e "$direct/session.json" ]; then printf '%s' "$direct"; return 0; fi
148
+ if [ -e "$direct/runtime.json" ]; then printf '%s' "$direct"; return 0; fi
149
149
  # same two-halves-of-absence rule as layout.ts's readAliasedRecordEntry: an id owning a store dir is already
150
150
  # one of ours, so its emptiness is settled and the alias grep is the wrong question (and a whole-store scan).
151
151
  if [ -d "$direct" ]; then printf '%s' "$direct"; return 0; fi
152
152
  local hit
153
- hit=$(grep -lF "\"harness_session_id\": \"$1\"" "$rd"/sessions/*/session.json 2>/dev/null | head -1)
154
- [ -n "$hit" ] && { printf '%s' "${hit%/session.json}"; return 0; }
153
+ hit=$(grep -lF "\"harness_session_id\": \"$1\"" "$rd"/sessions/*/runtime.json 2>/dev/null | head -1)
154
+ [ -n "$hit" ] && { printf '%s' "${hit%/runtime.json}"; return 0; }
155
155
  printf '%s' "$direct"
156
156
  }
157
157
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-cli",
3
- "version": "0.6.7",
3
+ "version": "0.7.0-next.0",
4
4
  "type": "module",
5
5
  "description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
6
6
  "bin": {
@@ -21,7 +21,6 @@
21
21
  ],
22
22
  "bundledDependencies": [
23
23
  "@spexcode/spec-core",
24
- "@spexcode/session-core",
25
24
  "@spexcode/spec-eval",
26
25
  "@spexcode/spec-forge"
27
26
  ],
@@ -36,10 +35,13 @@
36
35
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
37
36
  },
38
37
  "dependencies": {
39
- "@spexcode/session-core": "0.6.7",
40
- "@spexcode/spec-core": "0.6.7",
41
- "@spexcode/spec-eval": "0.6.7",
42
- "@spexcode/spec-forge": "0.6.7"
38
+ "@spexcode/session-application": "0.7.0-next.0",
39
+ "@spexcode/session-selflaunch": "0.7.0-next.0",
40
+ "@spexcode/spec-core": "0.7.0-next.0",
41
+ "@spexcode/spec-eval": "0.7.0-next.0",
42
+ "@spexcode/spec-forge": "0.7.0-next.0",
43
+ "@spexcode/transcript": "0.7.0-next.0",
44
+ "smol-toml": "^1.8.0"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@types/node": "^20.16.0",
@@ -15,10 +15,12 @@
15
15
  [ "${SPEXCODE_DEFER_FOOTPRINT_REFRESH:-}" = "session-create" ] && exit 0
16
16
  main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
17
17
  repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
18
- if command -v spex >/dev/null 2>&1; then
19
- spex internal refresh-footprint >/dev/null 2>&1 || true
18
+ if [ -x "$repo_root/spec-cli/bin/spex.mjs" ]; then
19
+ "$repo_root/spec-cli/bin/spex.mjs" internal refresh-footprint >/dev/null 2>&1 || true
20
20
  elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
21
21
  "$repo_root/node_modules/.bin/spex" internal refresh-footprint >/dev/null 2>&1 || true
22
+ elif command -v spex >/dev/null 2>&1; then
23
+ spex internal refresh-footprint >/dev/null 2>&1 || true
22
24
  elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
23
25
  "$main_root/spec-cli/bin/spex.mjs" internal refresh-footprint >/dev/null 2>&1 || true
24
26
  fi
@@ -13,8 +13,9 @@
13
13
  main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
14
14
  repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
15
15
  spex_run() {
16
- if command -v spex >/dev/null 2>&1; then spex "$@"
16
+ if [ -x "$repo_root/spec-cli/bin/spex.mjs" ]; then "$repo_root/spec-cli/bin/spex.mjs" "$@"
17
17
  elif [ -x "$repo_root/node_modules/.bin/spex" ]; then "$repo_root/node_modules/.bin/spex" "$@"
18
+ elif command -v spex >/dev/null 2>&1; then spex "$@"
18
19
  elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then "$main_root/spec-cli/bin/spex.mjs" "$@"
19
20
  else return 127; fi
20
21
  }
@@ -12,10 +12,12 @@
12
12
  main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
13
13
  repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
14
14
  spex_kind=
15
- if command -v spex >/dev/null 2>&1; then
16
- spex_kind=path
15
+ if [ -x "$repo_root/spec-cli/bin/spex.mjs" ]; then
16
+ spex_kind=pkg
17
17
  elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
18
18
  spex_kind=local
19
+ elif command -v spex >/dev/null 2>&1; then
20
+ spex_kind=path
19
21
  elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
20
22
  spex_kind=pkg
21
23
  fi
@@ -23,7 +25,7 @@ spex_cli() {
23
25
  case "$spex_kind" in
24
26
  path) spex "$@" ;;
25
27
  local) "$repo_root/node_modules/.bin/spex" "$@" ;;
26
- pkg) "$main_root/spec-cli/bin/spex.mjs" "$@" ;;
28
+ pkg) if [ -x "$repo_root/spec-cli/bin/spex.mjs" ]; then "$repo_root/spec-cli/bin/spex.mjs" "$@"; else "$main_root/spec-cli/bin/spex.mjs" "$@"; fi ;;
27
29
  *) return 127 ;;
28
30
  esac
29
31
  }
@@ -65,10 +65,12 @@ fi
65
65
  main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
66
66
  repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
67
67
  spex_kind=
68
- if command -v spex >/dev/null 2>&1; then
69
- spex_kind=path
68
+ if [ -x "$repo_root/spec-cli/bin/spex.mjs" ]; then
69
+ spex_kind=pkg
70
70
  elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
71
71
  spex_kind=local
72
+ elif command -v spex >/dev/null 2>&1; then
73
+ spex_kind=path
72
74
  elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
73
75
  spex_kind=pkg
74
76
  fi
@@ -76,7 +78,7 @@ spex_cli() {
76
78
  case "$spex_kind" in
77
79
  path) spex "$@" ;;
78
80
  local) "$repo_root/node_modules/.bin/spex" "$@" ;;
79
- pkg) "$main_root/spec-cli/bin/spex.mjs" "$@" ;;
81
+ pkg) if [ -x "$repo_root/spec-cli/bin/spex.mjs" ]; then "$repo_root/spec-cli/bin/spex.mjs" "$@"; else "$main_root/spec-cli/bin/spex.mjs" "$@"; fi ;;
80
82
  *) return 127 ;;
81
83
  esac
82
84
  }
@@ -21,10 +21,5 @@ resolution. A preset with `{{targets}}` always receives the resolved target bloc
21
21
  gets a target block only when the invocation actually names a target, so a targetless utility remains a small
22
22
  prompt. An unknown leading `/name` stays ordinary prompt text and is never swallowed or guessed.
23
23
 
24
- This node is a **shelf, not a surface** (the [[prompts]] shape): it declares no `surface` field and
25
- gathers nothing itself. Discovery is recursive and field-driven (surface), so a resident plugs in
26
- exactly as it would at the root — the gather set is path-independent, so shelving a command changes
27
- nothing about what `/api/plugins` and the launcher offer. A plugin that serves BOTH surfaces (e.g.
28
- [[distill]], skill and command) shelves once by its primary identity, never duplicated. The init
29
- templates mirror this layout. The shelf stays pure presentation: moving a resident beneath it changes
30
- neither that plugin's identity nor the surfaces gathered from its frontmatter.
24
+ This node is a **shelf, not a surface**. Its routing and relocation invariant is owned once by
25
+ [[.plugins]]'s shelf invariant; this node only describes command-specific discovery and invocation.
@@ -1,15 +1,9 @@
1
1
  #!/usr/bin/env bash
2
- # On an idle_prompt notification, mark the session idle (the active-only guard in `internal session-idle` keeps a
3
- # deliberate awaiting/asking/parked/error declaration from being clobbered). GATED on `governed`: only a
4
- # dashboard-launched session has board state to mark a self-launched agent's idle is none of our business.
5
- # State lives in the per-session GLOBAL record session.json (keyed by the harness session_id, grouped per-
6
- # project — see hp_store_dir); the id is passed to the cli via `--session` so it writes the right record
7
- # without depending on the worktree (which no longer holds any session file). NOTE the Notification event is
8
- # Claude-only ([[harness-adapter]]: Codex fires no Notification), so this never runs under Codex.
2
+ # On an idle_prompt notification, ask the canonical lifecycle writer to infer idle. The writer owns both the
3
+ # governed-session boundary and the active-only compare-and-set; this hook only decodes the native event and
4
+ # passes its acting session id. NOTE the Notification event is Claude-only ([[harness-adapter]]: Codex fires no
5
+ # Notification), so this never runs under Codex.
9
6
  . "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
10
7
  payload=$(cat 2>/dev/null)
11
8
  sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
12
- sdir=$(hp_store_dir "$sid") || exit 0
13
- rec="$sdir/session.json"
14
- grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
15
9
  [ "$(hp_notification_type "$payload")" = idle_prompt ] && exec ${SPEX:-spex} internal session-idle --session "$sid"
@@ -10,4 +10,4 @@ block: false
10
10
  ---
11
11
  Catches the undeclared stop the [[stop-gate]] misses. When the harness signals — via an idle-prompt notification — that the agent is simply sitting idle at its prompt rather than working, this hook marks the session `idle`, so a session that quietly ran out of things to do is not left reading as active on the board.
12
12
 
13
- It acts only on the idle-prompt notification, ignoring every other notification kind. As a board-lifecycle hook it also acts only on a GOVERNED session it resolves the record in the global store from the payload's `session_id` and no-ops unless `governed: true` then marks idle via `spex internal session-idle --session <id>`. It is guarded so it never clobbers a deliberate declaration: marking idle applies only to a session still in the undeclared `active` state, leaving any considered `awaiting`, `asking`, `parked`, or `error` claim untouched. Together with [[stop-gate]] and [[session-fail]] it closes the last gap where a session could stop without its true state reaching the board.
13
+ It acts only on the idle-prompt notification, ignoring every other notification kind. As a board-lifecycle hook it passes the payload's acting `session_id` to `spex internal session-idle --session <id>`; the canonical writer owns governed-session resolution and returns a no-op for self-launched or missing records. It is guarded so it never clobbers a deliberate declaration: marking idle applies only to a session still in the undeclared `active` state, leaving any considered `awaiting`, `asking`, `parked`, or `error` claim untouched. Together with [[stop-gate]] and [[session-fail]] it closes the last gap where a session could stop without its true state reaching the board.
@@ -13,15 +13,15 @@
13
13
  # request_user_input tool) — read via hp_is_ask, so this hook never names a harness tool.
14
14
  # Fires BEFORE the tool runs, so a `spex session done` declaration (itself a tool) lands AFTER this and wins;
15
15
  # the next real tool flips back to active, forcing a fresh Stop-gate declaration.
16
- # @@@ read cheap, write through the ONE writer - this hook is on the hot path (every tool call), so it does
17
- # its own READ in pure shell: three exact-line greps answer "already active, nothing stale to clear?", which
18
- # is the overwhelmingly common case, and that path exits without spawning anything. When there IS a change to
19
- # make it hands the write to `spex internal session-state`, the same structured writer the CLI declarations
20
- # use. It never edits session.json itself: an asking note is arbitrary human/agent prose, and a writer that
21
- # substitutes prose into existing JSON meets a quote, a backslash, or a newline and leaves a record nothing
22
- # can parse which is how a live session came to report "no session record" ([[sessions-core]]).
23
- # @@@ global store - state lives NOT in the worktree but in the per-session GLOBAL record session.json, keyed
24
- # by the harness session_id, grouped per-project (see hp_store_dir). GATED on `governed`: a user-self-launched
16
+ # @@@ one writer - this hook is on the hot path (every tool call), but it must not inspect runtime.json to
17
+ # decide whether a transition is needed. That file is only a runtime/worktree envelope; the canonical
18
+ # session application is the lifecycle authority. The structured writer is idempotent for an unchanged state,
19
+ # so every eligible event goes through the same writer and cannot short-circuit on a second fact. It never
20
+ # edits runtime.json itself: an asking note is arbitrary prose, and shell substitution is not a record writer
21
+ # ([[sessions-core]]).
22
+ # @@@ global store - the lifecycle state lives in the canonical session application, keyed by the harness
23
+ # session_id, grouped per-project (see hp_store_dir). The sibling runtime.json is only the runtime/worktree
24
+ # envelope. GATED on `governed`: a user-self-launched
25
25
  # (non-governed) session has no board to feed, so this no-ops on it. cwd = the session worktree.
26
26
  . "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
27
27
  payload=$(cat 2>/dev/null)
@@ -32,20 +32,20 @@ payload=$(cat 2>/dev/null)
32
32
  # discriminator is the payload's own top-level agent_id stamp (hp_is_subagent) — deterministic, never a
33
33
  # timing window.
34
34
  [ -n "$(hp_is_subagent "$payload")" ] && exit 0
35
+ # Managed watch deliveries are supervision messages, not work performed by this session. They arrive through
36
+ # the harness's ordinary UserPromptSubmit seam, so the freshness hook must recognize the protocol's exact
37
+ # prefix before treating that seam as a human re-entry. This is deliberately a prefix check, not a broad
38
+ # text heuristic: only the canonical `[spex watch] ` wire form is exempt; ordinary prompts and all tools still
39
+ # mark active.
40
+ if [ "$(hp_field "$payload" hook_event_name)" = "UserPromptSubmit" ]; then
41
+ case "$(hp_field "$payload" prompt)" in
42
+ "[spex watch] "*) exit 0 ;;
43
+ esac
44
+ fi
35
45
  sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
36
- sdir=$(hp_store_dir "$sid") || exit 0
37
- rec="$sdir/session.json"
38
- # board-lifecycle gate: only a GOVERNED (dashboard-launched) session has a board state to maintain.
39
- grep -q '^[[:space:]]*"governed"[[:space:]]*:[[:space:]]*true,\?$' "$rec" 2>/dev/null || exit 0
40
-
41
- # does FIELD's line hold exactly VALUE? The record is written one-field-per-line by the single writer
42
- # (sessions.ts writeRecord), so a whole-line match is exact — and, unlike a value regex, it cannot be fooled
43
- # by an escaped quote inside a neighbouring note.
44
- jline_is() { grep -q "^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"$2\",\?$" "$rec" 2>/dev/null; }
45
-
46
- # The writer's own stdout is a human confirmation, not hook output — swallow it so a PreToolUse handler never
47
- # emits a decision-shaped line; its stderr (a refusal — a corrupt or retired record) still surfaces. We always
48
- # exit 0: this hook observes freshness, it is not a gate on the tool that triggered it.
46
+ # The canonical writer owns governed/lifecycle validation. The hook must not inspect runtime.json: that file is
47
+ # a runtime envelope, and using it as a gate is how old/missing envelopes silently disabled mark-active.
48
+ # The writer's stdout is a human confirmation, not hook output; stderr remains visible for real refusals.
49
49
  if [ -n "$(hp_is_ask "$payload")" ]; then
50
50
  # first question's text → the note (best-effort). It is passed as ONE argv word to the writer, so quotes,
51
51
  # backslashes, newlines, and non-ASCII reach the record intact — no shell ever composes the JSON.
@@ -53,7 +53,5 @@ if [ -n "$(hp_is_ask "$payload")" ]; then
53
53
  exit 0
54
54
  fi
55
55
 
56
- # cheap path: already active with nothing stale to clear → no-op (the common every-tool case), no spawn.
57
- jline_is status active && jline_is proposal '' && jline_is note '' && exit 0
58
56
  ${SPEX:-spex} internal session-state active --session "$sid" >/dev/null
59
57
  exit 0
@@ -15,8 +15,16 @@ The state is read from ONE structured field in the hook payload, never sniffed f
15
15
 
16
16
  The one activity that does NOT count as the session acting is an IN-PROCESS SUBAGENT's tool call (the harness's Task tool — a sub-conversation inside the same process). Such a call fires the parent's hooks carrying the parent's `session_id`, so without a discriminator a supervising parent could never hold a declared state: its own subagents erased every park/ask within seconds and raced the stop-gate into "undeclared stop". The harness stamps subagent-executed calls with a top-level `agent_id` field the parent's own calls never carry; `hp_is_subagent` reads that stamp deterministically (scanning only the pre-`tool_input` payload prefix, where a tool parameter or file content can never fake an unescaped key), and this hook skips the flip entirely. A subagent working is its parent supervising, not the parent moving on — the parent's own next tool call still flips as before.
17
17
 
18
- It is a board-lifecycle hook, so it acts only on a GOVERNED (dashboard-launched) session — it resolves that session's record in the global per-session store from the payload's `session_id` and no-ops unless `governed: true`. The state it writes lives in that record's `session.json` (state), but it never edits that file itself: it READS it in pure shell (whole-line matches, the hot path stays jq-free) and hands every write to `spex internal session-state`, the one structured writer the CLI declarations use — an asking note is arbitrary prose, and a shell that substitutes prose into existing JSON eventually writes a record nothing can parse.
18
+ It is a board-lifecycle hook, so it acts only on a GOVERNED (dashboard-launched) session — it resolves that session's runtime envelope in the global per-session store from the payload's `session_id` and no-ops unless `governed: true`. Lifecycle state is written to the canonical session application; `runtime.json` is not a lifecycle authority. The hook never edits that envelope itself: it hands every lifecycle write to `spex internal session-state`, the one structured writer the CLI declarations use — an asking note is arbitrary prose, and a shell that substitutes prose into existing JSON eventually writes a record nothing can parse.
19
19
 
20
- This hook carries no conversation. A message addressed to the session reaches its agent as an ordinary prompt through the harness adapter (delivery-queue), which is the only way anything enters a turn, so an inter-agent message is indistinguishable from a human one at the point of arrival. A hook that also injected mail delivered every message a second time and made the agent's context depend on which of two paths won a race; a freshness signal reports a fact about the session and hands nothing over.
20
+ This hook carries no conversation. A message addressed to the session reaches its agent as an ordinary prompt through the harness adapter (delivery-queue), which is the only way anything enters a turn. The one protocol exception is a managed watch notice: the canonical `[spex watch] ` prefix identifies a supervision snapshot, not work performed by the receiving agent, so UserPromptSubmit for that exact wire form does not flip the receiver to `active`. Other inter-agent prompts remain ordinary prompt activity. A hook that also injected mail delivered every message a second time and made the agent's context depend on which of two paths won a race; a freshness signal reports a fact about the session and hands nothing over.
21
21
 
22
22
  This is the freshness half of the [[core]] discipline: it keeps the board honest about whether a session is working, waiting, or asking, so the gates and the dashboard read a true present state rather than a stale one.
23
+
24
+ ## legacy handler migration
25
+
26
+ For the two known pre-structured `mark-active` source blobs still tracked by existing projects, dispatch performs
27
+ a bounded migration at the adapter boundary: it executes the current package-owned structured implementation,
28
+ and the next materialize replaces the old tracked handler with that implementation. This is a migration of a
29
+ legacy source identity, not a second lifecycle protocol or a permanent backward-compatibility path. The old
30
+ handler's envelope writes are never allowed to author current lifecycle state.
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # Mark the session errored when a turn ends on an API failure (StopFailure). GATED on `governed`: only a
3
- # dashboard-launched session has board state to mark. State lives in the per-session GLOBAL record (keyed by
4
- # the harness session_id, grouped per-project — see hp_store_dir); the id is passed to the cli via `--session`
5
- # so it writes the right record without depending on the worktree.
2
+ # Mark the session errored when a turn ends on an API failure (StopFailure). The canonical lifecycle writer owns
3
+ # the governed-session boundary and active-only compare-and-set; this hook only decodes the native event and
4
+ # passes its acting session id.
6
5
  . "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
7
6
  payload=$(cat 2>/dev/null)
7
+ # an IN-PROCESS SUBAGENT's failed turn (Claude's Task tool) fires the parent's hooks with the PARENT's
8
+ # session_id, so without this the parent's own record would be flipped to `error` by a helper it spawned —
9
+ # the same defect already fixed for mark-active, and the same discriminator fixes it: the payload's own
10
+ # top-level agent_id stamp. A subagent's turn dying is not THIS session's turn dying.
11
+ [ -n "$(hp_is_subagent "$payload")" ] && exit 0
8
12
  sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
9
- sdir=$(hp_store_dir "$sid") || exit 0
10
- rec="$sdir/session.json"
11
- grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
12
13
  exec ${SPEX:-spex} internal session-fail --session "$sid"
@@ -12,4 +12,6 @@ code:
12
12
  ---
13
13
  When a turn ends not because the agent declared but because the API itself failed, this hook structurally marks the session `error`. A failed turn is a real outcome the board must show, and without this signal the session would freeze under whatever state it last held — reading as "active" or "awaiting" long after it actually died.
14
14
 
15
- It is non-blocking on the failure event: the failure already happened, so the only job is to report it truthfully. As a board-lifecycle hook it acts only on a GOVERNED session resolved in the global store from the payload's `session_id` — and writes via `spex internal session-fail --session <id>`. That machine entry reaches the same live-active compare-and-set as Codex's native failed completion and a headless turn's non-zero exit (harness-adapter): only an undeclared, non-stopped `active` record becomes `error`. A declaration, explicit stop, or archive that landed first remains authoritative; a late native failure never rewrites it. This one writer keeps the [[stop-gate]] family's invariant intact for every harness while each adapter retains only its native failure signal.
15
+ **A SUBAGENT'S FAILED TURN IS NOT THIS SESSION'S FAILED TURN.** An in-process subagent (Claude's Task tool) fires the parent's hooks carrying the PARENT's `session_id`, so a helper the session spawned could flip the session that spawned it to `error` — a supervising parent marked dead by a delegate it is still supervising. The discriminator is the payload's own top-level `agent_id` stamp, the deterministic one [[mark-active]] already uses for the same reason; it is not a timing window. This is one defect class, so it gets one answer at both hooks rather than a second idea of what "this session acted" means.
16
+
17
+ It is non-blocking on the failure event: the failure already happened, so the only job is to report it truthfully. As a board-lifecycle hook it passes the payload's acting `session_id` to `spex internal session-fail --session <id>`; the canonical writer, not shell parsing of `runtime.json`, resolves the governed record and owns the live-active compare-and-set. Only an undeclared, non-stopped `active` record becomes `error`. A declaration, explicit stop, or archive that landed first remains authoritative; a late native failure never rewrites it. This one writer keeps the [[stop-gate]] family's invariant intact for every harness while each adapter retains only its native failure signal.
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ # A self-launch project opts in only by configuring the adopter database. No configured database means no listener.
5
+ if [ -z "${SPEX_SESSION_DATABASE_PATH+x}" ] && [ -z "${SPEX_SESSION_CONFIG+x}" ]; then
6
+ exit 0
7
+ fi
8
+
9
+ # The explicit command is one executable path; an invalid explicit value must not silently fall through to PATH.
10
+ cli="${SPEX_SESSION_CLI:-}"
11
+ if [ -z "$cli" ]; then
12
+ cli=$(command -v spex-session 2>/dev/null || true)
13
+ fi
14
+ if [ -z "$cli" ] || [ ! -x "$cli" ]; then
15
+ printf '%s\n' 'session-listen: spex-session CLI not found; install @spexcode/session-selflaunch (npm install) or set SPEX_SESSION_CLI to its executable path' >&2
16
+ exit 2
17
+ fi
18
+
19
+ for tool in awk base64 iconv od grep tail sed mktemp cat tr rm; do
20
+ if ! command -v "$tool" >/dev/null 2>&1; then
21
+ printf "session-listen: required delivery tool '%s' is missing; install it or repair PATH before retrying\n" "$tool" >&2
22
+ exit 2
23
+ fi
24
+ done
25
+
26
+ capability_error() {
27
+ printf "session-listen: required delivery capability '%s' is unavailable; install compatible tooling or repair PATH before retrying\n" "$1" >&2
28
+ exit 2
29
+ }
30
+
31
+ escape_awk='
32
+ BEGIN { ORS = "" }
33
+ NR > 1 { printf "\\n" }
34
+ {
35
+ for (i = 1; i <= length($0); i++) {
36
+ c = substr($0, i, 1)
37
+ if (c == "\\") printf "\\\\"
38
+ else if (c == "\"") printf "\\\""
39
+ else if (c == "\t") printf "\\t"
40
+ else if (c == "\r") printf "\\r"
41
+ else if (c == "\b") printf "\\b"
42
+ else if (c == "\f") printf "\\f"
43
+ else printf "%s", c
44
+ }
45
+ }
46
+ '
47
+ control_pattern='(^|[[:space:]])(00|01|02|03|04|05|06|07|08|0b|0c|0e|0f|1[0-9a-f])([[:space:]]|$)'
48
+ body64_sed='s/.*"bodyBase64":"\([^"]*\)".*/\1/p'
49
+
50
+ # Existence is not capability. These fixed vectors prove every non-default operation before at-most-once dequeue.
51
+ [ "$(printf ' A ' | tr -d '[:space:]')" = A ] || capability_error 'tr -d character classes'
52
+ [ "$(printf A | od -An -v -tx1 | tr -d '[:space:]')" = 41 ] || capability_error 'od hexadecimal bytes'
53
+ [ "$(printf QQ== | base64 -d 2>/dev/null | od -An -v -tx1 | tr -d '[:space:]')" = 41 ] \
54
+ || capability_error 'base64 -d exact decoding'
55
+ [ "$(printf '\303\251' | iconv -f UTF-8 -t UTF-8 2>/dev/null | od -An -v -tx1 | tr -d '[:space:]')" = c3a9 ] \
56
+ || capability_error 'iconv UTF-8 validation'
57
+ printf ' 00 ' | grep -Eq "$control_pattern" || capability_error 'grep extended quiet match'
58
+ if printf ' 0a ' | grep -Eq "$control_pattern"; then capability_error 'grep extended quiet exclusion'; fi
59
+ [ "$(printf AB | tail -c 1)" = B ] || capability_error 'tail byte selection'
60
+ [ "$(printf '%s' '{"bodyBase64":"QQ=="}' | sed -n "$body64_sed")" = QQ== ] \
61
+ || capability_error 'sed JSON field extraction'
62
+ [ "$(printf 'A\nB\n' | awk "$escape_awk")" = 'A\nB' ] || capability_error 'awk JSON text escaping'
63
+ [ "$(printf A | cat)" = A ] || capability_error 'cat byte emission'
64
+
65
+ . "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
66
+ payload=$(cat 2>/dev/null || true)
67
+ sid=$(hp_field "$payload" session_id)
68
+ [ -n "$sid" ] || exit 0
69
+ event=$(hp_field "$payload" hook_event_name)
70
+
71
+ case "$event" in
72
+ SessionStart)
73
+ "$cli" initialize --session-id "$sid" >/dev/null || exit 2
74
+ ;;
75
+ UserPromptSubmit)
76
+ decoded=$(mktemp "${TMPDIR:-/tmp}/spex-session-listen.XXXXXX") || {
77
+ printf '%s\n' 'session-listen: could not allocate a temporary body file; repair TMPDIR and retry' >&2
78
+ exit 2
79
+ }
80
+ escaped_file="$decoded.escaped"
81
+ output_file="$decoded.output"
82
+ if ! : >"$escaped_file" || ! : >"$output_file"; then
83
+ rm -f "$decoded"
84
+ rm -f "$escaped_file" "$output_file"
85
+ printf '%s\n' 'session-listen: could not prepare temporary delivery files; repair TMPDIR and retry' >&2
86
+ exit 2
87
+ fi
88
+ cleanup() { rm -f "$decoded" "$escaped_file" "$output_file"; }
89
+ trap cleanup EXIT
90
+ raw=$("$cli" dequeue --session-id "$sid") || exit 2
91
+ [ "$raw" = 'null' ] && exit 0
92
+ body64=$(printf '%s' "$raw" | sed -n "$body64_sed")
93
+ message_id=$(printf '%s' "$raw" | sed -n 's/.*"messageId":"\([^"]*\)".*/\1/p')
94
+ [ -n "$body64" ] || {
95
+ printf '%s\n' 'session-listen: spex-session dequeue returned invalid JSON (missing bodyBase64)' >&2
96
+ exit 2
97
+ }
98
+ if ! printf '%s' "$body64" | base64 -d >"$decoded" 2>/dev/null; then
99
+ printf '%s\n' 'session-listen: spex-session dequeue returned invalid bodyBase64' >&2
100
+ exit 2
101
+ fi
102
+ if ! iconv -f UTF-8 -t UTF-8 "$decoded" >/dev/null 2>/dev/null; then
103
+ printf 'session-listen: refusing non-UTF-8 body; messageId=%s bodyBase64=%s\n' "$message_id" "$body64" >&2
104
+ exit 2
105
+ fi
106
+ if od -An -v -tx1 "$decoded" | grep -Eq "$control_pattern"; then
107
+ printf 'session-listen: refusing control-byte body; messageId=%s bodyBase64=%s\n' "$message_id" "$body64" >&2
108
+ exit 2
109
+ fi
110
+ if ! awk "$escape_awk" "$decoded" >"$escaped_file"; then
111
+ printf 'session-listen: could not encode body for harness input; messageId=%s bodyBase64=%s\n' "$message_id" "$body64" >&2
112
+ exit 2
113
+ fi
114
+ if [ -s "$decoded" ] && [ ! -s "$escaped_file" ]; then
115
+ printf 'session-listen: non-empty body encoded to empty additionalContext; messageId=%s bodyBase64=%s\n' "$message_id" "$body64" >&2
116
+ exit 2
117
+ fi
118
+ last_byte=$(tail -c 1 "$decoded" | od -An -tx1 | tr -d '[:space:]')
119
+ if ! {
120
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"'
121
+ cat "$escaped_file"
122
+ [ "$last_byte" = 0a ] && printf '\\n'
123
+ printf '"}}\n'
124
+ } >"$output_file"; then
125
+ printf 'session-listen: could not assemble harness input; messageId=%s bodyBase64=%s\n' "$message_id" "$body64" >&2
126
+ exit 2
127
+ fi
128
+ if ! cat "$output_file"; then
129
+ printf 'session-listen: could not emit harness input; messageId=%s bodyBase64=%s\n' "$message_id" "$body64" >&2
130
+ exit 2
131
+ fi
132
+ ;;
133
+ esac
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: session-listen
3
+ surface: hook
4
+ status: active
5
+ hue: 280
6
+ events:
7
+ - SessionStart
8
+ - UserPromptSubmit
9
+ order: 20
10
+ block: true
11
+ ---
12
+ The backend-free self-launch message loop. On `SessionStart`, it initializes the harness's native session id in the
13
+ adopter protocol database; initialization is idempotent and creates no governed lifecycle record. On each
14
+ `UserPromptSubmit`, it performs exactly one at-most-once `spex-session dequeue` for that same native id. A message
15
+ body is decoded and emitted as the harness's `hookSpecificOutput.additionalContext` JSON, so the harness input seam
16
+ receives it without a resident process or wake-hint dependency. An empty queue is a successful no-op and emits no
17
+ stdout. The hook never derives a database path: the adopter CLI owns path resolution and locality checks.
18
+
19
+ Every external delivery tool and each non-default operation it supplies is proven with a fixed result vector before
20
+ `dequeue`, so a binary that exists but lacks or misimplements the required flag cannot consume a message. In
21
+ particular, `base64 -d` must decode `QQ==` to exactly the one byte `0x41`; checking only its exit status would admit a
22
+ shim that merely copies input. `-d` is used because GNU base64 on Linux and base64 on both fleet Macs running macOS
23
+ 15.6.1 were measured to decode that vector correctly; this is an observed common capability, not a platform legend.
24
+ The opaque body
25
+ is decoded to a temporary file, validated as UTF-8, and rejected loudly (with its `messageId` and original
26
+ `bodyBase64`) when it contains NUL or other JSON-hostile control bytes. Clean text is escaped with the existing awk
27
+ toolchain without command substitution, preserving embedded and trailing newlines; a non-empty body can never become
28
+ an empty additionalContext success.
29
+
30
+ The CLI is resolved at runtime through one explicit seam: a non-empty `SPEX_SESSION_CLI` wins, otherwise PATH is
31
+ searched for `spex-session`. If either protocol database environment variable is configured and no CLI can be
32
+ resolved, the hook fails loudly with an installation or `SPEX_SESSION_CLI` repair entrypoint. With neither database
33
+ variable configured, the project has not adopted this capability, so the hook exits silently without trying another
34
+ delivery path. Only these two events are bound: startup establishes the address, and prompt submission is the natural
35
+ harness input seam; a configured-but-broken adopter is a blocking hook failure so dispatch cannot hide it. There is no
36
+ daemon, polling, retry loop, observer, governed record, or compatibility path.
@@ -10,6 +10,8 @@ Use noun-first CLI commands; `spex help` is the authoritative command map.
10
10
 
11
11
  Anything a human needs to inspect, whether a file or a local webpage, goes out through `spex session files add` or
12
12
  `spex session web add`: never paste an absolute path or `host:port`, and never start a static server yourself.
13
+ Put raw file evidence in a persistent directory outside the product repository by default, then run
14
+ `spex session files ls` before review and repair or retract every `INVALID` handoff.
13
15
 
14
16
  When this session has a clearly running child session (`active` or `parked`), the parent is supervising rather
15
17
  than finished: declare `park`, not `done`/`awaiting`, until the child reports a settled state.