@spexcode/spec-cli 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/bin/spex.mjs +76 -0
- package/dist/attach.d.ts +2 -0
- package/dist/attach.js +48 -0
- package/dist/claude-headless.d.ts +33 -0
- package/dist/claude-headless.js +307 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1859 -0
- package/dist/client.d.ts +111 -0
- package/dist/client.js +476 -0
- package/dist/cockpit.d.ts +7 -0
- package/dist/cockpit.js +26 -0
- package/dist/codex-headless.d.ts +1 -0
- package/dist/codex-headless.js +7 -0
- package/dist/codex-runtime-generations.d.ts +53 -0
- package/dist/codex-runtime-generations.js +686 -0
- package/dist/commit-surgery.d.ts +1 -0
- package/dist/commit-surgery.js +90 -0
- package/dist/contract-filter.d.ts +14 -0
- package/dist/contract-filter.js +219 -0
- package/dist/dashboard-assets.d.ts +6 -0
- package/dist/dashboard-assets.js +50 -0
- package/dist/delivery-queue.d.ts +23 -0
- package/dist/delivery-queue.js +179 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +547 -0
- package/dist/eval-host.d.ts +1 -0
- package/dist/eval-host.js +19 -0
- package/dist/execution-trace.d.ts +26 -0
- package/dist/execution-trace.js +475 -0
- package/dist/file-write.d.ts +2 -0
- package/dist/file-write.js +26 -0
- package/dist/flat.d.ts +89 -0
- package/dist/flat.js +1076 -0
- package/dist/gateway-auth.d.ts +56 -0
- package/dist/gateway-auth.js +168 -0
- package/dist/gateway-hub.d.ts +28 -0
- package/dist/gateway-hub.js +361 -0
- package/dist/gateway.d.ts +37 -0
- package/dist/gateway.js +503 -0
- package/dist/graphCache.d.ts +27 -0
- package/dist/graphCache.js +716 -0
- package/dist/graphSnapshot.d.ts +14 -0
- package/dist/graphSnapshot.js +35 -0
- package/dist/graphStream.d.ts +85 -0
- package/dist/graphStream.js +1195 -0
- package/dist/guidance-catalog.d.ts +64 -0
- package/dist/guidance-catalog.js +166 -0
- package/dist/guide.d.ts +8 -0
- package/dist/guide.js +684 -0
- package/dist/harness-select.d.ts +18 -0
- package/dist/harness-select.js +62 -0
- package/dist/harness.d.ts +325 -0
- package/dist/harness.js +3026 -0
- package/dist/headless-controller.d.ts +9 -0
- package/dist/headless-controller.js +47 -0
- package/dist/help.d.ts +10 -0
- package/dist/help.js +574 -0
- package/dist/hook-prompts.d.ts +18 -0
- package/dist/hook-prompts.js +122 -0
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.js +27 -0
- package/dist/host-resources.d.ts +97 -0
- package/dist/host-resources.js +795 -0
- package/dist/host.d.ts +88 -0
- package/dist/host.js +663 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +899 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +284 -0
- package/dist/issues-cli.d.ts +6 -0
- package/dist/issues-cli.js +334 -0
- package/dist/issues.d.ts +69 -0
- package/dist/issues.js +189 -0
- package/dist/lint.d.ts +34 -0
- package/dist/lint.js +476 -0
- package/dist/listen.d.ts +7 -0
- package/dist/listen.js +24 -0
- package/dist/localIssues.d.ts +65 -0
- package/dist/localIssues.js +582 -0
- package/dist/login-page.d.ts +5 -0
- package/dist/login-page.js +86 -0
- package/dist/loop-in.d.ts +23 -0
- package/dist/loop-in.js +68 -0
- package/dist/machine-peer.d.ts +69 -0
- package/dist/machine-peer.js +603 -0
- package/dist/materialize.d.ts +18 -0
- package/dist/materialize.js +468 -0
- package/dist/mentions.d.ts +50 -0
- package/dist/mentions.js +117 -0
- package/dist/opencode-headless.d.ts +4 -0
- package/dist/opencode-headless.js +208 -0
- package/dist/opencode.d.ts +3 -0
- package/dist/opencode.js +142 -0
- package/dist/pi-harness.d.ts +3 -0
- package/dist/pi-harness.js +129 -0
- package/dist/pi-headless.d.ts +25 -0
- package/dist/pi-headless.js +196 -0
- package/dist/plugin-harness.d.ts +21 -0
- package/dist/plugin-harness.js +146 -0
- package/dist/pty-bridge.d.ts +10 -0
- package/dist/pty-bridge.js +404 -0
- package/dist/pty-helper.d.mts +1 -0
- package/dist/pty-helper.mjs +101 -0
- package/dist/pty-native-helper.d.mts +2 -0
- package/dist/pty-native-helper.mjs +23 -0
- package/dist/public-graph.d.ts +46 -0
- package/dist/public-graph.js +56 -0
- package/dist/ranker.d.ts +15 -0
- package/dist/ranker.js +188 -0
- package/dist/reaper.d.ts +6 -0
- package/dist/reaper.js +81 -0
- package/dist/reviews.d.ts +189 -0
- package/dist/reviews.js +350 -0
- package/dist/runtime-guard.d.ts +6 -0
- package/dist/runtime-guard.js +37 -0
- package/dist/runtime-ownership.d.ts +21 -0
- package/dist/runtime-ownership.js +84 -0
- package/dist/search.bench.d.mts +1 -0
- package/dist/search.bench.mjs +75 -0
- package/dist/search.d.ts +20 -0
- package/dist/search.js +61 -0
- package/dist/session-cursors.d.ts +14 -0
- package/dist/session-cursors.js +82 -0
- package/dist/session-declarations.d.ts +13 -0
- package/dist/session-declarations.js +126 -0
- package/dist/session-execution.d.ts +10 -0
- package/dist/session-execution.js +70 -0
- package/dist/session-files.d.ts +28 -0
- package/dist/session-files.js +130 -0
- package/dist/session-follow.d.ts +31 -0
- package/dist/session-follow.js +179 -0
- package/dist/session-reparent.d.ts +10 -0
- package/dist/session-reparent.js +33 -0
- package/dist/session-timeline.d.ts +51 -0
- package/dist/session-timeline.js +233 -0
- package/dist/session-web.d.ts +24 -0
- package/dist/session-web.js +141 -0
- package/dist/sessions.d.ts +412 -0
- package/dist/sessions.js +4239 -0
- package/dist/sh.d.ts +1 -0
- package/dist/sh.js +3 -0
- package/dist/shim-runtime.d.ts +1 -0
- package/dist/shim-runtime.js +161 -0
- package/dist/slash-commands.d.ts +9 -0
- package/dist/slash-commands.js +307 -0
- package/dist/source-files.d.ts +9 -0
- package/dist/source-files.js +113 -0
- package/dist/supervise.d.ts +1 -0
- package/dist/supervise.js +308 -0
- package/dist/tree.d.ts +25 -0
- package/dist/tree.js +98 -0
- package/dist/tsx-bin.d.ts +3 -0
- package/dist/tsx-bin.js +41 -0
- package/dist/uninstall.d.ts +3 -0
- package/dist/uninstall.js +177 -0
- package/dist/uploads.d.ts +36 -0
- package/dist/uploads.js +243 -0
- package/dist/worktree-sources.d.ts +1 -0
- package/dist/worktree-sources.js +47 -0
- package/hooks/compat/mark-active-0.5.2-eef1.fixture +53 -0
- package/hooks/compat/mark-active-sed-v0.fixture +46 -0
- package/hooks/dispatch.sh +105 -0
- package/hooks/harness.sh +272 -0
- package/package.json +47 -0
- package/templates/hooks/commit-msg +14 -0
- package/templates/hooks/post-checkout +25 -0
- package/templates/hooks/post-merge +34 -0
- package/templates/hooks/pre-commit +120 -0
- package/templates/hooks/prepare-commit-msg +35 -0
- package/templates/hooks/reference-transaction +105 -0
- package/templates/spec/project/.plugins/commands/extract/spec.md +66 -0
- package/templates/spec/project/.plugins/commands/regroup/spec.md +25 -0
- package/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
- package/templates/spec/project/.plugins/commands/spec.md +30 -0
- package/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/templates/spec/project/.plugins/commands/tidy/spec.md +43 -0
- package/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
- package/templates/spec/project/.plugins/core/idle/idle.sh +15 -0
- package/templates/spec/project/.plugins/core/idle/spec.md +13 -0
- package/templates/spec/project/.plugins/core/mark-active/mark-active.sh +59 -0
- package/templates/spec/project/.plugins/core/mark-active/spec.md +22 -0
- package/templates/spec/project/.plugins/core/session-fail/fail.sh +12 -0
- package/templates/spec/project/.plugins/core/session-fail/spec.md +15 -0
- package/templates/spec/project/.plugins/core/spec-first/spec-first.sh +43 -0
- package/templates/spec/project/.plugins/core/spec-first/spec.md +17 -0
- package/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +66 -0
- package/templates/spec/project/.plugins/core/spec-of-file/spec.md +15 -0
- package/templates/spec/project/.plugins/core/spec.md +22 -0
- package/templates/spec/project/.plugins/core/stop-gate/spec.md +23 -0
- package/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +149 -0
- package/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
- package/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
- package/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
- package/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
- package/templates/spec/project/.plugins/prompts/spec.md +24 -0
- package/templates/spec/project/.plugins/skills/distill/digest.mjs +136 -0
- package/templates/spec/project/.plugins/skills/distill/spec.md +67 -0
- package/templates/spec/project/.plugins/skills/spec.md +19 -0
- package/templates/spec/project/.plugins/spec.md +23 -0
- package/templates/spec/project/spec.md +20 -0
package/hooks/harness.sh
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @@@ harness.sh - the SHELL face of the [[harness-adapter]] (spec-cli/src/harness.ts). The hook scripts run
|
|
3
|
+
# as pure shell, so they cannot import the TS adapter; this sourced library is its mirror. dispatch.sh sources
|
|
4
|
+
# it and exports SPEXCODE_HARNESS (claude|codex|opencode|zcode) — baked into the shim by each adapter, so a hook learns its
|
|
5
|
+
# harness deterministically, never by sniffing the payload shape. EVERY harness-divergent payload-parse lives
|
|
6
|
+
# HERE; the hook scripts stay harness-agnostic and just call hp_* (the one place Claude's tool names appear in
|
|
7
|
+
# shell, plus codex's Bash-command mapping). The session-id + global-store resolution is harness-agnostic and
|
|
8
|
+
# lives here too, so the six hooks no longer each repeat the git-common-dir → project-key dance.
|
|
9
|
+
# SPEXCODE_HARNESS is claude|codex|opencode|pi|zcode|plugin: `plugin` is the bundle form ([[plugin-harness]]) and its
|
|
10
|
+
# host (adopter-a/Claude) shares Claude's payload shape; `opencode`'s generated plugin ([[opencode-harness]]),
|
|
11
|
+
# `pi`'s generated extension ([[pi-harness]]), and z-code's native hook payload all share that Claude shape — so every
|
|
12
|
+
# `case "$SPEXCODE_HARNESS"` below routes all four through the claude branch via the default case; only codex
|
|
13
|
+
# keeps parse arms of its own.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# the string value of a top-level JSON string field (first match). Harness-agnostic — both harnesses' payloads
|
|
17
|
+
# carry session_id / tool_name as plain string fields. $1 = payload, $2 = field name. The value is scanned as a
|
|
18
|
+
# real JSON string: the closing quote is the first UNESCAPED `"`, so a value containing `\"` (a quoted literal
|
|
19
|
+
# inside a codex Bash/apply_patch command — extremely common) is captured WHOLE, not truncated at the inner
|
|
20
|
+
# quote. The standard escapes are decoded (`\"` `\\` `\/` `\n` `\t` `\r` `\b` `\f`), so a patch envelope's `\n`
|
|
21
|
+
# arrives as a real newline here (the downstream codex decode then no-ops). Pure awk, no jq on the hot path.
|
|
22
|
+
hp_field() {
|
|
23
|
+
printf '%s' "$1" | awk -v field="$2" '
|
|
24
|
+
BEGIN { s = ""; while ((getline line) > 0) s = s (s == "" ? "" : "\n") line }
|
|
25
|
+
END {
|
|
26
|
+
key = "\"" field "\""
|
|
27
|
+
n = length(s); i = 1
|
|
28
|
+
while (i <= n) {
|
|
29
|
+
p = index(substr(s, i), key)
|
|
30
|
+
if (p == 0) exit
|
|
31
|
+
i += p + length(key) - 1
|
|
32
|
+
# skip whitespace, require a colon, skip whitespace, require an opening quote
|
|
33
|
+
while (i <= n && substr(s, i, 1) ~ /[ \t\n]/) i++
|
|
34
|
+
if (substr(s, i, 1) != ":") continue
|
|
35
|
+
i++
|
|
36
|
+
while (i <= n && substr(s, i, 1) ~ /[ \t\n]/) i++
|
|
37
|
+
if (substr(s, i, 1) != "\"") continue
|
|
38
|
+
i++
|
|
39
|
+
out = ""
|
|
40
|
+
while (i <= n) {
|
|
41
|
+
c = substr(s, i, 1)
|
|
42
|
+
if (c == "\\") {
|
|
43
|
+
e = substr(s, i + 1, 1)
|
|
44
|
+
if (e == "n") out = out "\n"
|
|
45
|
+
else if (e == "t") out = out "\t"
|
|
46
|
+
else if (e == "r") out = out "\r"
|
|
47
|
+
else if (e == "b") out = out "\b"
|
|
48
|
+
else if (e == "f") out = out "\f"
|
|
49
|
+
else out = out e # \" \\ \/ and any other → the literal char
|
|
50
|
+
i += 2
|
|
51
|
+
} else if (c == "\"") {
|
|
52
|
+
print out; exit
|
|
53
|
+
} else {
|
|
54
|
+
out = out c; i++
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exit
|
|
58
|
+
}
|
|
59
|
+
}'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# the session id from a payload (both harnesses use session_id).
|
|
63
|
+
# Codex hooks run inside ONE shared per-project app-server. That process can inherit the FIRST launched
|
|
64
|
+
# session's SPEXCODE_SESSION_ID, so on codex the payload session_id (the acting thread id) must win and then
|
|
65
|
+
# hp_store_dir aliases it to the governed record. Claude's payload id equals its governed record id, so the
|
|
66
|
+
# PAYLOAD is the acting identity; the inherited env is only a fallback for payload-less events. Env-first was
|
|
67
|
+
# a live bug: a nested subagent (Task tool) inherits the parent's SPEXCODE_SESSION_ID, so with env winning,
|
|
68
|
+
# every child tool call fired mark-active against the PARENT's record — the parent read `working` forever and
|
|
69
|
+
# every park/done declaration was clobbered within seconds (measured). Same staleness class the codex branch
|
|
70
|
+
# already guards against (payload-first below); claude now follows the same rule.
|
|
71
|
+
# The claude preference is RESOLUTION-AWARE, not blind. "Claude's payload id equals its governed record id"
|
|
72
|
+
# holds only while the launched conversation lives: a compaction/continuation MINTS A NEW conversation id
|
|
73
|
+
# while the record keeps the launched one, and a blindly-preferred payload id then resolves to no record at
|
|
74
|
+
# all — hp_store_dir echoes a path nothing sits at, so every record-dependent hook (stop-gate, mark-active,
|
|
75
|
+
# idle, session-fail) silently no-ops and the session reads `working` forever (measured: a live session froze
|
|
76
|
+
# at `active` while its terminal still answered). So the payload wins only when a record actually answers to
|
|
77
|
+
# it, and an unresolvable payload falls back to the launched SPEXCODE_SESSION_ID. This cannot reopen the
|
|
78
|
+
# subagent bug above: a Task subagent's payload carries the PARENT's session_id, which DOES resolve, so the
|
|
79
|
+
# preference is unchanged there — the divergent branch is reached only when the two ids differ AND the
|
|
80
|
+
# payload names nothing. The common path costs one string compare (the ids are equal), never a store read.
|
|
81
|
+
hp_session_id() {
|
|
82
|
+
local pid
|
|
83
|
+
case "$SPEXCODE_HARNESS" in
|
|
84
|
+
codex) hp_field "$1" session_id ;;
|
|
85
|
+
*) pid=$(hp_field "$1" session_id)
|
|
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
|
|
89
|
+
printf '%s' "$SPEXCODE_SESSION_ID"
|
|
90
|
+
else
|
|
91
|
+
printf '%s' "${pid:-$SPEXCODE_SESSION_ID}"
|
|
92
|
+
fi ;;
|
|
93
|
+
esac
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# is THIS payload a tool call executed by an IN-PROCESS SUBAGENT (Claude's Task tool — a sub-conversation
|
|
97
|
+
# inside the SAME harness process) rather than the session's own top-level agent? The subagent's payload
|
|
98
|
+
# carries the PARENT's session_id/transcript_path (measured live, claude 2.1.207), so hp_session_id cannot
|
|
99
|
+
# tell them apart — but the harness stamps every subagent-executed call with a top-level `agent_id`
|
|
100
|
+
# (+ `agent_type`) field that a top-level call NEVER carries. The check is structural, never a heuristic:
|
|
101
|
+
# scan ONLY the payload prefix before the first `"tool_input"` (every harness-stamped top-level field
|
|
102
|
+
# precedes tool_input), and require the key shape `"agent_id":`. Inside a JSON string value every quote is
|
|
103
|
+
# escaped (\"), so a bare-quoted `"agent_id"` followed by a colon in that prefix can only be the harness's
|
|
104
|
+
# own stamp — a tool parameter literally NAMED agent_id (an MCP tool) sits inside tool_input, past the
|
|
105
|
+
# truncation, and file/prompt content can never fake an unescaped quote. Codex payloads carry no such field
|
|
106
|
+
# (its verified field set) so this never matches there. Echoes "1" when yes, else nothing.
|
|
107
|
+
hp_is_subagent() {
|
|
108
|
+
printf '%s' "${1%%\"tool_input\"*}" | grep -q '"agent_id"[[:space:]]*:' && printf 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# the per-PROJECT GLOBAL runtime dir (mirrors packages/spec-core/src/layout.ts `runtimeRoot`): <store>/projects/<enc>,
|
|
112
|
+
# keyed by the project (dirname of the ABSOLUTE git-common-dir, so the answer is identical from main or any
|
|
113
|
+
# worktree). The per-session dirs and the per-tree materialize slots (hp_tree_dir) live under it.
|
|
114
|
+
# Echoes the dir; returns non-zero (echoing nothing) when git can't resolve, so a caller can `|| exit 0`.
|
|
115
|
+
hp_runtime_dir() {
|
|
116
|
+
local gcd
|
|
117
|
+
gcd=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || gcd=$(realpath "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null)
|
|
118
|
+
[ -n "$gcd" ] || return 1
|
|
119
|
+
printf '%s/projects/%s' "${SPEXCODE_HOME:-$HOME/.spexcode}" "$(printf '%s' "$(dirname "$gcd")" | sed 's#[/.]#-#g')"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# the per-WORKTREE materialize slot (mirrors layout.ts `treeSlotDir`): <runtime>/trees/<enc(worktree-toplevel)> —
|
|
123
|
+
# where THIS tree's materialized hook manifest + content-hash marker live. Keyed by the cwd's own
|
|
124
|
+
# `rev-parse --show-toplevel` through the same enc transform, so a dispatch can only ever read the manifest
|
|
125
|
+
# of the tree it fires in ([[hook-dispatch]] — the old single global file let the last-materialized tree's
|
|
126
|
+
# hook set reach every other tree's sessions). Echoes the dir; returns non-zero when git can't resolve.
|
|
127
|
+
hp_tree_dir() {
|
|
128
|
+
local rd top
|
|
129
|
+
rd=$(hp_runtime_dir) || return 1
|
|
130
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
131
|
+
[ -n "$top" ] || return 1
|
|
132
|
+
printf '%s/trees/%s' "$rd" "$(printf '%s' "$top" | sed 's#[/.]#-#g')"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# the per-session GLOBAL store dir for a session id — <runtime>/sessions/<id> (sibling of the per-project
|
|
136
|
+
# runtime above). Echoes the dir; returns non-zero (echoing nothing) when git can't resolve.
|
|
137
|
+
# ALIAS resolution: a codex hook fires from the shared per-PROJECT app-server process, whose env may carry a
|
|
138
|
+
# stale SPEXCODE_SESSION_ID from the first launched codex session. hp_session_id therefore returns the acting
|
|
139
|
+
# payload session_id on codex: the codex THREAD id, NOT the SpexCode record id the dir is keyed by. So when no
|
|
140
|
+
# record sits at <id> directly, find the one record that captured this id as `harness_session_id` (the backend
|
|
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
|
|
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
|
+
# unchanged, so the caller's `[ -e "$rec" ]` still no-ops gracefully. Mirrors layout.ts `readAliasedRawRecord`.
|
|
145
|
+
hp_store_dir() {
|
|
146
|
+
local rd; rd=$(hp_runtime_dir) || return 1
|
|
147
|
+
local direct="$rd/sessions/$1"
|
|
148
|
+
if [ -e "$direct/session.json" ]; then printf '%s' "$direct"; return 0; fi
|
|
149
|
+
# same two-halves-of-absence rule as layout.ts's readAliasedRecordEntry: an id owning a store dir is already
|
|
150
|
+
# one of ours, so its emptiness is settled and the alias grep is the wrong question (and a whole-store scan).
|
|
151
|
+
if [ -d "$direct" ]; then printf '%s' "$direct"; return 0; fi
|
|
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; }
|
|
155
|
+
printf '%s' "$direct"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# the TOOLCHAIN's own version fingerprint — the toolchain side of the content key. The materialized artifacts
|
|
159
|
+
# are a function of (config content, toolchain), so a TOOLCHAIN update must move the key too, or an updated
|
|
160
|
+
# deploy never self-heals its stale contract/shims/manifest until someone happens to edit .plugins (the field
|
|
161
|
+
# lesson: a toolchain update does NOT self-heal). A source checkout answers with the git TREE hash of the
|
|
162
|
+
# package dir (moves exactly when the toolchain's content moves, not on every repo commit); an npm install
|
|
163
|
+
# (no .git) answers with the package.json hash (npm bumps the version). env-stripped git — a git hook's
|
|
164
|
+
# exported GIT_DIR must not misdirect repo discovery (same rule as git.ts's git()).
|
|
165
|
+
SPEXCODE_HP_PKG="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)"
|
|
166
|
+
hp_toolchain_version() {
|
|
167
|
+
( cd "$SPEXCODE_HP_PKG" 2>/dev/null && env -u GIT_DIR -u GIT_INDEX_FILE git rev-parse 'HEAD:./' 2>/dev/null ) \
|
|
168
|
+
|| sha256sum "$SPEXCODE_HP_PKG/package.json" 2>/dev/null | cut -d' ' -f1 \
|
|
169
|
+
|| echo unversioned
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# the deterministic content fingerprint of EVERYTHING the materialize is a function of: the EDITABLE config
|
|
173
|
+
# roots (.plugins + plugin-system md/sh), the PERSISTED POLICY files (the MAIN checkout's spexcode.json +
|
|
174
|
+
# spexcode.local.json — the `harnesses` set materialize reads via readConfig(mainCheckout)), and the
|
|
175
|
+
# toolchain version above. Since the dispatch-gate retired ([[commit-surgery]] — materialize anchors on
|
|
176
|
+
# git-native events only), this is a FRESHNESS STAMP materialize records after each pass, a diagnostic
|
|
177
|
+
# (is the last materialize current?) rather than a trigger. Run with cwd = the project. ONE definition:
|
|
178
|
+
# materialize.ts shells to it. env-stripped git, same rule as hp_toolchain_version.
|
|
179
|
+
hp_config_hash() {
|
|
180
|
+
local gcd
|
|
181
|
+
gcd=$(env -u GIT_DIR -u GIT_INDEX_FILE git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) \
|
|
182
|
+
|| gcd=$(realpath "$(env -u GIT_DIR -u GIT_INDEX_FILE git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null)
|
|
183
|
+
{ hp_toolchain_version
|
|
184
|
+
find .spec/*/.plugins .spec/*/plugin-system \( -name '*.md' -o -name '*.sh' \) -type f -print0 2>/dev/null \
|
|
185
|
+
| sort -z | xargs -0 cat 2>/dev/null
|
|
186
|
+
[ -n "$gcd" ] && cat "$(dirname "$gcd")/spexcode.json" "$(dirname "$gcd")/spexcode.local.json" 2>/dev/null
|
|
187
|
+
} | sha256sum | cut -d' ' -f1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# the tool a payload is about to run / just ran (harness-agnostic field name).
|
|
191
|
+
hp_tool() { hp_field "$1" tool_name; }
|
|
192
|
+
|
|
193
|
+
# is THIS payload the agent pausing to ask the HUMAN a question? Claude: the AskUserQuestion tool. Codex: the
|
|
194
|
+
# experimental request_user_input tool (its only structured ask path). Echoes "1" when yes, else nothing.
|
|
195
|
+
hp_is_ask() {
|
|
196
|
+
case "$SPEXCODE_HARNESS" in
|
|
197
|
+
codex) [ "$(hp_tool "$1")" = request_user_input ] && printf 1 ;;
|
|
198
|
+
*) [ "$(hp_tool "$1")" = AskUserQuestion ] && printf 1 ;;
|
|
199
|
+
esac
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# the question text of an ask payload (best-effort; for the board note). Both harnesses carry it under a
|
|
203
|
+
# "question" field of the tool input — so it is just the first such JSON string value: collapse onto hp_field,
|
|
204
|
+
# which (unlike the old grep `[^"]*`) handles an embedded `\"` and decodes escapes instead of truncating.
|
|
205
|
+
hp_ask_note() { hp_field "$1" question; }
|
|
206
|
+
|
|
207
|
+
# the CODE file(s) a payload touches, mapped to the trigger the spec hooks key on. $2 = mode:
|
|
208
|
+
# read → the file being READ ([[spec-first]] gates the first GOVERNED read)
|
|
209
|
+
# mutate → the file being EDITED ([[spec-of-file]] fires only on a mutation)
|
|
210
|
+
# access → either operation
|
|
211
|
+
# Echoes the path(s), ONE PER LINE — a codex multi-file apply_patch (several `*** Update File:` markers)
|
|
212
|
+
# touches several files in one tool call, so every consuming hook iterates the lines. Echoes nothing when the
|
|
213
|
+
# payload is not a code touch of that mode. The harness divergence:
|
|
214
|
+
# Claude — Read/Edit/Write/NotebookEdit + tool_input.file_path|notebook_path.
|
|
215
|
+
# Codex — NO file_path. An EDIT is its own first-class tool `tool_name:"apply_patch"` whose tool_input.command
|
|
216
|
+
# is the bare patch envelope (`*** Update File: <path>`); a READ/shell is `tool_name:"Bash"` +
|
|
217
|
+
# tool_input.command. Both carry the touched file inside `command`, so we parse that one field.
|
|
218
|
+
hp_code_path() {
|
|
219
|
+
local payload="$1" mode="$2" tool
|
|
220
|
+
tool=$(hp_tool "$payload")
|
|
221
|
+
case "$SPEXCODE_HARNESS" in
|
|
222
|
+
codex)
|
|
223
|
+
case "$mode:$tool" in
|
|
224
|
+
read:Bash|mutate:apply_patch|mutate:Bash|access:apply_patch|access:Bash) ;;
|
|
225
|
+
*) return 0 ;;
|
|
226
|
+
esac
|
|
227
|
+
_hp_codex_cmd_path "$(hp_field "$payload" command)" "$mode"
|
|
228
|
+
;;
|
|
229
|
+
*)
|
|
230
|
+
case "$mode" in
|
|
231
|
+
read) case "$tool" in Read) ;; *) return 0 ;; esac ;;
|
|
232
|
+
mutate) case "$tool" in Edit|Write|NotebookEdit) ;; *) return 0 ;; esac ;;
|
|
233
|
+
access) case "$tool" in Read|Edit|Write|NotebookEdit) ;; *) return 0 ;; esac ;;
|
|
234
|
+
*) return 0 ;;
|
|
235
|
+
esac
|
|
236
|
+
local p; p=$(hp_field "$payload" file_path); [ -n "$p" ] || p=$(hp_field "$payload" notebook_path)
|
|
237
|
+
printf '%s' "$p"
|
|
238
|
+
;;
|
|
239
|
+
esac
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# @@@ codex command → path - the apply_patch / sed / cat path-extractor. Codex never sends a file_path; the
|
|
243
|
+
# touched file lives inside `command`. Two shapes: a PATCH envelope carries `*** (Add|Update|Delete) File:
|
|
244
|
+
# <path>` lines (always a MUTATION) — this is what the apply_patch tool sends, the bare envelope with NO literal
|
|
245
|
+
# `apply_patch` token, so we detect it by the File: markers themselves (the legacy `apply_patch` token is kept
|
|
246
|
+
# too, for a shell-wrapped invocation); a plain command (sed/cat/head/rg/…) carries the path as a token.
|
|
247
|
+
# A MUTATION is a patch envelope or a write shape (a redirect, tee, `sed -i`, dd of=); in `mutate` mode a pure
|
|
248
|
+
# read yields nothing. A patch envelope can carry SEVERAL `*** Update File:` markers (a multi-file edit) — ALL
|
|
249
|
+
# of them are emitted, one per line, so the consuming hook acts on every touched file (a single-file patch
|
|
250
|
+
# emits one line; a plain command emits its one token). The plain-command path is the last path-like token
|
|
251
|
+
# (has a / or a .ext), ignoring flags — matches the verified-facts example `sed -n 1p f.ts` → `f.ts`.
|
|
252
|
+
# Best-effort: an exotic command may not resolve, which only means a missed nudge, never a wrong block.
|
|
253
|
+
_hp_codex_cmd_path() {
|
|
254
|
+
local mode="$2" cmd
|
|
255
|
+
# the command arrives as a JSON STRING value; hp_field already decodes JSON escapes (so a patch File: line
|
|
256
|
+
# already ends at a real newline). This gsub is a no-op safety net for any caller that bypassed hp_field.
|
|
257
|
+
cmd=$(printf '%s' "$1" | awk '{gsub(/\\n/,"\n"); gsub(/\\t/,"\t")}1')
|
|
258
|
+
case "$cmd" in
|
|
259
|
+
*apply_patch*|*applypatch*|*'*** Add File:'*|*'*** Update File:'*|*'*** Delete File:'*)
|
|
260
|
+
[ "$mode" = read ] && return 0
|
|
261
|
+
printf '%s\n' "$cmd" | sed -n 's/^\*\*\* \(Add\|Update\|Delete\) File: \(.*\)$/\2/p' | sed 's/[[:space:]]*$//'
|
|
262
|
+
return 0 ;;
|
|
263
|
+
esac
|
|
264
|
+
local is_mutate=0
|
|
265
|
+
case "$cmd" in *' >> '*|*' > '*|*' >>'*|*' >'*|*' tee '*|*'sed -i'*|*' dd '*) is_mutate=1 ;; esac
|
|
266
|
+
[ "$mode" = mutate ] && [ "$is_mutate" = 0 ] && return 0
|
|
267
|
+
[ "$mode" = read ] && [ "$is_mutate" = 1 ] && return 0
|
|
268
|
+
printf '%s\n' "$cmd" | tr ' \t' '\n\n' | grep -E '^[^-].*[/.][A-Za-z0-9_]+' | grep -vE '^(apply_patch|applypatch)$' | tail -1
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# the notification type of a Notification payload (Claude only — Codex fires no Notification event).
|
|
272
|
+
hp_notification_type() { hp_field "$1" notification_type; }
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spexcode/spec-cli",
|
|
3
|
+
"version": "0.6.5",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"spex": "bin/spex.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js",
|
|
11
|
+
"./bin/spex.mjs": "./bin/spex.mjs",
|
|
12
|
+
"./ranker": "./dist/ranker.js",
|
|
13
|
+
"./eval-host": "./dist/eval-host.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"dist",
|
|
19
|
+
"templates",
|
|
20
|
+
"hooks"
|
|
21
|
+
],
|
|
22
|
+
"bundledDependencies": [
|
|
23
|
+
"@spexcode/spec-core",
|
|
24
|
+
"@spexcode/spec-eval",
|
|
25
|
+
"@spexcode/spec-forge"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node ../scripts/build-dist.mjs",
|
|
32
|
+
"prepublishOnly": "node ../scripts/release-publish.mjs --from-package-publish",
|
|
33
|
+
"serve": "npm run build && node bin/spex.mjs serve",
|
|
34
|
+
"lint": "npm run build && node bin/spex.mjs spec lint",
|
|
35
|
+
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@spexcode/spec-core": "0.6.5",
|
|
39
|
+
"@spexcode/spec-eval": "0.6.5",
|
|
40
|
+
"@spexcode/spec-forge": "0.6.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.16.0",
|
|
44
|
+
"tsx": "^4.19.2",
|
|
45
|
+
"typescript": "^5.6.3"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spexcode-managed-hook-v1
|
|
3
|
+
# Candidate lint is driven solely by reference-transaction. This hook intentionally carries no marker,
|
|
4
|
+
# message hash, tree binding, or TTL; Git's immutable commit object is the only candidate identity.
|
|
5
|
+
# Git does not apply scissors cleanup to every no-editor/-F path, so normalize the final message here,
|
|
6
|
+
# after every editor, using scissors only when the repository explicitly selected that cleanup mode.
|
|
7
|
+
if [ "$(git config --get commit.cleanup 2>/dev/null || true)" = scissors ]; then
|
|
8
|
+
msg_file="$1"
|
|
9
|
+
tmp="${msg_file}.spexcode-scissors.$$"
|
|
10
|
+
awk '/^# ------------------------ >8 ------------------------$/ { exit } { print }' "$msg_file" \
|
|
11
|
+
| git stripspace --strip-comments > "$tmp"
|
|
12
|
+
mv "$tmp" "$msg_file"
|
|
13
|
+
fi
|
|
14
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spexcode-managed-hook-v1
|
|
3
|
+
# @@@ footprint refresh (post-checkout) ([[commit-surgery]]) - a branch checkout is one of the three git
|
|
4
|
+
# transitions that can move the materialize's inputs: .spec/.plugins content changes across branches, and a
|
|
5
|
+
# contract file's TRACKEDNESS can flip (switching to a branch that tracks CLAUDE.md checks out the pristine
|
|
6
|
+
# index prose — the re-materialize here writes the block back into the working file, binds the clean/smudge
|
|
7
|
+
# filter, and withdraws the exclude entry; the kind-transition heals itself). Git-native anchors only —
|
|
8
|
+
# never a harness event. Quiet, best-effort: a failed refresh self-heals at the next anchor (pre-commit's
|
|
9
|
+
# unconditional materialize is the correctness backstop).
|
|
10
|
+
#
|
|
11
|
+
# args: <prev-HEAD> <new-HEAD> <flag>; flag=1 is a branch checkout, flag=0 a file checkout (git checkout --
|
|
12
|
+
# <path> restores files and moves nothing the materialize depends on — skip those).
|
|
13
|
+
[ "${3:-0}" = "1" ] || exit 0
|
|
14
|
+
# Session creation owns the one post-seed materialize in its transaction.
|
|
15
|
+
[ "${SPEXCODE_DEFER_FOOTPRINT_REFRESH:-}" = "session-create" ] && exit 0
|
|
16
|
+
main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
|
|
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
|
|
20
|
+
elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
|
|
21
|
+
"$repo_root/node_modules/.bin/spex" internal refresh-footprint >/dev/null 2>&1 || true
|
|
22
|
+
elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
|
|
23
|
+
"$main_root/spec-cli/bin/spex.mjs" internal refresh-footprint >/dev/null 2>&1 || true
|
|
24
|
+
fi
|
|
25
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spexcode-managed-hook-v1
|
|
3
|
+
# @@@ issue nudge (post-merge) - the taste store ([[local-issues]]) fires AFTER the work lands, not while the
|
|
4
|
+
# agent is finishing it: the agent's own task comes first. Merge is dispatched to the session's OWN agent
|
|
5
|
+
# (see [[dispatch]]), so this hook runs in that live merge turn and its stdout reaches the agent as the merge
|
|
6
|
+
# command's own output — a git-native nudge that costs no harness block-cap and reaches a self-launched agent
|
|
7
|
+
# too. GUARDED to the `merge node/<id>:` commit, so an ordinary `git pull`/unrelated merge never nags.
|
|
8
|
+
#
|
|
9
|
+
# The nudge TEXT and the on/off toggle both live in the CLI (`spex internal nudge <node>` prints nothing when
|
|
10
|
+
# the feature is OFF in spexcode.json), so this hook is a THIN resolver + caller: resolve `spex` the same
|
|
11
|
+
# three ways the pre-commit hook does, pass the merged node, echo whatever it prints. No CLI resolvable → no
|
|
12
|
+
# nudge (safe degradation, same as the lint shim's advisory mode).
|
|
13
|
+
main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
|
|
14
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
15
|
+
spex_run() {
|
|
16
|
+
if command -v spex >/dev/null 2>&1; then spex "$@"
|
|
17
|
+
elif [ -x "$repo_root/node_modules/.bin/spex" ]; then "$repo_root/node_modules/.bin/spex" "$@"
|
|
18
|
+
elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then "$main_root/spec-cli/bin/spex.mjs" "$@"
|
|
19
|
+
else return 127; fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# @@@ footprint refresh ([[commit-surgery]]) - a merge is one of the three git transitions that can move the
|
|
23
|
+
# materialize's inputs (.spec/.plugins content, contract-file trackedness), so re-materialize here — git-native
|
|
24
|
+
# anchors only, never a harness event. Quiet, best-effort: a failed refresh self-heals at the next anchor.
|
|
25
|
+
spex_run internal refresh-footprint >/dev/null 2>&1 || true
|
|
26
|
+
|
|
27
|
+
subj=$(git log -1 --format=%s 2>/dev/null)
|
|
28
|
+
case "$subj" in
|
|
29
|
+
"merge node/"*) ;;
|
|
30
|
+
*) exit 0 ;;
|
|
31
|
+
esac
|
|
32
|
+
node=$(printf '%s' "$subj" | sed -n 's#^merge \(node/[^:]*\):.*#\1#p')
|
|
33
|
+
spex_run internal nudge "$node"
|
|
34
|
+
exit 0
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spexcode-managed-hook-v1
|
|
3
|
+
# @@@ shared spex runner - resolve the `spex` CLI ONCE, into spex_cli(), so BOTH the main-guard trunk
|
|
4
|
+
# lookup and the spec-lint shim shim through the same mechanism (no duplicated, drift-prone resolution).
|
|
5
|
+
# This must work for BOTH ways this hook ships: a real project that installed @spexcode/spec-cli, and the
|
|
6
|
+
# SpexCode monorepo dogfooding itself from source. cwd stays the committing worktree either way. main_root
|
|
7
|
+
# is the parent of the shared git *common* dir (= layout.ts mainCheckout()); a fresh session worktree has
|
|
8
|
+
# no node_modules of its own, so the dogfood tsx path reaches the main checkout's. First that exists wins:
|
|
9
|
+
# 1. `spex` on PATH - installed globally / on the shell PATH.
|
|
10
|
+
# 2. <repo>/node_modules/.bin/spex - a project that did `npm i @spexcode/spec-cli` (the dep's bin).
|
|
11
|
+
# 3. monorepo launcher by path - dogfood: bin/spex.mjs (owns tsx resolution + the mid-merge guard).
|
|
12
|
+
main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
|
|
13
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
14
|
+
spex_kind=
|
|
15
|
+
if command -v spex >/dev/null 2>&1; then
|
|
16
|
+
spex_kind=path
|
|
17
|
+
elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
|
|
18
|
+
spex_kind=local
|
|
19
|
+
elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
|
|
20
|
+
spex_kind=pkg
|
|
21
|
+
fi
|
|
22
|
+
spex_cli() {
|
|
23
|
+
case "$spex_kind" in
|
|
24
|
+
path) spex "$@" ;;
|
|
25
|
+
local) "$repo_root/node_modules/.bin/spex" "$@" ;;
|
|
26
|
+
pkg) "$main_root/spec-cli/bin/spex.mjs" "$@" ;;
|
|
27
|
+
*) return 127 ;;
|
|
28
|
+
esac
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# A commit containing only issue storage cannot change governed code or an anchor range. Keep this as a
|
|
32
|
+
# path-property check at the hook boundary so the common dashboard write does not pay materialize/eval
|
|
33
|
+
# backstops; any mixed commit continues through the full hook chain and the reference transaction gate.
|
|
34
|
+
staged_issue_only=1
|
|
35
|
+
staged_any=0
|
|
36
|
+
while IFS= read -r -d '' staged_path; do
|
|
37
|
+
staged_any=1
|
|
38
|
+
case "$staged_path" in .spec/.issues/*) ;; *) staged_issue_only=0 ;; esac
|
|
39
|
+
done < <(git diff --cached --name-only -z 2>/dev/null)
|
|
40
|
+
if [ "$staged_any" -eq 1 ] && [ "$staged_issue_only" -eq 1 ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# @@@ tree-unchanged stamp - a commit whose tree EQUALS HEAD's tree (`git write-tree` over the commit's own
|
|
45
|
+
# index — the hook's GIT_INDEX_FILE, a temp HEAD-tree index for `commit --only --allow-empty`) adds NO
|
|
46
|
+
# content: it cannot smuggle code past the main-guard below and cannot introduce spec↔code drift, so the
|
|
47
|
+
# whole hook passes it. This is `spex spec ack`'s empty Spec-OK stamp — the reason ack works on a trunk merge
|
|
48
|
+
# commit (amending it would re-author the merge after MERGE_HEAD is gone and read as direct authoring) and
|
|
49
|
+
# with a DIRTY real index (the lint shim below reads the real index via git.ts's env-strip, which would
|
|
50
|
+
# otherwise block the stamp on the very drift it acknowledges). Deliberately a one-line tree compare, not an
|
|
51
|
+
# "is this an amend?" heuristic (undecidable at pre-commit time).
|
|
52
|
+
if [ "$(git write-tree 2>/dev/null)" = "$(git rev-parse 'HEAD^{tree}' 2>/dev/null || echo none)" ]; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# @@@ main-guard - the worktree model: never author directly on the trunk; the trunk only RECEIVES merges.
|
|
57
|
+
# Reject a direct commit while HEAD is the trunk. Merges pass (MERGE_HEAD present), and a tree-unchanged
|
|
58
|
+
# stamp already passed above. Escape hatch for seeding / eager topology: SPEXCODE_ALLOW_MAIN=1 git commit …
|
|
59
|
+
#
|
|
60
|
+
# The trunk is resolved the SAME way the rest of SpexCode resolves it — `spex internal trunk` = layout.ts
|
|
61
|
+
# mainBranch() (the stable mainBranch project fact stamped at adoption; conventional default 'main') — so
|
|
62
|
+
# the guard protects whatever the repo's trunk is actually named. When the CLI isn't resolvable (advisory
|
|
63
|
+
# mode), only the convention is available: shell does not guess identity from a mutable checkout.
|
|
64
|
+
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo)
|
|
65
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null)
|
|
66
|
+
trunk=$(spex_cli internal trunk 2>/dev/null | head -1)
|
|
67
|
+
[ -n "$trunk" ] || trunk=main
|
|
68
|
+
if [ "$branch" = "$trunk" ] && [ ! -f "$git_dir/MERGE_HEAD" ] && [ -z "${SPEXCODE_ALLOW_MAIN:-}" ]; then
|
|
69
|
+
echo "✗ SpexCode: direct commits on $trunk (the trunk) are blocked." >&2
|
|
70
|
+
echo " Work in a worktree (.worktrees/<node>, branch node/<id>) and merge back." >&2
|
|
71
|
+
echo " Merges and tree-unchanged stamps (spex spec ack) pass automatically; for seeding/topology: SPEXCODE_ALLOW_MAIN=1 git commit …" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# @@@ footprint surgery ([[commit-surgery]]) - the history anchor: an UNCONDITIONAL materialize (masks fresh
|
|
76
|
+
# at the one moment history is written) + staged-index repair — strip the spexcode sentinel block from any
|
|
77
|
+
# staged blob (source = the staged blob, so `git add -p` partial staging survives), unstage HEAD-untracked
|
|
78
|
+
# generated artifacts. REPAIRS AND PROCEEDS, never rejects; one note per repair on stderr. Runs INSIDE the
|
|
79
|
+
# hook env on purpose: the surgery honors GIT_INDEX_FILE so a pathspec/`-a` commit's TEMPORARY index is the
|
|
80
|
+
# one repaired. Advisory when spex is unresolvable or errors (CI lint enforces); never blocks the commit.
|
|
81
|
+
if [ -n "$spex_kind" ]; then
|
|
82
|
+
spex_cli internal commit-surgery || echo "• SpexCode: footprint surgery skipped (error above) — a generated artifact may be staged; CI still enforces." >&2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# @@@ spec-lint - reference-transaction owns the candidate gate. When the installed consumer is the
|
|
86
|
+
# SpexCode unmarked predicate, running full HEAD lint here would restore the history-scaled cost this gate
|
|
87
|
+
# removes. A missing/custom consumer keeps the old full lint fallback, so a hook collision never reduces
|
|
88
|
+
# local coverage. Static identity matters: never execute a user-owned hook with made-up probe arguments.
|
|
89
|
+
# Bypass with SPEXCODE_SKIP_LINT=1. It shims through shared spex_cli() resolved above.
|
|
90
|
+
if [ -z "${SPEXCODE_SKIP_LINT:-}" ]; then
|
|
91
|
+
if [ -n "$spex_kind" ]; then
|
|
92
|
+
consume_hook=$(git rev-parse --git-path hooks/reference-transaction 2>/dev/null)
|
|
93
|
+
if [ -x "$consume_hook" ] && grep -q 'one unmarked predicate at Git' "$consume_hook" 2>/dev/null; then
|
|
94
|
+
lint_rc=0
|
|
95
|
+
else
|
|
96
|
+
spex_cli spec lint >&2
|
|
97
|
+
lint_rc=$?
|
|
98
|
+
fi
|
|
99
|
+
# exit 75 = the launcher's mid-merge guard (spex's own source holds conflict markers — transient).
|
|
100
|
+
# Degrade to advisory rather than walling every commit behind a merge someone else is resolving.
|
|
101
|
+
if [ "$lint_rc" -eq 75 ]; then
|
|
102
|
+
echo "• SpexCode: spec-lint skipped — spex is paused mid-merge (exit 75); CI still enforces." >&2
|
|
103
|
+
elif [ "$lint_rc" -ne 0 ]; then
|
|
104
|
+
echo "✗ SpexCode: spec-lint failed — fix the spec↔code links or bypass with SPEXCODE_SKIP_LINT=1." >&2
|
|
105
|
+
exit 1
|
|
106
|
+
else
|
|
107
|
+
# @@@ eval backstop - reject a staged eval offender: a stray evidence blob copied into the tree (blobs
|
|
108
|
+
# belong in the shared git common dir, never committed) OR a malformed eval.md (a scenario schema
|
|
109
|
+
# violation). Logic lives in spec-eval; this is the lint-shim's twin. (this node's contract)
|
|
110
|
+
if ! spex_cli internal check-staged >&2; then
|
|
111
|
+
echo "✗ SpexCode: internal check-staged failed (see above) — fix it or bypass with SPEXCODE_SKIP_LINT=1." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
else
|
|
116
|
+
# Advisory hook: no CLI found (project hasn't installed @spexcode/spec-cli; monorepo not `npm install`ed).
|
|
117
|
+
echo "• SpexCode: spec-lint skipped — no \`spex\` CLI found (install @spexcode/spec-cli); CI still enforces." >&2
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
exit 0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spexcode-managed-hook-v1
|
|
3
|
+
# @@@ session-stamp - auto-attribute a commit to the SESSION that authored it, ACROSS HARNESSES. SpexCode
|
|
4
|
+
# reads the `Session:` trailer as a version's attribution (git is the database), keyed by the SpexCode record
|
|
5
|
+
# id so the dashboard's session window links to it.
|
|
6
|
+
#
|
|
7
|
+
# The id is simply READ here, because it was established where it is KNOWN: whoever creates an agent's
|
|
8
|
+
# execution context injects that context's own record id, and nothing else may carry one.
|
|
9
|
+
# - a session launch bakes `SPEXCODE_SESSION_ID=<record id>` into the agent's process — and STRIPS every
|
|
10
|
+
# session-identity variable it inherited first, so the pane's ancestry (a tmux server started long ago by
|
|
11
|
+
# someone else) cannot smuggle one in;
|
|
12
|
+
# - a codex thread gets the same variable injected into every command it spawns, at `thread/start`, via
|
|
13
|
+
# codex's own `shell_environment_policy.set` — the tool shell is a child of the SHARED app-server and must
|
|
14
|
+
# therefore inherit nothing, so the backend hands it its identity per thread instead;
|
|
15
|
+
# - that shared app-server, and any other process we own that belongs to no single session, is spawned with
|
|
16
|
+
# those variables stripped. A process that outlived its session handing its id to strangers is exactly how
|
|
17
|
+
# this trailer once named a session that no longer existed (github#76).
|
|
18
|
+
# So there is nothing to resolve, alias, or verify at commit time: the variable is present only in processes
|
|
19
|
+
# that genuinely belong to that session, and it already IS the record id. No store lookup, no per-harness
|
|
20
|
+
# tiers, and nothing derived from the current directory — where a process stands says nothing about who it is.
|
|
21
|
+
# No id → no trailer. An existing Session: trailer is left alone, so the ritual / a human can set one by hand.
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
msg_file="$1"
|
|
24
|
+
grep -qiE '^Session:[[:space:]]' "$msg_file" && exit 0
|
|
25
|
+
[ -n "${SPEXCODE_SESSION_ID:-}" ] || exit 0
|
|
26
|
+
|
|
27
|
+
# Stamp via interpret-trailers (git's own sample-hook mechanism), NOT a raw printf append: appending after a
|
|
28
|
+
# message that already carries a trailer block (e.g. `spex spec ack`'s Spec-OK) opens a new paragraph, and git then
|
|
29
|
+
# parses ONLY the last paragraph as trailers — the earlier trailers silently become body prose. interpret-trailers
|
|
30
|
+
# appends into the existing block, keeping ONE parseable block. It needs the file newline-terminated first:
|
|
31
|
+
# on an unterminated message (git merge -m writes MERGE_MSG without one) it folds the trailer into the subject
|
|
32
|
+
# paragraph, where git's trailer parser never looks — the reason merge commits' Session: never parsed.
|
|
33
|
+
[ -n "$(tail -c1 "$msg_file")" ] && echo >> "$msg_file"
|
|
34
|
+
git interpret-trailers --in-place --trailer "Session: $SPEXCODE_SESSION_ID" "$msg_file"
|
|
35
|
+
exit 0
|