spexcode 0.6.7 → 0.6.8

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 (205) 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 +20 -0
  42. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +83 -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 +56 -19
  54. package/node_modules/@spexcode/spec-cli/dist/cli.js +102 -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/execution-trace.d.ts +1 -0
  63. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +2 -2
  64. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
  65. package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
  66. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
  67. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
  68. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
  69. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
  70. package/node_modules/@spexcode/spec-cli/dist/guide.js +20 -7
  71. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
  72. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -3
  73. package/node_modules/@spexcode/spec-cli/dist/harness.js +331 -50
  74. package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
  75. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
  76. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
  77. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
  78. package/node_modules/@spexcode/spec-cli/dist/host.js +93 -0
  79. package/node_modules/@spexcode/spec-cli/dist/index.js +324 -22
  80. package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
  81. package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
  82. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
  83. package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
  84. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +1 -1
  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/pty-bridge.js +14 -14
  88. package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
  89. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
  90. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
  91. package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
  92. package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
  93. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
  94. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
  95. package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
  96. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
  97. package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
  98. package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
  99. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
  100. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
  101. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
  102. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
  103. package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
  104. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +108 -15
  105. package/node_modules/@spexcode/spec-cli/dist/sessions.js +1465 -744
  106. package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
  107. package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
  108. package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
  109. package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
  110. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
  111. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
  112. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
  113. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
  114. package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
  115. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.d.ts +36 -0
  116. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +251 -0
  117. package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
  118. package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
  119. package/node_modules/@spexcode/spec-cli/package.json +6 -6
  120. package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
  121. package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
  122. package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
  123. package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
  124. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
  125. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
  126. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  127. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
  128. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
  129. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
  130. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
  131. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
  132. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
  133. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
  134. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  135. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
  136. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
  137. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
  138. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
  139. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
  140. package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
  141. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  142. package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
  143. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -0
  144. package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
  145. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
  146. package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
  147. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
  148. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
  149. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
  150. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
  151. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
  152. package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
  153. package/node_modules/@spexcode/spec-core/package.json +4 -1
  154. package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
  155. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
  156. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
  157. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
  158. package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
  159. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
  160. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
  161. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
  162. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  163. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  164. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  165. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  166. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  167. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  168. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  169. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  170. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  171. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  172. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  173. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  174. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  175. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  176. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  177. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  178. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  179. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  180. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  181. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  182. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  183. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  184. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  185. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  186. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  187. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  188. package/package.json +9 -3
  189. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
  190. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
  191. package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
  192. package/node_modules/@spexcode/session-core/dist/index.js +0 -5
  193. package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
  194. package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
  195. package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
  196. package/node_modules/@spexcode/session-core/dist/message.js +0 -53
  197. package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
  198. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
  199. package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
  200. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
  201. package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
  202. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
  203. package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
  204. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
  205. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
@@ -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.
@@ -18,6 +18,6 @@ The block text is where the declaration ritual is taught, so it is written to be
18
18
 
19
19
  The clean-done eval nudge is advisory only and must never corrupt the Stop hook protocol. Claude-family hooks can receive it as `hookSpecificOutput.additionalContext`; Codex Stop allows are silent because Codex treats unsupported non-block stdout as invalid hook JSON. Blocking decisions stay shared across harnesses through `{"decision":"block"}` plus the dispatcher’s Codex stderr bridge.
20
20
 
21
- Both gates act ONLY on a GOVERNED (dashboard-launched) session: the gate resolves the session's record in the global store from the payload's `session_id`, and on a non-governed (user-self-launched) record — or none — it exits 0 SILENTLY. A self-launched agent has no board to feed, so the declare-demand must never misfire on it. Its own state writes go through `spex session --session <id>`, passing the id explicitly since there is no worktree file to read it from.
21
+ Both gates act ONLY on a GOVERNED (dashboard-launched) session: the gate resolves the session's record in the global store from the payload's `session_id`, and on a non-governed (user-self-launched) record — or none — it exits 0 SILENTLY. A self-launched agent has no board to feed, so the declare-demand must never misfire on it. The forced-continuation fallback writes `asking` through `spex internal session-state --session <id>` — the canonical writer, with no delivery or build side effects — passing the id explicitly since there is no worktree file to read it from. Porcelain declarations remain agent-facing; the hook never invokes them to repair its own stop.
22
22
 
23
23
  It is the enforcement edge of [[core]]: nothing leaves a session except as committed work under a truthful declaration. The freshness it reads is set by [[mark-active]].
@@ -11,28 +11,20 @@
11
11
  # without inventing a completion state.
12
12
  # $SPEX is the PATH-independent CLI invocation (abs tsx + cli) injected by settingsArg, so the gate's own
13
13
  # auto-default AND the command it shows the agent both work even when `spex` is absent from PATH.
14
- # @@@ global store + governed gate - state lives in the per-session GLOBAL record session.json (keyed by the
15
- # harness session_id from the payload, grouped per-project mirrors spec-cli/src/layout.ts). The gate acts
16
- # ONLY on a GOVERNED (dashboard-launched) session: a user-self-launched agent has no board to feed, so an
17
- # undeclared stop is none of our business we exit 0 SILENTLY (the bug this fixes: the declare-demand
18
- # misfiring on a self-launched codex/claude). cwd = the session worktree (resolves the project key + the
19
- # commit-gate's git); state writes go through `$SPEX session … --session <id>` (TS owns the JSON).
14
+ # @@@ governed gate - the session id comes from the payload. The gate acts ONLY on a GOVERNED
15
+ # (dashboard-launched) session: a user-self-launched agent has no board to feed, so an undeclared stop is
16
+ # none of our business. Lifecycle status/proposal come from the canonical session application through one
17
+ # CLI read; this shell never treats runtime.json as a second lifecycle database.
20
18
  . "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
21
19
  S="${SPEX:-spex}"
22
20
  input=$(cat 2>/dev/null || true)
23
21
  sid=$(hp_session_id "$input"); [ -n "$sid" ] || exit 0
24
22
  sdir=$(hp_store_dir "$sid") || exit 0
25
- rec="$sdir/session.json"
26
- # non-governed (or no record) silently let the stop through. THIS is the self-launch fix.
27
- grep -q '^[[:space:]]*"governed"[[:space:]]*:[[:space:]]*true,\?$' "$rec" 2>/dev/null || exit 0
28
-
29
- # read a CLOSED-VOCABULARY field (status / proposal) off its OWN line. The record is one-field-per-line from
30
- # the single writer ([[sessions-core]]), so anchoring to the line start is what keeps a neighbouring note's
31
- # escaped quote from being read as this field's value — the read half of the same rule that stops shell from
32
- # ever composing the record.
33
- jget() { sed -n "s/^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\",\?$/\1/p" "$rec" 2>/dev/null | head -1; }
34
- status=$(jget status)
35
- proposal=$(jget proposal)
23
+ # non-governed (or no record) → silently let the stop through. THIS is the self-launch fix. The CLI response
24
+ # is governed<TAB>status<TAB>proposal; status/proposal are canonical, while governed is identity metadata.
25
+ hook_state=$($S internal session-hook-state --session "$sid" 2>/dev/null) || exit 0
26
+ IFS=$'\t' read -r governed status proposal <<< "$hook_state"
27
+ [ "$governed" = 1 ] || exit 0
36
28
 
37
29
  # the value of the payload's structured `stop_hook_active` field (true on the hook-forced continuation),
38
30
  # read by field name rather than substring-sniffing the JSON blob. ([a-z]* captures true/false portably —
@@ -91,7 +83,10 @@ if [ "${status:-active}" = awaiting ] && { [ "$proposal" = merge ] || [ "$propos
91
83
  exit 0 # work is committed and ahead of main -> the proposal is honest, let it stop.
92
84
  fi
93
85
  if [ "$cont" = true ]; then
94
- $S session ask --session "$sid" --note "stopped with uncommitted work — commit your spec+code on the node branch, then re-declare done" >/dev/null 2>&1 || true
86
+ # The hook is a thin boundary. Do not call the porcelain declaration here: it may trigger
87
+ # delivery/build work and can remain running after the harness has already accepted the stop.
88
+ # The internal writer is the same canonical lifecycle path, with no dispatch side effects.
89
+ $S internal session-state asking --session "$sid" --note "stopped with uncommitted work — commit your spec+code on the node branch, then re-declare done" >/dev/null 2>&1 || true
95
90
  exit 0
96
91
  fi
97
92
  reason=$($S internal hook-prompt stop-gate --variant commit --reason "$gatemsg" --cli "$S" --propose "$proposal") || exit 1
@@ -106,13 +101,15 @@ fi
106
101
  if [ "$cont" = true ]; then
107
102
  # The forced continuation also stopped without declaring. Escape into asking: no default may invent a
108
103
  # completed lane, and the stopped agent now needs a human prompt to choose merge, close, ask, or park.
109
- $S session ask --session "$sid" --note "auto: stopped without declaring — choose merge, close, ask, or park; done --propose nothing records no state" >/dev/null 2>&1 || true
104
+ # Keep this fallback inside the hook/CLI boundary. The porcelain `session ask` command can wait on
105
+ # delivery and workspace builds; a stop hook must settle the canonical state independently of those paths.
106
+ $S internal session-state asking --session "$sid" --note "auto: stopped without declaring — choose merge, close, ask, or park; done --propose nothing records no state" >/dev/null 2>&1 || true
110
107
  exit 0
111
108
  fi
112
109
 
113
110
  # first stop in an undeclared state -> block. The FULL teaching text prints ONCE per session; every later
114
111
  # undeclared stop gets a ONE-LINE version (a heavy session hits this gate 15-20x a night — re-printing the
115
- # full menu each time is pure token noise). The once-sentinel is a plain file beside session.json in the
112
+ # full menu each time is pure token noise). The once-sentinel is a plain file beside runtime.json in the
116
113
  # session's global store dir — the same per-session-sentinel mechanism as the CLI's note-echo-taught; $sdir
117
114
  # is already alias-resolved here, so a codex thread id lands on the same file, and an unwritable dir just
118
115
  # teaches again (never blocks the block). The terse line must stay SELF-EXPLANATORY: an agent whose context
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: merge
3
+ surface: skill, command
4
+ status: active
5
+ hue: 130
6
+ desc: Land this session's completed branch into the repository's source-of-truth branch as one verified no-ff merge, preserve unrelated dirty work, push it, and settle the session honestly. Use when the user says /merge, asks to merge or land this session, or when a supervisor dispatches a merge.
7
+ kind: mutating
8
+ ---
9
+ # merge
10
+
11
+ Land the current SpexCode session's branch; do not dispatch another merge request back to yourself.
12
+
13
+ 1. Inspect the current session, branch, worktree, source-of-truth checkout, and live Git status. Treat every
14
+ pre-existing dirty or untracked path as user-owned. Record its exact status and diff fingerprint; never
15
+ discard it, fold it into your commit, or hide it behind an unverified stash.
16
+ 2. Commit this session's intended spec and code first. In this worktree, merge the latest source-of-truth
17
+ head into the session branch. Resolve conflicts here, then rerun the focused proof, build, `spex spec lint`,
18
+ and `spex eval lint --changed` required by the changed behavior.
19
+ 3. Immediately before landing, verify
20
+ `git merge-base --is-ancestor <source-head> <session-head>`. If it fails, sync again. A clean textual merge
21
+ is not product proof.
22
+ 4. In the source-of-truth checkout, make one `--no-ff` merge of the already-synced session tip. Do not resolve
23
+ conflicts there. If unrelated dirty work prevents the merge, preserve it byte-for-byte and report the exact
24
+ overlap rather than forcing, resetting, or committing it.
25
+ 5. Verify the source checkout has no `MERGE_HEAD`, the session tip is its ancestor, unrelated dirty
26
+ fingerprints are unchanged, and the post-merge gates pass. Push the source-of-truth branch only after
27
+ those checks.
28
+ 6. Stop test-owned processes and publish any evidence a human must inspect. If the work is fully landed and
29
+ no decision remains, run `spex session done --propose close` as the final action. Never close your own
30
+ session directly. If a real decision or external wake-up remains, declare the truthful alternative instead.
31
+
32
+ `spex session merge <SEL>` is the supervisor-facing dispatcher that sends this workflow to another session.
33
+ Inside the target session, execute the workflow above; do not call `spex session merge .` recursively.
@@ -11,9 +11,5 @@ where an agent invokes them on demand, each carrying `surface: skill`. Grouping
11
11
  legible at a glance — the skill plugins on this shelf, the command presets on [[commands]], the auxiliary
12
12
  system contracts on [[prompts]], with [[core]] a flat child beside them.
13
13
 
14
- This node is a **shelf, not a surface** (the [[prompts]] shape): it declares no `surface` field and
15
- gathers nothing itself. Discovery is recursive and field-driven (surface), so a resident plugs in
16
- exactly as it would at the root. A plugin that serves BOTH surfaces — [[distill]] is skill and command —
17
- shelves here by its primary (skill) identity and still gathers as a command through its field. The init
18
- templates mirror this layout. The shelf stays pure presentation: moving a resident beneath it changes
19
- neither that plugin's identity nor the surfaces gathered from its frontmatter.
14
+ This node is a **shelf, not a surface**. Its routing and relocation invariant is owned once by
15
+ [[.plugins]]'s shelf invariant; this node only describes skill-specific materialization and invocation.
@@ -21,3 +21,10 @@ materialized into the agent's contract.
21
21
  Which plugins `spex init` ships is the init-preset rule. `seed: false` excludes a plugin subtree; shared
22
22
  plugins have one body and one helper set — there is no separately authored adopter variant. Dogfood eval
23
23
  scenarios/readings remain with the implementation and git history they measure.
24
+
25
+ ## shelf invariant
26
+
27
+ Grouping shelves (`[[commands]]`, `[[skills]]`, `[[review]]`, and `[[prompts]]`) are presentation parents, not
28
+ surfaces. Discovery is recursive and field-driven, so moving a resident under a shelf changes neither its
29
+ identity nor the surfaces gathered from its frontmatter. Each shelf therefore states only its surface-specific
30
+ purpose; it must not copy this routing invariant.
@@ -9,8 +9,8 @@ export type Unit = {
9
9
  export type Extractor = {
10
10
  id: string;
11
11
  claims(ext: string): boolean;
12
- ready(): true | string;
13
- extract(content: string, filename: string): Unit[];
12
+ ready(): true | string | Promise<true | string>;
13
+ extract(content: string, filename: string): Unit[] | Promise<Unit[]>;
14
14
  memoKey: (filename: string) => string;
15
15
  };
16
16
  export type CodeEntry = {
@@ -56,6 +56,16 @@ export type LangSpec = {
56
56
  export declare function heuristicExtractor(spec: LangSpec): Extractor;
57
57
  export declare const JS_LANG_R5B: LangSpec;
58
58
  export declare const PYTHON_LANG: LangSpec;
59
+ type TreeSitterNode = any;
60
+ export type TreeSitterLanguageRow = {
61
+ id: string;
62
+ extensions: string[];
63
+ grammar: string;
64
+ schema: string;
65
+ units(root: TreeSitterNode): Unit[];
66
+ };
67
+ export declare const TREE_SITTER_ROWS: TreeSitterLanguageRow[];
68
+ export declare function treeSitterExtractor(row: TreeSitterLanguageRow): Extractor;
59
69
  export declare function extractors(root: string): Extractor[];
60
70
  export declare function extractorFor(regs: Extractor[], ext: string): Extractor | null;
61
71
  export declare const extOf: (path: string) => string;
@@ -92,3 +102,4 @@ export type AnchorHitQuery = {
92
102
  export declare function anchorHitQueries(root: string, queries: AnchorHitQuery[], regs: Extractor[]): Promise<AnchorHit[][]>;
93
103
  export declare function anchorHitExists(root: string, queries: AnchorHitQuery[], regs: Extractor[]): Promise<boolean[]>;
94
104
  export declare function anchorHitCommits(root: string, win: DriftPathEvent[], symbols: string[], regs: Extractor[]): Promise<AnchorHit[]>;
105
+ export {};