@chrono-meta/fh-gate 1.4.76 → 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,184 @@
1
+ #!/usr/bin/env bash
2
+ # fh_node_check.sh — per-NODE environment floor check, fired at SessionStart.
3
+ #
4
+ # WHY A NODE-SCOPED CHECK EXISTS:
5
+ # A user's context (companion store, memory, session card) travels between machines; the machine's
6
+ # own wiring does not. A rich context makes an unwired laptop read as "already configured".
7
+ # Measured 2026-07-30: a machine holding the full companion store and memory ran sessions with its
8
+ # SessionStart hooks unregistered; nothing surfaced it — it was found by accident.
9
+ #
10
+ # WHY IT IS NOT INSIDE fh_session_load.sh:
11
+ # That script is registered in the gitignored .claude/settings.local.json, so on a fresh clone it
12
+ # is not registered — the situation this check exists for is the one in which it could not run.
13
+ # This script carries no operator-private path, so it can be registered from
14
+ # templates/settings.SessionStart.snippet.json, which IS tracked and ships with a clone.
15
+ # HONEST SCOPE: registration still requires the wizard to merge that snippet — every
16
+ # .claude/settings*.json path in this repo is gitignored, so no SessionStart entry can be tracked.
17
+ # The chicken-and-egg is REDUCED (script + snippet ship), not ELIMINATED (a user who never runs
18
+ # the wizard still gets nothing). An earlier revision of this header claimed "survives a clone";
19
+ # that was false, and a cross-family review caught it before merge. Do not restore the claim
20
+ # without running `git check-ignore` on the settings paths first.
21
+ #
22
+ # EMISSION IS STATE-BASED, NOT EVENT-BASED — this is the load-bearing design decision:
23
+ # A missing floor is a PERSISTENT CONDITION, so it is reported EVERY session until fixed.
24
+ # A healthy machine is silent AFTER its one event line (first session / machine change / infra
25
+ # delta) — events report once, conditions report until they stop being true.
26
+ # Earlier revisions fired on events only (first session / machine change / idle >= 7 days / HEAD
27
+ # advanced >= 20 commits) and that was wrong in both directions: on this hub's measured velocity
28
+ # (~33 commits/week) the commit axis fired every ~4 days on a healthy machine (noise, and an
29
+ # ignored detector cannot be revived), while a broken machine reported once then went quiet
30
+ # forever — reproducing the very accident above.
31
+ #
32
+ # Detector, never a gate: always exits 0, and it RECOMMENDS — it cannot compel.
33
+ # State: tracks/_meta/.fh_node_state (gitignored; excluded from companion sync — it is machine-local
34
+ # by nature, and mirroring it would make node identity flap between machines).
35
+ # FH_NODE_STATE overrides the state path (used by the wizard's verification so that verifying does
36
+ # not consume a one-shot event report).
37
+
38
+ set -uo pipefail
39
+
40
+ FH="${HUB_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}"
41
+ STATE="${FH_NODE_STATE:-$FH/tracks/_meta/.fh_node_state}"
42
+
43
+ NODE_ID="${FH_MACHINE_ID:-$(hostname -s 2>/dev/null || echo unknown)}"
44
+ HEAD_NOW="$(git -C "$FH" rev-parse --short HEAD 2>/dev/null || echo none)"
45
+ NOW="$(date +%s)"
46
+
47
+ PREV_ID=""; PREV_EPOCH=0; PREV_HEAD=""
48
+ if [ -f "$STATE" ]; then
49
+ IFS='|' read -r PREV_ID PREV_EPOCH PREV_HEAD < "$STATE" 2>/dev/null || true
50
+ fi
51
+ # Never feed a file-sourced value straight into arithmetic (bash arithmetic evaluates command
52
+ # substitution inside array subscripts).
53
+ case "${PREV_EPOCH:-}" in ''|*[!0-9]*) PREV_EPOCH=0 ;; esac
54
+
55
+ # ── floor probes — always run, before any decision about whether to speak ──────
56
+ MISS=""
57
+
58
+ # ① git-side floor. Probe the EXECUTABLE HOOK, not the config key: `core.hooksPath` unset is a
59
+ # normal working install when hooks live in .git/hooks, and a set-but-empty path is a broken
60
+ # install the key alone reports as fine. Resolve the directory with `git rev-parse --git-path`,
61
+ # which handles set/unset, relative/absolute, AND linked worktrees (where .git is a file, so a
62
+ # hand-built "$FH/.git/hooks" is simply wrong — FH runs worktree-isolated agents, so that path
63
+ # is reachable, not hypothetical).
64
+ if ! git -C "$FH" rev-parse --git-dir >/dev/null 2>&1; then
65
+ # NOT a git repo (plugin-only / marketplace install, or a non-repo directory). A git hook cannot
66
+ # be installed here at all, so this floor is N/A — not missing. Applicability is decided
67
+ # mechanically, per CLAUDE.md §Irreversibility Surface-Class: reporting it would print an
68
+ # UNFIXABLE notice every session (emission is state-based), training the reader to ignore the
69
+ # one check that must not be ignored.
70
+ :
71
+ else
72
+ # --path-format needs git >= 2.31; fall back to the relative form resolved against the repo root
73
+ # (still correct in a linked worktree, where a hand-built "$FH/.git/hooks" does not exist at all).
74
+ HD="$(git -C "$FH" rev-parse --path-format=absolute --git-path hooks 2>/dev/null)"
75
+ if [ -z "$HD" ]; then
76
+ _rel="$(git -C "$FH" rev-parse --git-path hooks 2>/dev/null || echo .git/hooks)"
77
+ case "$_rel" in /*) HD="$_rel" ;; *) HD="$(git -C "$FH" rev-parse --show-toplevel 2>/dev/null || echo "$FH")/$_rel" ;; esac
78
+ fi
79
+ for h in pre-commit pre-push; do
80
+ if [ ! -x "$HD/$h" ]; then
81
+ MISS="${MISS}no executable ${h} hook · "
82
+ elif ! grep -qE 'fh-gate|FH .*Gate|4-Axis|4축' "$HD/$h" 2>/dev/null; then
83
+ # Executable is not the same proposition as OURS. husky and pre-commit-framework are standard
84
+ # equipment in the JS/Python projects FH maps, and they install an executable pre-commit that
85
+ # runs a linter — under an executable-only probe such a machine reports "floors present" and
86
+ # then goes silent, which is precisely the accident this check exists to prevent.
87
+ MISS="${MISS}${h} hook present but not FH's gate (another framework owns it) · "
88
+ fi
89
+ done
90
+ fi
91
+
92
+ # ② companion-load hook (Mode D only). Reported as INFORMATION, never as a missing floor: this hook
93
+ # is registered for ALL users, and a public non-Mode-D user has no companion store to load, so
94
+ # listing it under ❌ would be a false positive for the majority path.
95
+ # APPLICABILITY: "is this a Mode D user", NOT "does settings.local.json exist". Keying on that
96
+ # file was wrong in the worst possible way — it is gitignored, so a FRESH CLONE never has it, and
97
+ # a fresh clone with a full companion store is EXACTLY the measured 2026-07-30 incident. The gate
98
+ # silenced its own flagship case. Mode D signals that survive a clone: an exported BE_DIR, or the
99
+ # operator's CLAUDE.local.md binding.
100
+ COMPANION_NOTE=""
101
+ _IS_MODE_D=""
102
+ { [ -n "${BE_DIR:-}" ] && [ -d "$BE_DIR" ]; } && _IS_MODE_D=1
103
+ # CLAUDE.local.md is Claude Code's STANDARD local-override file — anyone may have one for any
104
+ # reason, and having one says nothing about a companion store. Keying on its EXISTENCE re-admitted
105
+ # the majority-path false positive through a second door, and state-based emission made it permanent
106
+ # rather than one-shot (cross-family review 2026-07-30). So key on the file MENTIONING a companion
107
+ # binding instead.
108
+ # The vocabulary spans every backend the wizard documents — the store is a ROLE, not a repo layout
109
+ # (Obsidian vault · gbrain ingest target · *-be repo all qualify), and an FH-flavoured regex would
110
+ # have silently excluded two first-class backends: the same "flagship case goes quiet" shape as the
111
+ # fresh-clone defect, one door over.
112
+ # HONEST SCOPE: this is a MENTION test, not a semantic one. "I do not use a companion store" also
113
+ # matches. The cost of that over-match is one informational line, never a floor claim — deliberately
114
+ # the cheap direction, since the expensive direction is silence.
115
+ [ -f "$FH/CLAUDE.local.md" ] \
116
+ && grep -qiE 'BE_DIR|companion[ -]store|컴패니언|vault|gbrain|obsidian' "$FH/CLAUDE.local.md" 2>/dev/null \
117
+ && _IS_MODE_D=1
118
+ if [ -n "$_IS_MODE_D" ] && ! command -v python3 >/dev/null 2>&1; then
119
+ # not found ≠ 0: without a JSON parser the registration verdict is unknown, not clean.
120
+ COMPANION_NOTE="companion-load registration UNMEASURED (no python3 — cannot parse the hook config; do not read this as 'registered')"
121
+ elif [ -n "$_IS_MODE_D" ]; then
122
+ python3 - "$FH" <<'PY' || COMPANION_NOTE="companion-load SessionStart not registered (Mode D — freshness + env-delta will not fire at turn 0)"
123
+ import json, os, sys
124
+ hub = sys.argv[1]
125
+ for p in (os.path.join(hub, ".claude", "settings.local.json"),
126
+ os.path.expanduser("~/.claude/settings.json")):
127
+ try:
128
+ groups = json.load(open(p)).get("hooks", {}).get("SessionStart", [])
129
+ except Exception:
130
+ continue
131
+ if any("fh_session_load" in h.get("command", "") for g in groups for h in g.get("hooks", [])):
132
+ sys.exit(0)
133
+ sys.exit(1)
134
+ PY
135
+ fi
136
+
137
+ # ── event: infra delta since the commit this clone last saw ───────────────────
138
+ # Reported ONCE per pull (it is an event). Distinguishes "no change" from "could not measure".
139
+ INFRA=""; INFRA_NOTE=""
140
+ if [ -n "$PREV_HEAD" ] && [ "$PREV_HEAD" != "$HEAD_NOW" ]; then
141
+ if INFRA_RAW="$(git -C "$FH" diff --name-only "${PREV_HEAD}..HEAD" 2>/dev/null)"; then
142
+ INFRA="$(printf '%s\n' "$INFRA_RAW" \
143
+ | grep -E '^(templates/\.git-hooks/|templates/settings\.|scripts/fh_|plugins/[^/]+/skills/install-(wizard|doctor)/)' \
144
+ | head -6)"
145
+ else
146
+ INFRA_NOTE="UNMEASURED — cannot reach the previously seen commit ($PREV_HEAD), so the infra delta was not computed (rebase, shallow clone, or GC). Not the same as 'nothing changed'."
147
+ fi
148
+ fi
149
+
150
+ IDENTITY=""
151
+ if [ -z "$PREV_ID" ]; then IDENTITY="first session for this clone"
152
+ elif [ "$PREV_ID" != "$NODE_ID" ]; then IDENTITY="machine changed ($PREV_ID → $NODE_ID)"; fi
153
+
154
+ # ── state write — unconditional, and a failure is reported, never swallowed ────
155
+ # Writing only when the check speaks would make the recorded timestamp mean "last time it spoke",
156
+ # and a write failure would make this banner repeat forever with no explanation.
157
+ STATE_WARN=""
158
+ if ! { mkdir -p "$(dirname "$STATE")" 2>/dev/null \
159
+ && printf '%s|%s|%s' "$NODE_ID" "$NOW" "$HEAD_NOW" > "$STATE" 2>/dev/null; }; then
160
+ STATE_WARN="could not record node state ($STATE) — this notice may repeat every session"
161
+ fi
162
+
163
+ # ── emit: condition (every session) OR event (once) ───────────────────────────
164
+ [ -n "$MISS$COMPANION_NOTE$INFRA$INFRA_NOTE$IDENTITY$STATE_WARN" ] || exit 0
165
+
166
+ if [ -n "$MISS" ]; then
167
+ echo "🖥️ [node] Missing mechanical floor on this machine (node: $NODE_ID): ${MISS% · }"
168
+ echo " → Run /install-doctor, then /install-wizard. A rich context (memory, companion store)"
169
+ echo " is a different proposition from this machine being wired."
170
+ elif [ -n "$IDENTITY" ]; then
171
+ echo "🖥️ [node] $IDENTITY (node: $NODE_ID) — floors present."
172
+ fi
173
+ [ -n "$STATE_WARN" ] && echo " ⚠️ $STATE_WARN"
174
+ [ -n "$COMPANION_NOTE" ] && echo " ℹ️ $COMPANION_NOTE"
175
+ if [ -n "$INFRA_NOTE" ]; then
176
+ echo " ⚠️ $INFRA_NOTE"
177
+ elif [ -n "$INFRA" ]; then
178
+ echo " 🆕 Install-relevant assets changed since this clone last ran — being registered is not"
179
+ echo " the same as being current, and a pull moves files without wiring hooks:"
180
+ printf '%s\n' "$INFRA" | while IFS= read -r f; do [ -n "$f" ] && echo " - $f"; done
181
+ echo " → Re-run /install-wizard (idempotent)."
182
+ fi
183
+
184
+ exit 0
@@ -27,6 +27,51 @@
27
27
  set -uo pipefail
28
28
 
29
29
  FH="${HUB_DIR:-${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}}"
30
+ BE="${BE_DIR:-}" # companion-store path — supplied by the gitignored hook registration; no public default.
31
+ # Resolved HERE (not at the Mode-D block below) because the frontier-digest check
32
+ # needs it: on a multi-node setup the digest producer may be a DIFFERENT machine.
33
+
34
+ # ── node re-entry floor check ────────────────────────────────────────────────
35
+ # 이 검사는 scripts/fh_node_check.sh 로 분리했다. 이유: 이 파일(fh_session_load.sh)은 gitignored
36
+ # settings.local.json 에 등록되므로 새 클론/새 기계에선 애초에 안 돈다 — 검사가 존재 이유가 되는
37
+ # 상황에서 도달 불가였다(Sonnet 타깃-티어 심 2026-07-30 지적).
38
+ # ⚠️ 분리해도 자동 배선은 아니다: .claude/settings.json 도 gitignored 라(.gitignore:3-4) 등록 자체는
39
+ # 추적될 수 없다. 추적되는 건 templates/settings.SessionStart.snippet.json 이고, 배선은 위자드가 한다.
40
+ # 여기서 다시 호출하지 않는다: 두 곳에서 부르면 같은 이벤트를 두 번 찍는다.
41
+
42
+ # ── §early-refresh: 컴패니언 refresh 를 frontier 판정보다 먼저 한다 ─────────────
43
+ # 왜 순서가 문제인가: frontier 판정은 러너가 아닌 노드에서 $BE/tracks-meta 를 본다. refresh 가
44
+ # 그 뒤에 있으면 **그날의 첫 세션**은 아직 안 끌어온 워킹트리를 읽어 오늘 digest 를 못 보고,
45
+ # 이 수정이 없애려던 바로 그 오경보를 그대로 낸다(하루 한 번, 가장 값진 시점에서 실패).
46
+ # cross-family 리뷰 2026-07-30 [HIGH] 지적. 비-Mode-D(공개) 사용자는 BE 가 비어 통째로 건너뛴다.
47
+ PULL_NOTE="(no companion store configured)"
48
+ if [ -d "$BE/.git" ]; then
49
+ export GIT_TERMINAL_PROMPT=0
50
+ export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new}"
51
+
52
+ # Hard wall-clock deadline on the ONLY network step. ConnectTimeout bounds the handshake, but a
53
+ # slow/stalled TRANSFER after connect has no bound — measured 2026-07-12: SessionStart worst-case
54
+ # 17.5s with 2 hook-timeout kills, all attributable to the fetch. perl-alarm is the portable
55
+ # watchdog (macOS ships no coreutils `timeout`); on overrun the fetch dies and the offline branch
56
+ # reports honestly. FH_FETCH_DEADLINE overrides (seconds). If perl is absent the wrapper degrades
57
+ # to running the command with NO deadline — a missing watchdog must never become a permanently
58
+ # skipped fetch misreported as "offline" (challenger catch 2026-07-12).
59
+ if command -v perl >/dev/null 2>&1; then
60
+ _deadline() { perl -e 'alarm shift @ARGV; exec @ARGV' "$@"; }
61
+ else
62
+ _deadline() { shift; "$@"; }
63
+ fi
64
+ PULL_NOTE=""
65
+ if _deadline "${FH_FETCH_DEADLINE:-8}" git -C "$BE" fetch --quiet >/dev/null 2>&1; then
66
+ if git -C "$BE" merge --ff-only --quiet >/dev/null 2>&1; then
67
+ PULL_NOTE="fetched + fast-forwarded"
68
+ else
69
+ PULL_NOTE="fetched but NOT fast-forward (companion diverged — read local + newest remote)"
70
+ fi
71
+ else
72
+ PULL_NOTE="fetch skipped (offline or deadline hit — read local state)"
73
+ fi
74
+ fi
30
75
 
31
76
  # ── frontier-digest: 부재를 0으로 읽지 않는다 ────────────────────────────────────
32
77
  # 왜: digest 는 launchd 로 매일 09:00 에 돌지만 **31회 중 6회(19%) 산출물 없이 끝났다**
@@ -51,9 +96,19 @@ _FD_STAMP="$(date +%H:%M) 기준"
51
96
  # 존재 판정은 러너 digest_ready 와 동일 술어(glob + -size +1k). 정확명 [ -f ] 는 러너와 관대함이
52
97
  # 갈린다 — partial 파일(>0 <1k)이 성공으로 오독되고, suffix 착지가 영구 오경보가 된다
53
98
  # (divergent-leniency: 같은 상태를 두 술어가 다르게 읽으면 한쪽 결과가 무음으로 샌다).
54
- _fd_ready() { find "$FH/tracks/_meta" -maxdepth 1 -name "frontier_digest_$(date +%Y_%m_%d)*.md" -size +1k 2>/dev/null | grep -q .; }
99
+ _fd_hit() { find "$1" -maxdepth 1 -name "frontier_digest_$(date +%Y_%m_%d)*.md" -size +1k 2>/dev/null | grep -q .; }
100
+ # 노드-로컬만 보면 **다른 머신이 만든 산출물이 구조적으로 안 보인다**. 멀티머신에선 러너가 한 대이고
101
+ # 나머지 노드는 컴패니언 스토어로만 그 산출물을 받는다 → 러너 아닌 노드가 매일 "실패다" 오경보를 낸다.
102
+ # (2026-07-30 실측: 프로가 07-24~30 매일 정상 생산 중인데 에어는 7일 연속 FAILED 를 띄웠다.
103
+ # 계기의 스코프가 대상보다 좁았던 케이스 — 대상은 '오늘 digest 가 있나'지 '이 디스크에 있나'가 아니다.)
104
+ # 술어는 로컬과 **동일**(glob + -size +1k) — divergent-leniency 를 만들지 않는다.
105
+ _fd_ready() { _fd_hit "$FH/tracks/_meta" || { [ -n "$BE" ] && _fd_hit "$BE/tracks-meta"; }; }
55
106
  if _fd_ready; then
56
- :
107
+ # 로컬엔 없고 컴패니언에만 있으면 = 이 노드는 러너가 아니다. 침묵하면 토폴로지가 안 보이므로 한 줄 알린다.
108
+ if ! _fd_hit "$FH/tracks/_meta"; then
109
+ echo "ℹ️ [frontier-digest] 오늘 digest 는 **다른 노드**가 생산했다(컴패니언 스토어 경유) — 이 노드는 러너가 아니다."
110
+ echo " 읽을 것: \$BE_DIR/tracks-meta/frontier_digest_$(date +%Y_%m_%d)*.md"
111
+ fi
57
112
  elif [ "$((10#$_FD_NOW))" -lt "$_FD_SCHED" ]; then
58
113
  echo "ℹ️ [frontier-digest] 오늘 digest 는 09:00 예정 — 아직 전이다($_FD_STAMP). 부재는 정상."
59
114
  elif [ -f "$_FD_LOG" ]; then
@@ -77,7 +132,7 @@ elif [ -f "$_FD_LOG" ]; then
77
132
  else
78
133
  echo "⚠️ [frontier-digest] 스케줄(09:00) 지났는데 로그도 산출물도 없다($_FD_STAMP) — 잡이 아예 안 돌았을 수 있다(launchd 확인)."
79
134
  fi
80
- BE="${BE_DIR:-}" # companion-store path supplied by the gitignored hook registration; no public default.
135
+ # (BE is resolved at the top of this script — the frontier-digest block above needs it too.)
81
136
 
82
137
  # Non-Mode-D / no companion store → silent no-op (this is the majority path for public users).
83
138
  [ -d "$BE/.git" ] || exit 0
@@ -89,31 +144,7 @@ BE="${BE_DIR:-}" # companion-store path — supplied by the gitignored hook re
89
144
  # - fail-fast env: no credential/SSH/host-key prompts can hang SessionStart.
90
145
  # - fetch + merge --ff-only: a fast-forward is the only safe hook mutation; a diverged companion
91
146
  # simply does not advance (no merge commit, no conflict state left behind) and we say so.
92
- export GIT_TERMINAL_PROMPT=0
93
- export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new}"
94
-
95
- # Hard wall-clock deadline on the ONLY network step. ConnectTimeout bounds the handshake, but a
96
- # slow/stalled TRANSFER after connect has no bound — measured 2026-07-12: SessionStart worst-case
97
- # 17.5s with 2 hook-timeout kills, all attributable to the fetch. perl-alarm is the portable
98
- # watchdog (macOS ships no coreutils `timeout`); on overrun the fetch dies and the offline branch
99
- # reports honestly. FH_FETCH_DEADLINE overrides (seconds). If perl is absent the wrapper degrades
100
- # to running the command with NO deadline — a missing watchdog must never become a permanently
101
- # skipped fetch misreported as "offline" (challenger catch 2026-07-12).
102
- if command -v perl >/dev/null 2>&1; then
103
- _deadline() { perl -e 'alarm shift @ARGV; exec @ARGV' "$@"; }
104
- else
105
- _deadline() { shift; "$@"; }
106
- fi
107
- PULL_NOTE=""
108
- if _deadline "${FH_FETCH_DEADLINE:-8}" git -C "$BE" fetch --quiet >/dev/null 2>&1; then
109
- if git -C "$BE" merge --ff-only --quiet >/dev/null 2>&1; then
110
- PULL_NOTE="fetched + fast-forwarded"
111
- else
112
- PULL_NOTE="fetched but NOT fast-forward (companion diverged — read local + newest remote)"
113
- fi
114
- else
115
- PULL_NOTE="fetch skipped (offline or deadline hit — read local state)"
116
- fi
147
+ # (컴패니언 refresh 는 위 §early-refresh 로 올렸다 — frontier 판정이 최신 트리를 보게 하려고.)
117
148
 
118
149
  # 2) Session card date (the pointer the operator's close chain writes last).
119
150
  CARD="$FH/tracks/_meta/reference_next_session_starter.md"
@@ -43,6 +43,10 @@ fi
43
43
  # no shipped hook invokes it.
44
44
  ACCEPTED_ABSENT=(
45
45
  ".claude/registry/LOCAL_SKILL_REGISTRY.md"
46
+ # An INSTALL DESTINATION the user creates (`cp templates/local_fh_context.md
47
+ # .claude/rules/local_fh_context.md`), not a file FH ships. Shipping it would overwrite the
48
+ # user's own cross-context wiring — the template it is copied FROM is what ships.
49
+ ".claude/rules/local_fh_context.md"
46
50
  ".claude/regression/probes.md"
47
51
  "scripts/sync-to-be.sh"
48
52
  "scripts/sync_guard_check.sh"
@@ -83,6 +87,24 @@ for s in shipped:
83
87
  # Only a path that REALLY EXISTS here but is left out of the tarball is this defect.
84
88
  # A path that exists nowhere is the ordinary phantom-reference class the ref-path
85
89
  # check above already owns; a path outside files[] that is also absent is nothing.
90
+ # EXISTENCE, not tracked-ness. A 2026-07-30 revision narrowed this to `git ls-files`
91
+ # to silence what looked like a machine-local false positive; measurement showed that was a
92
+ # WEAKENING — an existing-but-untracked path named by a shipped doc is exactly the defect
93
+ # (the npm user cannot have that file), and selfcheck's ref-path check SKIPs gitignored
94
+ # paths, so nothing else owns it. Reverted.
95
+ #
96
+ # WIDENING IS DEFERRED, AND THE REASON IS NOT A MEASUREMENT. Dropping `exists` entirely
97
+ # (flag every referenced ∧ ¬covered path) is arguably the correct predicate, but it cannot
98
+ # be evaluated while the extractor below is known-broken: its `(sh|py|js|md|json|…)`
99
+ # alternation puts `js` before `json`, so `settings.json` is captured as `settings.js`.
100
+ # A first pass at this comment cited a count of artifacts as evidence that `exists` is
101
+ # load-bearing — that count came FROM the broken extractor, i.e. an instrument was used to
102
+ # justify keeping a predicate before the instrument itself was validated (the circularity
103
+ # CLAUDE.md §Instrument-Calibration exists to forbid; a cross-family review caught it, and
104
+ # an independent extractor produced materially different numbers).
105
+ # HONEST STATE: fix the `js|json` alternation first, re-measure, then decide. Until then
106
+ # this check's true coverage is UNQUANTIFIED — treat a PASS as "no defect of the narrow
107
+ # exists-and-uncovered kind", not as "every shipped reference is sound".
86
108
  if os.path.exists(m) and not covered(m):
87
109
  if m in accepted:
88
110
  exercised.add(m)
@@ -132,6 +132,51 @@ fi
132
132
  # prevent. test_card_drift_probe.sh had shipped with ZERO callers since it was written; wiring it
133
133
  # here closes that, and the anchors are added to files[] in the same change so package mode runs
134
134
  # them too rather than reporting a deleted anchor.
135
+ # sidecar_wait stdin plumbing. Its subject is dispatched by auto-decorrelation / steel-quench /
136
+ # sim-conductor / AGENTS.md as the REQUIRED wait form, so a regression there silently empties every
137
+ # cross-family verification. The anchor's first version shipped in tests/ with ZERO callers — the
138
+ # same defect the comment above records for test_card_drift_probe.sh, repeated one file later.
139
+ if [ ! -f scripts/sidecar_wait.sh ]; then
140
+ echo "SKIP test_sidecar_wait_stdin.sh (subject scripts/sidecar_wait.sh absent)"
141
+ elif [ -f scripts/test_sidecar_wait_stdin.sh ]; then
142
+ if ! bash scripts/test_sidecar_wait_stdin.sh; then
143
+ fail=1
144
+ fi
145
+ else
146
+ echo "FAIL test_sidecar_wait_stdin.sh: sidecar_wait.sh present but its anchor is missing"
147
+ fail=1
148
+ fi
149
+
150
+ # fh_node_check.sh gets the same treatment, and for the same reason: three adversarial rounds on it
151
+ # produced defects that were ALL negative legs (floor N/A on a non-git install · another framework's
152
+ # hooks counted as ours · the Mode D applicability gate silencing its own flagship case), and each
153
+ # round's fix reverted a previous one because no anchor pinned it. Subject-present-but-anchor-absent
154
+ # is a FAIL, not a skip — that is how an anchor gets quietly dropped.
155
+ # Same treatment for the sidecar calibrator, same reason: its verdicts are all distinctions between
156
+ # states that look identical from outside ("the sidecar ran" vs "the model I pinned answered",
157
+ # "absent" vs "unmeasured"), and its lanes are hermetic stubs, so running them costs nothing.
158
+ if [ ! -f scripts/sidecar_calibrate.sh ]; then
159
+ echo "SKIP test_sidecar_calibrate_lanes.sh (subject scripts/sidecar_calibrate.sh absent)"
160
+ elif [ -f scripts/test_sidecar_calibrate_lanes.sh ]; then
161
+ if ! bash scripts/test_sidecar_calibrate_lanes.sh; then
162
+ fail=1
163
+ fi
164
+ else
165
+ echo "FAIL test_sidecar_calibrate_lanes.sh: sidecar_calibrate.sh present but its anchor is missing"
166
+ fail=1
167
+ fi
168
+
169
+ if [ ! -f scripts/fh_node_check.sh ]; then
170
+ echo "SKIP test_node_check_lanes.sh (subject scripts/fh_node_check.sh absent)"
171
+ elif [ -f scripts/test_node_check_lanes.sh ]; then
172
+ if ! bash scripts/test_node_check_lanes.sh; then
173
+ fail=1
174
+ fi
175
+ else
176
+ echo "FAIL test_node_check_lanes.sh: fh_node_check.sh present but its anchor is missing"
177
+ fail=1
178
+ fi
179
+
135
180
  for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh; do
136
181
  if [ ! -f scripts/session_close_check.sh ]; then
137
182
  echo "SKIP ${_anchor##*/} (subject scripts/session_close_check.sh absent)"
@@ -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