@chrono-meta/fh-gate 1.4.77 → 1.4.78

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.
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+ # sidecar_calibrate.sh — measure the cross-family sidecar panel before trusting it.
3
+ #
4
+ # WHY: `auto-decorrelation` and the load-bearing cross-family gate both ask "is a different-family
5
+ # auditor reachable?" — and until now that question was answered from memory. Two measured failures
6
+ # on 2026-07-30, one in each direction:
7
+ # · A marker was written claiming `cross-family unavailable this session`. Probed later in the same
8
+ # session, codex ran fine. Unavailability was DECLARED, never measured.
9
+ # · agy pinned with the slug `gemini-3.1-pro-high` answered as **Gemini 3.6 Flash** — silently, with
10
+ # no error. "The sidecar ran" and "the model I pinned answered" are different propositions, and
11
+ # only the second one licenses a claim about model-family diversity.
12
+ # A panel you have not probed is not a panel; it is an assumption with a hostname.
13
+ #
14
+ # WHAT IT MEASURES, per runtime — four legs, because each catches a different lie:
15
+ # REACHABLE the binary exists and runs at all
16
+ # PIN-OK / UNTRUSTED-PIN
17
+ # a discriminating identity probe: does the answer NAME the model that was
18
+ # pinned? A generic "OK" proves nothing — any model returns it. This is the only
19
+ # anchor for a runtime that falls back silently.
20
+ # control: rejects-bogus / accepts-bogus
21
+ # pin a model that cannot exist. This measures ONE thing only: whether unknown
22
+ # names are validated. It does NOT mean known names are served faithfully, and
23
+ # the gap between those is where the real trap lives — agy REJECTS nonsense yet
24
+ # silently served Flash for `gemini-3.1-pro-high`, a slug from its own
25
+ # catalogue (measured 2026-07-30). So `rejects-bogus` must never be read as
26
+ # "the pin is safe": the identity probe still decides. `accepts-bogus` is the
27
+ # stronger warning — there, the identity probe is the ONLY evidence at all.
28
+ # VERDICT-OK / VERDICT-UNPARSEABLE
29
+ # ask for one bare token. A runtime that answers with agentic prose cannot carry
30
+ # a machine-read verdict. Measured for agy at 1.0.14 (2026-07-04) and NOT
31
+ # inherited here: the version has moved, and this file re-measures rather than
32
+ # quoting. Fitness is a per-run measurement, not a property.
33
+ #
34
+ # Detector, never a gate: ALWAYS exits 0. It reports; the caller decides. A calibration run that
35
+ # could block would make callers stop running it, which is the failure this exists to prevent.
36
+ #
37
+ # Cost: real API calls (3 short probes per runtime). Use --only to scope. `--stub-model` exists for
38
+ # the lane harness so it can drive stub CLIs without touching a real catalogue.
39
+ #
40
+ # Usage: bash scripts/sidecar_calibrate.sh [--only codex|agy] [--stub-model NAME] [--quiet]
41
+ # Output: one block per runtime, then a PANEL line — the line a marker's `crossfamily:` leg quotes.
42
+
43
+ set -uo pipefail
44
+
45
+ ONLY=""; STUB_MODEL=""; QUIET=""
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --only) ONLY="${2:-}"; shift 2 ;;
49
+ --stub-model) STUB_MODEL="${2:-}"; shift 2 ;;
50
+ --quiet) QUIET=1; shift ;;
51
+ *) shift ;;
52
+ esac
53
+ done
54
+
55
+ TIMEOUT_BIN=""
56
+ command -v timeout >/dev/null 2>&1 && TIMEOUT_BIN="timeout"
57
+ command -v gtimeout >/dev/null 2>&1 && TIMEOUT_BIN="gtimeout"
58
+ # No coreutils timeout on stock macOS. perl's alarm is the portable watchdog; if perl is missing too,
59
+ # run WITHOUT a deadline rather than skipping the probe — a missing watchdog must never become a
60
+ # silently skipped measurement reported as absence (the same rule fh_session_load.sh applies).
61
+ _run() {
62
+ local secs="$1"; shift
63
+ if [ -n "$TIMEOUT_BIN" ]; then "$TIMEOUT_BIN" "$secs" "$@"
64
+ elif command -v perl >/dev/null 2>&1; then perl -e 'alarm shift @ARGV; exec @ARGV' "$secs" "$@"
65
+ else "$@"; fi
66
+ }
67
+
68
+ # _answer — reduce a runtime's stdout to THE MODEL'S ANSWER.
69
+ # Measured on the first real run (2026-07-30): matching against whole stdout is unsound, because a
70
+ # real CLI prints a session banner that REPEATS the pinned model back (`codex exec` emits its
71
+ # version, workdir and model config before the reply). Under a whole-stdout match, a runtime that
72
+ # merely echoes its own configuration passes the identity probe — the transport layer supplying the
73
+ # very evidence the probe exists to obtain from the model. So: take the last non-empty line, skipping
74
+ # trailing telemetry (`tokens used`, bare numbers, rule lines).
75
+ # RESIDUAL, named: this is a heuristic on line position. A runtime that prints its answer and then
76
+ # unrecognised trailing chatter would be mis-read. It is checked by the banner-echo lane, not proven
77
+ # in general; a structured output mode (JSON) would replace the heuristic and none is used here yet.
78
+ _answer() {
79
+ awk 'BEGIN{last=""}
80
+ {line=$0
81
+ gsub(/\r/,"",line)
82
+ gsub(/^[[:space:]]+|[[:space:]]+$/,"",line)
83
+ if (line=="") next
84
+ if (line ~ /^[-=_]{3,}$/) next
85
+ if (tolower(line) ~ /^tokens? used/) next
86
+ if (line ~ /^[0-9,.]+$/) next
87
+ last=line}
88
+ END{print last}'
89
+ }
90
+
91
+ IDENTITY_PROMPT="Answer with one line only: your exact model name and version."
92
+ VERDICT_PROMPT="Reply with exactly one word, PASS or FAIL, and nothing else. The word is PASS."
93
+ BOGUS_MODEL="zzz-nonexistent-model-9.9"
94
+
95
+ PANEL=""
96
+
97
+ # build_cmd <runtime> <model> <prompt> — fills CMD as an argv array.
98
+ # NOT a shell function passed to the watchdog: `timeout`/`gtimeout` exec a BINARY and cannot see
99
+ # shell functions, so an earlier revision fed them a function name and every probe came back as the
100
+ # runtime failing to run (caught by lane 3/4b/5b, not by reading).
101
+ build_cmd() {
102
+ case "$1" in
103
+ codex) CMD=(codex exec -m "$2" -c model_reasoning_effort=high --skip-git-repo-check "$3") ;;
104
+ agy) CMD=(agy -p "$3" --model "$2" --print-timeout 170s) ;;
105
+ *) CMD=("$1" "$3") ;;
106
+ esac
107
+ }
108
+
109
+ # probe_runtime <name> <real-model-pin>
110
+ probe_runtime() {
111
+ local rt="$1" model="$2"
112
+ [ -n "$ONLY" ] && [ "$ONLY" != "$rt" ] && return 0
113
+ [ -n "$STUB_MODEL" ] && model="$STUB_MODEL"
114
+
115
+ if ! command -v "$rt" >/dev/null 2>&1; then
116
+ printf '%-6s ABSENT — not installed on this machine (absence measured, not assumed)\n' "$rt"
117
+ return 0
118
+ fi
119
+
120
+ local id_out pin_state ctl_out ctl_state v_out v_state
121
+ build_cmd "$rt" "$model" "$IDENTITY_PROMPT"
122
+ id_out="$(_run 200 "${CMD[@]}" 2>&1 | _answer)"
123
+
124
+ # Discriminating check — the answer must be the MODEL's self-report naming the model that was
125
+ # pinned. What counts as "naming it" is the VERSION token, not every token of the pin slug:
126
+ # vendors answer with a product name (`gpt-5.6-sol` → "GPT-5.6 Codex"), and demanding the suffix
127
+ # made this report UNTRUSTED-PIN for a pin that had actually held (measured 2026-07-30). The
128
+ # version is also exactly where the real failures differ — 3.1 asked, 3.6 answered. If a pin
129
+ # carries no version token at all, fall back to requiring the name words, since then there is
130
+ # nothing sharper to test.
131
+ local ver name_words hit=0
132
+ ver="$(printf '%s' "$model" | grep -oE '[0-9]+\.[0-9]+' | head -1)"
133
+ name_words="$(printf '%s' "$model" | tr 'A-Z' 'a-z' | sed -E 's/[^a-z]+/ /g' \
134
+ | tr ' ' '\n' | grep -E '^[a-z]{3,}$' \
135
+ | grep -vE '^(high|low|medium|thinking|the|exec)$' | head -1)"
136
+ if [ -n "$id_out" ]; then
137
+ if [ -n "$ver" ]; then
138
+ printf '%s' "$id_out" | grep -qF "$ver" && hit=1
139
+ elif [ -n "$name_words" ]; then
140
+ printf '%s' "$id_out" | tr 'A-Z' 'a-z' | grep -qF "$name_words" && hit=1
141
+ fi
142
+ fi
143
+ if [ "$hit" -eq 1 ]; then pin_state="PIN-OK"; else pin_state="UNTRUSTED-PIN"; fi
144
+
145
+ build_cmd "$rt" "$BOGUS_MODEL" "$IDENTITY_PROMPT"
146
+ ctl_out="$(_run 120 "${CMD[@]}" 2>&1)"
147
+ if [ $? -ne 0 ] || printf '%s' "$ctl_out" | grep -qiE 'not supported|invalid|unknown model|error'; then
148
+ ctl_state="rejects-bogus"
149
+ else
150
+ ctl_state="accepts-bogus"
151
+ fi
152
+
153
+ build_cmd "$rt" "$model" "$VERDICT_PROMPT"
154
+ v_out="$(_run 200 "${CMD[@]}" 2>&1 | _answer)"
155
+ # Parseable = a bare verdict token is recoverable from a short answer. Prose that merely CONTAINS
156
+ # the word does not qualify: a verdict channel must be readable without a human deciding what the
157
+ # runtime meant.
158
+ local v_compact
159
+ v_compact="$(printf '%s' "$v_out" | tr -s '[:space:]' ' ' | sed 's/^ *//; s/ *$//')"
160
+ if [ "${#v_compact}" -le 12 ] && printf '%s' "$v_compact" | grep -qiE '^(pass|fail)[.!]?$'; then
161
+ v_state="VERDICT-OK"
162
+ else
163
+ v_state="VERDICT-UNPARSEABLE"
164
+ fi
165
+
166
+ printf '%-6s REACHABLE · %s · control: %s · %s\n' "$rt" "$pin_state" "$ctl_state" "$v_state"
167
+ [ -z "$QUIET" ] && printf ' pinned: %s\n identity said: %s\n' "$model" "$(printf '%s' "$id_out" | cut -c1-100)"
168
+ if [ "$ctl_state" = "accepts-bogus" ]; then
169
+ printf ' ⚠️ this runtime does not validate the pin at all, so the identity probe is the ONLY\n'
170
+ printf ' evidence that the intended model answered — a clean run proves nothing here.\n'
171
+ elif [ "$pin_state" = "UNTRUSTED-PIN" ]; then
172
+ printf ' ⚠️ it rejects UNKNOWN names, which says nothing about serving KNOWN ones faithfully.\n'
173
+ printf ' The identity probe disagreed with the pin — treat this runtime as substituting.\n'
174
+ fi
175
+ # Only a runtime whose pin is trustworthy counts toward the panel. A reachable runtime answering as
176
+ # some other model contributes no family diversity, which is the entire point of the panel.
177
+ [ "$pin_state" = "PIN-OK" ] && PANEL="${PANEL:+$PANEL, }$rt"
178
+ return 0
179
+ }
180
+
181
+ probe_runtime codex "gpt-5.6-sol"
182
+ probe_runtime agy "Gemini 3.1 Pro (High)"
183
+
184
+ if [ -n "$PANEL" ]; then
185
+ echo "PANEL: $PANEL — usable different-family auditor(s), pin verified this run"
186
+ else
187
+ echo "PANEL: none — no runtime passed the identity probe on this machine, this run"
188
+ echo " State this, do not infer it: a marker's crossfamily leg may say 'none' but never stay silent."
189
+ fi
190
+ exit 0
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env bash
2
+ # test_node_check_lanes.sh — regression lanes for scripts/fh_node_check.sh.
3
+ #
4
+ # WHY THIS FILE EXISTS: across three adversarial rounds on the node check, every surviving defect
5
+ # was a NEGATIVE leg nobody was testing — "the floor does not apply here", "those are someone
6
+ # else's hooks", "that file is absent on a fresh clone". Each round's fix reverted a previous
7
+ # round's fix because no lane pinned it. The lanes below are that pin: they encode the *shape* of
8
+ # each defect, not just its instance.
9
+ #
10
+ # Usage: bash scripts/test_node_check_lanes.sh
11
+ # Exit: 0 = all lanes pass; 1 = at least one lane failed (prints which and why).
12
+
13
+ set -uo pipefail
14
+
15
+ FH_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
16
+ CHECK="$FH_REPO/scripts/fh_node_check.sh"
17
+ TMP="$(mktemp -d)"
18
+ trap 'rm -rf "$TMP"' EXIT
19
+
20
+ PASS=0; FAIL=0
21
+ ok() { PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
22
+ bad() { FAIL=$((FAIL+1)); printf ' ❌ %s\n got: %s\n' "$1" "$(printf '%s' "$2" | tr '\n' '|' | cut -c1-220)"; }
23
+
24
+ # run <hubdir> <statefile> [env assignments...] → stdout of one check run
25
+ run() { local hub="$1" st="$2"; shift 2; env "$@" HUB_DIR="$hub" FH_NODE_STATE="$st" bash "$CHECK" 2>&1; }
26
+
27
+ mk_git_hub() { # a git repo with FH-style hooks installed and executable
28
+ local d="$1" fh_sentinel="${2:-yes}"
29
+ mkdir -p "$d" && git -C "$d" init -q && git -C "$d" config user.email t@t && git -C "$d" config user.name t
30
+ echo x > "$d/f" && git -C "$d" add f && git -C "$d" -c commit.gpgsign=false commit -qm init
31
+ mkdir -p "$d/.git/hooks"
32
+ local body='#!/bin/sh\nexit 0\n'
33
+ [ "$fh_sentinel" = "yes" ] && body='#!/bin/sh\n# FH 4-Axis Gate Pre-Commit Hook\nfh-gate\nexit 0\n'
34
+ printf "$body" > "$d/.git/hooks/pre-commit"; printf "$body" > "$d/.git/hooks/pre-push"
35
+ chmod +x "$d/.git/hooks/pre-commit" "$d/.git/hooks/pre-push"
36
+ }
37
+
38
+ echo "── node-check lanes ──"
39
+
40
+ # LANE 1 (S3-1) — NOT a git repo. The git-hook floor cannot be installed here at all, so it is
41
+ # N/A, not missing. Reporting it would be an unfixable notice repeating every session forever.
42
+ mkdir -p "$TMP/nogit"
43
+ out="$(run "$TMP/nogit" "$TMP/s1" FH_MACHINE_ID=nogitbox)"
44
+ # POSITIVE CONTROL: assert what MUST appear alongside what must not. Absence-only lanes are
45
+ # satisfied by a script that prints nothing at all — an `exit 0` stub passed lanes 1, 4 and 7
46
+ # (cross-family mutation 2026-07-30), so each now pins an expected utterance too.
47
+ case "$out" in
48
+ *"Missing mechanical floor"*) bad "lane1 non-git: must NOT claim a missing floor (N/A, unfixable)" "$out" ;;
49
+ *"first session for this clone"*) ok "lane1 non-git: N/A on the git floor, but still reports the node event" ;;
50
+ *) bad "lane1 non-git: silent — a stub would pass this lane" "$out" ;;
51
+ esac
52
+
53
+ # LANE 2 (S3-2) — hooks exist but belong to another framework (husky/pre-commit). Executable ≠ FH's
54
+ # gate. Silence here is the exact accident this check was built for: FH gates absent, machine quiet.
55
+ mk_git_hub "$TMP/husky" no
56
+ out="$(run "$TMP/husky" "$TMP/s2" FH_MACHINE_ID=huskybox)"
57
+ case "$out" in
58
+ *"Missing mechanical floor"*) ok "lane2 foreign hooks: FH gate absence reported" ;;
59
+ *) bad "lane2 foreign hooks: non-FH hooks were accepted as FH floors" "$out" ;;
60
+ esac
61
+
62
+ # LANE 3 (S3-3) — Mode D user on a FRESH clone: companion store present, settings.local.json absent
63
+ # (it is gitignored, so a clone never has it). This is the MEASURED 2026-07-30 incident. Must speak.
64
+ mk_git_hub "$TMP/moded" yes
65
+ mkdir -p "$TMP/companion/.git"
66
+ out="$(run "$TMP/moded" "$TMP/s3" FH_MACHINE_ID=modedbox BE_DIR="$TMP/companion")"
67
+ case "$out" in
68
+ *companion-load*) ok "lane3 fresh Mode D clone: companion-load absence surfaced" ;;
69
+ *) bad "lane3 fresh Mode D clone: SILENT — the measured incident would recur" "$out" ;;
70
+ esac
71
+
72
+ # LANE 4 (M2-4) — public non-Mode-D user: no companion store anywhere. The companion item must not
73
+ # appear, or the majority path gets a false positive for a feature it does not use.
74
+ mk_git_hub "$TMP/public" yes
75
+ out="$(run "$TMP/public" "$TMP/s4" FH_MACHINE_ID=publicbox)"
76
+ case "$out" in
77
+ *companion-load*) bad "lane4a non-Mode-D: companion item shown to a user with no store" "$out" ;;
78
+ *"first session for this clone"*) ok "lane4a non-Mode-D: companion absent, node event still reported" ;;
79
+ *) bad "lane4a non-Mode-D: silent — a stub would pass this lane" "$out" ;;
80
+ esac
81
+
82
+ # LANE 4b (S4-1) — CLAUDE.local.md is Claude Code's STANDARD local-override file; anyone may keep
83
+ # one for any reason. Its mere EXISTENCE must not classify a user as Mode D, or the majority path
84
+ # gets a companion notice every session forever (state-based emission makes it permanent, not
85
+ # one-shot). Only the binding INSIDE the file counts.
86
+ printf '# my local notes\nuse tabs not spaces\n' > "$TMP/public/CLAUDE.local.md"
87
+ out="$(run "$TMP/public" "$TMP/s4b" FH_MACHINE_ID=publicbox)"
88
+ case "$out" in
89
+ *companion-load*) bad "lane4b plain CLAUDE.local.md: existence alone classified the user as Mode D" "$out" ;;
90
+ *) ok "lane4b plain CLAUDE.local.md: not treated as a Mode D signal" ;;
91
+ esac
92
+
93
+ # LANE 4c — the same file WITH a companion binding must classify as Mode D and speak. Without this
94
+ # leg, "never classify as Mode D" would also pass 4b.
95
+ # One fixture PER alternative: a single fixture like `BE_DIR=/some/companion-store` satisfies two
96
+ # alternatives at once, so either could be deleted and the lane would still pass. The store is a
97
+ # ROLE, not a repo layout (install-wizard SKILL.md: Obsidian vault / gbrain ingest target / *-be
98
+ # repo all qualify) — so the vocabulary variants are the documented user base, not hypotheticals.
99
+ i=0
100
+ # `backend: obsidian` carries no other keyword on purpose — with a `vault:` fixture only, the
101
+ # `obsidian` alternative is never exercised and could be deleted with the suite staying green
102
+ # (verified: removing it left 16/16). An untested alternative is an untested branch.
103
+ for binding in 'BE_DIR=/x/store' 'companion store: ~/notes' '컴패니언 스토어: ~/notes' \
104
+ 'vault: ~/vaults/notes' 'gbrain ingest target: ~/gbrain' 'backend: obsidian'; do
105
+ i=$((i+1))
106
+ printf '# local\n%s\n' "$binding" > "$TMP/public/CLAUDE.local.md"
107
+ out="$(run "$TMP/public" "$TMP/s4c$i" FH_MACHINE_ID=publicbox)"
108
+ case "$out" in
109
+ *companion-load*) ok "lane4c.$i Mode D detected via: $binding" ;;
110
+ *) bad "lane4c.$i binding present but Mode D not detected: $binding" "$out" ;;
111
+ esac
112
+ done
113
+ rm -f "$TMP/public/CLAUDE.local.md"
114
+
115
+ # LANE 5 — emission model: a healthy machine speaks once (event) then goes silent.
116
+ mk_git_hub "$TMP/healthy" yes
117
+ r1="$(run "$TMP/healthy" "$TMP/s5" FH_MACHINE_ID=healthybox)"
118
+ r2="$(run "$TMP/healthy" "$TMP/s5" FH_MACHINE_ID=healthybox)"
119
+ if [ -n "$r1" ] && [ -z "$r2" ]; then ok "lane5 healthy: event once, then silent"
120
+ else bad "lane5 healthy: expected run1 non-empty and run2 empty" "r1=[$r1] r2=[$r2]"; fi
121
+
122
+ # LANE 6 (S2-4) — a missing floor is a CONDITION: it must be reported on every run, not once.
123
+ mk_git_hub "$TMP/broken" yes
124
+ rm -f "$TMP/broken/.git/hooks/pre-commit"
125
+ n=0
126
+ for i in 1 2 3; do
127
+ o="$(run "$TMP/broken" "$TMP/s6" FH_MACHINE_ID=brokenbox)"
128
+ case "$o" in *"Missing mechanical floor"*) n=$((n+1)) ;; esac
129
+ done
130
+ [ "$n" -eq 3 ] && ok "lane6 broken: reported on all 3 runs (condition, not event)" \
131
+ || bad "lane6 broken: reported $n/3 runs — a broken machine went quiet" "n=$n"
132
+
133
+ # LANE 7 (M2-2) — linked worktree: .git is a FILE there, so a hand-built "$FH/.git/hooks" does not
134
+ # exist and working hooks read as missing.
135
+ mk_git_hub "$TMP/wt" yes
136
+ git -C "$TMP/wt" worktree add -q "$TMP/wt_linked" -b lane7 2>/dev/null
137
+ out="$(run "$TMP/wt_linked" "$TMP/s7" FH_MACHINE_ID=wtbox)"
138
+ case "$out" in
139
+ *"Missing mechanical floor"*) bad "lane7 worktree: false missing-floor (hooks resolve to the main gitdir)" "$out" ;;
140
+ *"first session for this clone"*) ok "lane7 worktree: hooks resolved correctly, node event reported" ;;
141
+ *) bad "lane7 worktree: silent — a stub would pass this lane" "$out" ;;
142
+ esac
143
+
144
+ # LANE 8 (M3-3) — python3 unavailable: the companion verdict is UNMEASURED, never silently "fine".
145
+ # Simulated by a PATH with no python3, for a Mode D hub (so the check is applicable).
146
+ mk_git_hub "$TMP/nopy" yes
147
+ mkdir -p "$TMP/emptybin" "$TMP/companion2/.git"
148
+ out="$(PATH="$TMP/emptybin:/usr/bin:/bin" run "$TMP/nopy" "$TMP/s8" FH_MACHINE_ID=nopybox BE_DIR="$TMP/companion2")"
149
+ if command -v python3 >/dev/null 2>&1 && [ -x /usr/bin/python3 ]; then
150
+ ok "lane8 skipped: /usr/bin/python3 exists so absence cannot be simulated via PATH"
151
+ else
152
+ case "$out" in
153
+ *UNMEASURED*|*unmeasured*) ok "lane8 no python3: reported UNMEASURED, not silence" ;;
154
+ *) bad "lane8 no python3: absence read as pass" "$out" ;;
155
+ esac
156
+ fi
157
+
158
+ # LANE 9 (S4-2) — the sentinel regex is coupled to PROSE THAT LIVES IN ANOTHER FILE. Every other
159
+ # lane hands it a fixture containing the string it expects, so the suite would stay fully green
160
+ # while a purely cosmetic edit to the real hook headers (which no gate checks) made every FH machine
161
+ # report "not FH's gate" every session. Calibrate the instrument against the shipped article.
162
+ # DERIVE the regex from the script — never retype it. A hardcoded copy is the divergent-copy class
163
+ # this repo already paid for once (SYNC_EXCLUDES in three places, which needed its own parity
164
+ # checker): tighten the script's regex and a duplicated lane keeps validating the SHIPPED hooks
165
+ # against the OLD pattern, staying green while the real check drifts.
166
+ SENT="$(sed -n "s/.*grep -qE '\([^']*\)'.*/\1/p" "$FH_REPO/scripts/fh_node_check.sh" | head -1)"
167
+ if [ -z "$SENT" ]; then
168
+ bad "lane9 sentinel: could not derive the regex from fh_node_check.sh (extraction broke — not a pass)" "empty"
169
+ SENT='__never_matches__'
170
+ fi
171
+ for h in "$FH_REPO"/templates/.git-hooks/pre-commit "$FH_REPO"/templates/.git-hooks/pre-push; do
172
+ if [ ! -f "$h" ]; then bad "lane9 sentinel: shipped hook missing: $h" "absent"; continue; fi
173
+ if grep -qE "$SENT" "$h"; then ok "lane9 sentinel matches shipped $(basename "$h")"
174
+ else bad "lane9 sentinel does NOT match shipped $(basename "$h") — every FH machine would report 'not FH gate'" "$(head -3 "$h")"; fi
175
+ done
176
+
177
+ printf '\nnode-check lanes: %d passed, %d failed\n' "$PASS" "$FAIL"
178
+ [ "$FAIL" -eq 0 ] || exit 1
179
+ exit 0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bash
2
+ # test_sidecar_calibrate_lanes.sh — regression lanes for scripts/sidecar_calibrate.sh.
3
+ #
4
+ # WHY LANES FIRST: the calibrator's whole job is to distinguish states that LOOK the same from the
5
+ # outside — "the sidecar ran" vs "the model I asked for answered", "absent" vs "unmeasured". Those
6
+ # are negative legs, and negative legs are what three adversarial rounds on the node check showed
7
+ # nobody tests until a lane forces it. Each lane below drives the calibrator with a STUB CLI whose
8
+ # behaviour is known, so the calibrator's verdict can be checked against ground truth.
9
+ #
10
+ # No network, no API spend: every sidecar binary is replaced by a stub on PATH.
11
+ #
12
+ # Usage: bash scripts/test_sidecar_calibrate_lanes.sh
13
+ # Exit: 0 = all lanes pass; 1 = at least one failed.
14
+
15
+ set -uo pipefail
16
+
17
+ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
18
+ CAL="$REPO/scripts/sidecar_calibrate.sh"
19
+ TMP="$(mktemp -d)"
20
+ trap 'rm -rf "$TMP"' EXIT
21
+
22
+ PASS=0; FAIL=0
23
+ ok() { PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
24
+ bad() { FAIL=$((FAIL+1)); printf ' ❌ %s\n got: %s\n' "$1" "$(printf '%s' "$2" | tr '\n' '|' | cut -c1-240)"; }
25
+
26
+ # stub <name> <behaviour> — write a fake sidecar CLI onto the lane PATH.
27
+ # honest : echoes back the model it was pinned to (a truthful runtime)
28
+ # silent-fallback: ALWAYS answers as one fixed model, whatever the pin (the agy trap)
29
+ # loud-reject : exits non-zero on an unknown pin (the codex behaviour)
30
+ # chatty : answers with agentic prose instead of the requested token (unparseable verdict)
31
+ # banner-echo : prints a session banner that REPEATS the pinned model, then answers as a
32
+ # different model — the real shape of `codex exec` stdout (measured 2026-07-30)
33
+ # listed-substitute: REJECTS an unknown name (so the bogus control says "rejects-bogus") yet
34
+ # silently serves a different model for a name that IS in its own catalogue —
35
+ # the measured agy shape (`gemini-3.1-pro-high` answered as 3.6 Flash, no error)
36
+ # alias-name : answers with the vendor's PRODUCT name carrying the right version but not every
37
+ # token of the pin slug — measured: pin `gpt-5.6-sol` → "GPT-5.6 Codex"
38
+ # absent : not created at all
39
+ mkstub() {
40
+ local name="$1" mode="$2" bin="$TMP/bin"
41
+ mkdir -p "$bin"
42
+ case "$mode" in
43
+ absent) rm -f "$bin/$name"; return ;;
44
+ esac
45
+ cat > "$bin/$name" <<STUB
46
+ #!/usr/bin/env bash
47
+ mode="$mode"
48
+ pin=""
49
+ prev=""
50
+ for a in "\$@"; do
51
+ case "\$prev" in -m|--model) pin="\$a" ;; esac
52
+ prev="\$a"
53
+ done
54
+ case "\$1" in --version) echo "stub 9.9.9"; exit 0 ;; esac
55
+ # Scan ALL arguments for the verdict prompt — it is NOT always last. codex puts the prompt at the
56
+ # end; agy puts it after -p with `--print-timeout 170s` trailing. A first draft read only the last
57
+ # argument, so the agy-shaped call never looked like a verdict request and lane5b failed against a
58
+ # correct script. An honest runtime ANSWERS THE QUESTION ASKED: a one-token request gets one token.
59
+ is_verdict=0
60
+ for a in "\$@"; do case "\$a" in *"PASS or FAIL"*) is_verdict=1 ;; esac; done
61
+ case "\$mode" in
62
+ loud-reject)
63
+ case "\$pin" in *nonexistent*|*bogus*) echo "ERROR: model '\$pin' is not supported" >&2; exit 1 ;; esac
64
+ if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am \$pin, by StubCorp."; fi ;;
65
+ silent-fallback)
66
+ if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am stub-flash-3.6, by StubCorp."; fi ;;
67
+ alias-name)
68
+ if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "StubGPT-3.1 Coder"; fi ;;
69
+ listed-substitute)
70
+ case "\$pin" in *nonexistent*|*bogus*) echo "ERROR: model '\$pin' is not supported" >&2; exit 1 ;; esac
71
+ if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am stub-flash-3.6, by StubCorp."; fi ;;
72
+ banner-echo)
73
+ echo "Reading additional input from stdin... CLI v0.0.0"
74
+ echo "--------"
75
+ echo "model: \$pin workdir: /tmp approval: never"
76
+ echo "--------"
77
+ if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am stub-flash-3.6, by StubCorp."; fi ;;
78
+ chatty)
79
+ printf 'Summary of Work\n- considered the request\n- the answer is probably PASS\n' ;;
80
+ honest|*)
81
+ if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am \$pin, by StubCorp."; fi ;;
82
+ esac
83
+ exit 0
84
+ STUB
85
+ chmod +x "$bin/$name"
86
+ }
87
+
88
+ # HERMETIC PATH — system paths only, plus the stub dir. Inheriting $PATH looked harmless and was
89
+ # not: "absent" is simulated by NOT creating a stub, so the real codex/agy further down $PATH were
90
+ # found instead and the lanes fired REAL, billed API calls (measured: a lane run hung past 120s
91
+ # against live runtimes). A test that can reach production is not a test.
92
+ run_cal() { PATH="$TMP/bin:/usr/bin:/bin:/usr/sbin:/sbin" bash "$CAL" --stub-model "stub-pro-3.1" "$@" 2>&1; }
93
+
94
+ echo "── sidecar-calibrate lanes ──"
95
+
96
+ # LANE 1 — absent runtime must read as ABSENT, never as a failure of the panel and never as "fine".
97
+ mkstub codex absent; mkstub agy absent
98
+ out="$(run_cal --only codex)"
99
+ case "$out" in
100
+ *"codex"*ABSENT*) ok "lane1 absent runtime reported ABSENT" ;;
101
+ *) bad "lane1 absent runtime not reported as ABSENT" "$out" ;;
102
+ esac
103
+
104
+ # LANE 2 — THE POINT OF THE WHOLE SCRIPT. A runtime that silently answers as a different model must
105
+ # be reported as UNTRUSTED-PIN, not as reachable. "The sidecar ran" and "the model I pinned answered"
106
+ # are different propositions; agy's slug fallback is the measured instance (2026-07-30).
107
+ mkstub agy silent-fallback
108
+ out="$(run_cal --only agy)"
109
+ case "$out" in
110
+ *UNTRUSTED-PIN*) ok "lane2 silent fallback caught (pinned model did not answer)" ;;
111
+ *) bad "lane2 silent fallback passed as a healthy sidecar" "$out" ;;
112
+ esac
113
+
114
+ # LANE 3 — an honest runtime that echoes its pin is PIN-OK.
115
+ mkstub agy honest
116
+ out="$(run_cal --only agy)"
117
+ case "$out" in
118
+ *PIN-OK*) ok "lane3 honest runtime reported PIN-OK" ;;
119
+ *) bad "lane3 honest runtime not reported PIN-OK" "$out" ;;
120
+ esac
121
+
122
+ # LANE 4 — the negative control must actually control. A runtime that ACCEPTS a bogus model pin has
123
+ # no server-side validation, so "it ran without error" proves nothing about which model answered;
124
+ # that must be visible in the report rather than inferred.
125
+ mkstub codex loud-reject
126
+ out="$(run_cal --only codex)"
127
+ case "$out" in
128
+ *"control: rejects-bogus"*) ok "lane4 bogus-pin control observed (runtime validates pins)" ;;
129
+ *) bad "lane4 bogus-pin control not reported" "$out" ;;
130
+ esac
131
+ mkstub codex honest # honest stub answers ANY pin, including a bogus one
132
+ out="$(run_cal --only codex)"
133
+ case "$out" in
134
+ *"control: accepts-bogus"*) ok "lane4b runtime accepting a bogus pin is flagged" ;;
135
+ *) bad "lane4b runtime accepting a bogus pin was not flagged" "$out" ;;
136
+ esac
137
+
138
+ # LANE 5 — verdict fitness is MEASURED, not assumed. A runtime that answers a one-token request with
139
+ # agentic prose cannot carry a machine-read verdict. The recorded 2026-07-04 finding said exactly
140
+ # this about agy at 1.0.14; the version has moved since, so the answer must be re-measured, never
141
+ # inherited.
142
+ mkstub agy chatty
143
+ out="$(run_cal --only agy)"
144
+ case "$out" in
145
+ *VERDICT-UNPARSEABLE*) ok "lane5 prose-answering runtime flagged VERDICT-UNPARSEABLE" ;;
146
+ *) bad "lane5 prose answer accepted as a usable verdict channel" "$out" ;;
147
+ esac
148
+ mkstub agy honest
149
+ out="$(run_cal --only agy)"
150
+ case "$out" in
151
+ *VERDICT-OK*) ok "lane5b token-answering runtime reported VERDICT-OK" ;;
152
+ *) bad "lane5b clean token answer not reported VERDICT-OK" "$out" ;;
153
+ esac
154
+
155
+ # LANE 8 (measured on the first real run) — BANNER ECHO MUST NOT SATISFY THE IDENTITY PROBE.
156
+ # Real CLIs print a session banner before the answer, and that banner repeats the pinned model name
157
+ # back (`codex exec` prints its version and config header). Matching against whole stdout therefore
158
+ # passes any runtime that merely echoes its own configuration — which is precisely the lie this
159
+ # script exists to catch, re-entering through the transport layer instead of the model.
160
+ mkstub agy banner-echo
161
+ out="$(run_cal --only agy)"
162
+ case "$out" in
163
+ *UNTRUSTED-PIN*) ok "lane8 banner echo rejected (config echo is not a self-report)" ;;
164
+ *) bad "lane8 banner echoing the pin passed the identity probe" "$out" ;;
165
+ esac
166
+
167
+ # LANE 10 (measured 2026-07-30) — a vendor product alias carrying the RIGHT VERSION is not a pin
168
+ # failure. Pinning `gpt-5.6-sol` returned "GPT-5.6 Codex": same family, same version, different
169
+ # product suffix. Requiring every token of the pin slug to appear made the calibrator report
170
+ # UNTRUSTED-PIN on a runtime whose pin had actually held — and that runtime also validates pins
171
+ # server-side, so the corroborating evidence was there to read. The version token is the
172
+ # discriminator that matters; the fallback cases (3.1 asked, 3.6 answered) differ exactly there.
173
+ mkstub agy alias-name
174
+ out="$(run_cal --only agy)"
175
+ case "$out" in
176
+ *PIN-OK*) ok "lane10 product alias with matching version accepted as PIN-OK" ;;
177
+ *) bad "lane10 matching version rejected because a suffix token was absent" "$out" ;;
178
+ esac
179
+
180
+ # LANE 9 (measured 2026-07-30) — "rejects unknown names" does NOT imply "serves known names
181
+ # faithfully", and the gap between them is where the real trap lives. agy rejects a nonsense model
182
+ # yet silently substituted Flash for `gemini-3.1-pro-high`, a slug from its OWN catalogue. A control
183
+ # that only probes nonsense therefore returns reassuring evidence about the wrong question: the
184
+ # identity probe must still decide, and the report must not let `rejects-bogus` read as "pin safe".
185
+ mkstub agy listed-substitute
186
+ out="$(run_cal --only agy)"
187
+ case "$out" in
188
+ *"control: rejects-bogus"*UNTRUSTED-PIN*|*UNTRUSTED-PIN*"control: rejects-bogus"*)
189
+ ok "lane9 validates unknown names yet substitutes a known one → still UNTRUSTED-PIN" ;;
190
+ *) bad "lane9 pin substitution masked by a passing bogus-control" "$out" ;;
191
+ esac
192
+
193
+ # LANE 6 — panel verdict. With no usable different-family sidecar the script must say so explicitly:
194
+ # this is the line a marker's `crossfamily:` leg quotes, and the 2026-07-30 defect was asserting
195
+ # "cross-family unavailable" without ever probing.
196
+ mkstub codex absent; mkstub agy absent
197
+ out="$(run_cal)"
198
+ case "$out" in
199
+ *"PANEL: none"*) ok "lane6 empty panel stated explicitly (not silence)" ;;
200
+ *) bad "lane6 empty panel not stated" "$out" ;;
201
+ esac
202
+ mkstub codex honest
203
+ out="$(run_cal)"
204
+ case "$out" in
205
+ *"PANEL: "*codex*) ok "lane6b populated panel names the usable runtime" ;;
206
+ *) bad "lane6b populated panel not named" "$out" ;;
207
+ esac
208
+
209
+ # LANE 7 — detector, not gate: always exit 0, so a calibration run can never block a caller. The
210
+ # caller reads the verdict; the script does not decide for it.
211
+ mkstub codex absent; mkstub agy absent
212
+ PATH="$TMP/bin:/usr/bin:/bin:/usr/sbin:/sbin" bash "$CAL" >/dev/null 2>&1
213
+ [ $? -eq 0 ] && ok "lane7 exits 0 even with an empty panel (detector, not gate)" \
214
+ || bad "lane7 non-zero exit on empty panel" "exit=$?"
215
+
216
+ printf '\nsidecar-calibrate lanes: %d passed, %d failed\n' "$PASS" "$FAIL"
217
+ [ "$FAIL" -eq 0 ] || exit 1
218
+ exit 0
@@ -0,0 +1,54 @@
1
+ {
2
+ "_README": [
3
+ "SessionStart hook snippet — TRACKED, so it arrives with a clone. Every .claude/settings*.json",
4
+ "path in this repo is gitignored (confirm with `git check-ignore -v .claude/settings.json`; do",
5
+ "not cite a line number, they move), so a hook registration cannot itself be tracked. This is the tracked",
6
+ "SOURCE that install-wizard merges into the user's local settings. Without the merge step the",
7
+ "hooks do not run — shipping the file is not the same proposition as wiring it.",
8
+ "",
9
+ "Two entries, deliberately split by privacy:",
10
+ " - fh_node_check.sh → NO private path. Belongs in .claude/settings.json (project-local).",
11
+ " - fh_session_load.sh + fh_env_delta_scan.sh → carry BE_DIR (an operator-private companion",
12
+ " store path), so they belong ONLY in .claude/settings.local.json, and only for Mode D users.",
13
+ "",
14
+ "Merge, do not overwrite: preserve any SessionStart entries the user already has; replace only",
15
+ "the FH ones, keyed by script name."
16
+ ],
17
+ "project_settings_json": {
18
+ "hooks": {
19
+ "SessionStart": [
20
+ {
21
+ "matcher": "",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/fh_node_check.sh\"",
26
+ "timeout": 10
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }
32
+ },
33
+ "settings_local_json_MODE_D_ONLY": {
34
+ "hooks": {
35
+ "SessionStart": [
36
+ {
37
+ "matcher": "",
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "BE_DIR=\"<absolute path to your companion store>\" bash \"$CLAUDE_PROJECT_DIR/scripts/fh_session_load.sh\"",
42
+ "timeout": 20
43
+ },
44
+ {
45
+ "type": "command",
46
+ "command": "BE_DIR=\"<absolute path to your companion store>\" bash \"$CLAUDE_PROJECT_DIR/scripts/fh_env_delta_scan.sh\"",
47
+ "timeout": 15
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ }