@chrono-meta/fh-gate 1.4.77 → 1.4.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.claude/rules/fh_4axis_gate.md +63 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/AGENTS.md +96 -260
  4. package/CLAUDE.md +2 -7
  5. package/docs/codex-compat.md +4 -1
  6. package/knowledge/shared/harness-core/agents_md_runtime_details.md +233 -0
  7. package/knowledge/shared/harness-core/loop_engineering.md +1 -1
  8. package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +1 -1
  9. package/knowledge/shared/learnings/subagent_invocations_log.yaml +14 -0
  10. package/knowledge/shared/rules/operational_adaptation.md +1 -130
  11. package/package.json +14 -5
  12. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  13. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  14. package/plugins/fh-meta/skills/install-doctor/SKILL.md +88 -0
  15. package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
  16. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +117 -3
  17. package/scripts/fh_node_check.sh +184 -0
  18. package/scripts/fh_session_load.sh +101 -31
  19. package/scripts/halffix_propagation_scan.sh +126 -0
  20. package/scripts/package_coverage_check.sh +40 -1
  21. package/scripts/pipe_verdict_guard.sh +93 -0
  22. package/scripts/selfcheck.sh +114 -21
  23. package/scripts/session_close_check.sh +15 -1
  24. package/scripts/sidecar_calibrate.sh +275 -0
  25. package/scripts/test_card_drift_probe.sh +55 -0
  26. package/scripts/test_halffix_lanes.sh +170 -0
  27. package/scripts/test_node_check_lanes.sh +179 -0
  28. package/scripts/test_ollama_panel_lanes.sh +120 -0
  29. package/scripts/test_package_coverage_lanes.sh +250 -0
  30. package/scripts/test_pipe_verdict_guard_lanes.sh +96 -0
  31. package/scripts/test_sidecar_calibrate_lanes.sh +218 -0
  32. package/scripts/test_sidecar_wait_stdin.sh +13 -1
  33. package/templates/.git-hooks/pre-commit +9 -0
  34. package/templates/settings.PreToolUse.snippet.json +49 -0
  35. package/templates/settings.SessionStart.snippet.json +54 -0
  36. package/scripts/consent_registry_check.sh +0 -390
  37. package/scripts/test_consent_registry.sh +0 -255
  38. package/templates/consent_classes.yaml.example +0 -75
@@ -21,10 +21,27 @@ set -uo pipefail
21
21
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
22
22
  cd "$REPO_ROOT" || exit 1
23
23
 
24
- if [ ! -d .git ] || [ ! -f package.json ]; then
24
+ # Source-checkout test uses `-e`, not `-d`. In a git WORKTREE `.git` is a FILE (a gitdir pointer),
25
+ # so the old `-d` test read every worktree as "installed package" and skipped the check entirely —
26
+ # silently, with exit 0. Measured 2026-07-31: a worktree created specifically to approximate CI
27
+ # reported PASS while this check had not run at all, i.e. the instrument used to justify wiring CI
28
+ # was itself fail-open on the surface it was standing in. `-e` covers both the ordinary checkout
29
+ # (dir) and the worktree (file); genuine package mode has no `.git` of either kind, so it still
30
+ # skips. Anchored by scripts/test_package_coverage_lanes.sh.
31
+ if [ ! -e .git ]; then
25
32
  echo "SKIP package-coverage (not a source checkout)"
26
33
  exit 0
27
34
  fi
35
+ # PREDICATE SPLIT (cross-family review, 2026-07-31). These were one condition, and folding them
36
+ # together meant `.git` present + manifest missing returned SKIP + exit 0 — "we are in a checkout
37
+ # and cannot read what ships" reported as "nothing to check here". Absence of the input is not
38
+ # absence of the defect; `not found != 0` (CLAUDE.md §Instrument-Calibration). Only the no-.git
39
+ # case is a legitimate skip (an installed package, where the un-shipped files are correctly gone).
40
+ if [ ! -f package.json ]; then
41
+ echo "FAIL package-coverage: source checkout with no package.json — the shipped file list is"
42
+ echo " unreadable, so coverage is UNMEASURED, not clean"
43
+ exit 1
44
+ fi
28
45
 
29
46
  # ── Accepted-absent, with the reason each one is NOT a defect ────────────────────────────────
30
47
  # Adding a line here is a decision, not a silencer: each entry states why shipping it would be
@@ -43,6 +60,10 @@ fi
43
60
  # no shipped hook invokes it.
44
61
  ACCEPTED_ABSENT=(
45
62
  ".claude/registry/LOCAL_SKILL_REGISTRY.md"
63
+ # An INSTALL DESTINATION the user creates (`cp templates/local_fh_context.md
64
+ # .claude/rules/local_fh_context.md`), not a file FH ships. Shipping it would overwrite the
65
+ # user's own cross-context wiring — the template it is copied FROM is what ships.
66
+ ".claude/rules/local_fh_context.md"
46
67
  ".claude/regression/probes.md"
47
68
  "scripts/sync-to-be.sh"
48
69
  "scripts/sync_guard_check.sh"
@@ -83,6 +104,24 @@ for s in shipped:
83
104
  # Only a path that REALLY EXISTS here but is left out of the tarball is this defect.
84
105
  # A path that exists nowhere is the ordinary phantom-reference class the ref-path
85
106
  # check above already owns; a path outside files[] that is also absent is nothing.
107
+ # EXISTENCE, not tracked-ness. A 2026-07-30 revision narrowed this to `git ls-files`
108
+ # to silence what looked like a machine-local false positive; measurement showed that was a
109
+ # WEAKENING — an existing-but-untracked path named by a shipped doc is exactly the defect
110
+ # (the npm user cannot have that file), and selfcheck's ref-path check SKIPs gitignored
111
+ # paths, so nothing else owns it. Reverted.
112
+ #
113
+ # WIDENING IS DEFERRED, AND THE REASON IS NOT A MEASUREMENT. Dropping `exists` entirely
114
+ # (flag every referenced ∧ ¬covered path) is arguably the correct predicate, but it cannot
115
+ # be evaluated while the extractor below is known-broken: its `(sh|py|js|md|json|…)`
116
+ # alternation puts `js` before `json`, so `settings.json` is captured as `settings.js`.
117
+ # A first pass at this comment cited a count of artifacts as evidence that `exists` is
118
+ # load-bearing — that count came FROM the broken extractor, i.e. an instrument was used to
119
+ # justify keeping a predicate before the instrument itself was validated (the circularity
120
+ # CLAUDE.md §Instrument-Calibration exists to forbid; a cross-family review caught it, and
121
+ # an independent extractor produced materially different numbers).
122
+ # HONEST STATE: fix the `js|json` alternation first, re-measure, then decide. Until then
123
+ # this check's true coverage is UNQUANTIFIED — treat a PASS as "no defect of the narrow
124
+ # exists-and-uncovered kind", not as "every shipped reference is sound".
86
125
  if os.path.exists(m) and not covered(m):
87
126
  if m in accepted:
88
127
  exercised.add(m)
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # pipe_verdict_guard.sh — PreToolUse(Bash) advisory: a verdict read from the wrong end of a pipe.
3
+ #
4
+ # THE DEFECT
5
+ # `cmd | tail -5; echo "exit=$?"` reports TAIL's status, not cmd's. A gate that FAILED reads as
6
+ # exit 0. The degrade direction is toward PASS, which is the direction that never announces
7
+ # itself. Measured 6× in this project between 2026-07-29 and 2026-07-31.
8
+ #
9
+ # WHY A HOOK AND NOT A FILE LINTER (measured 2026-07-31, and it reversed the plan on record)
10
+ # The session card prescribed "add an S6 class to scripts/degrade_direction_scan.sh". Every
11
+ # `pipe + $?` occurrence in this repo's shell scripts was then hand-verified: 7 hits, 7 correct
12
+ # — in all 7 the final stage WAS the command under test. True positives in shipped files: 0.
13
+ # All 6 recurrences lived in interactively-composed commands, which a repo scanner never reads.
14
+ # S6 would have shipped a probe with no true positives, the exact failure mode S5 records in
15
+ # its own comment ("100% FP trains dismissal of the one hit that will matter"). So the guard
16
+ # moved to the surface where the defect actually occurs: the Bash tool call itself.
17
+ #
18
+ # TWO RULES, DELIBERATELY UNEQUAL IN CONFIDENCE
19
+ # R1 — deterministic, zero-FP. `${PIPESTATUS[…]}` is bash-only. This project's Bash tool runs
20
+ # zsh, where that expands to the EMPTY STRING (zsh spells it `$pipestatus[1]`, 1-indexed).
21
+ # A verdict read from it is not wrong, it is ABSENT. No judgment involved.
22
+ # R2 — heuristic, narrowed to DISPLAY FILTERS as the final stage (tail/head/cat/less/more).
23
+ # A final `grep -q`, a script, or a subshell is usually the thing whose status is wanted —
24
+ # those are the 7 correct shapes above and are not flagged. Narrowing costs recall; the
25
+ # alternative is a probe nobody reads.
26
+ #
27
+ # DEGRADE DIRECTION: advisory. This guard WARNS and exits 0 — it never blocks a Bash call, because
28
+ # a mis-read verdict is re-runnable and a false block on a developer's shell trains --no-verify
29
+ # reflexes on hooks that DO guard irreversible surfaces. Set FH_PIPE_VERDICT_BLOCK=1 to escalate.
30
+ # If the command cannot be extracted, it stays silent: an unparsed input is not a finding.
31
+ #
32
+ # Usage:
33
+ # hook: PreToolUse matcher "Bash" → bash scripts/pipe_verdict_guard.sh
34
+ # test: printf '%s' "<command>" | bash scripts/pipe_verdict_guard.sh --stdin-raw
35
+ # Opt out on a single command with a trailing `# noqa: pipe-verdict`.
36
+
37
+ set -u
38
+
39
+ CMD=""
40
+ if [ "${1:-}" = "--stdin-raw" ]; then
41
+ CMD=$(cat)
42
+ else
43
+ RAW=$(cat)
44
+ # PreToolUse payload. Absent/!Bash/unparseable → stay silent (see degrade direction above).
45
+ CMD=$(printf '%s' "$RAW" | python3 -c '
46
+ import json,sys
47
+ try: d = json.load(sys.stdin)
48
+ except Exception: sys.exit(0)
49
+ if d.get("tool_name") != "Bash": sys.exit(0)
50
+ sys.stdout.write(d.get("tool_input", {}).get("command", "") or "")
51
+ ' 2>/dev/null) || CMD=""
52
+ fi
53
+ [ -n "$CMD" ] || exit 0
54
+
55
+ # Explicit opt-outs, checked before any rule.
56
+ printf '%s' "$CMD" | grep -qE '#[[:space:]]*noqa:?[[:space:]]*pipe-verdict' && exit 0
57
+
58
+ hits=""
59
+ add() { hits="${hits} ⚠️ PIPE-VERDICT $1
60
+ $2
61
+ "; }
62
+
63
+ # Flatten to ONE line before any matching. grep is line-oriented, so `.*` never spans a newline and
64
+ # every multi-line command missed — which is the worse half, because the invocations that actually
65
+ # recur here are multi-line. A newline is a statement separator, so `; ` is the faithful substitute.
66
+ # (Found by the Axis-2 adversarial pass on this guard, 2026-07-31; lanes A* pin it.)
67
+ FLAT=$(printf '%s' "$CMD" | tr '\n' ';' | sed 's/;/; /g')
68
+
69
+ # ── R1 — PIPESTATUS under zsh: the value is empty, so the verdict is absent. ──────────────────
70
+ # Brace-optional: zsh accepts `$PIPESTATUS[0]` as well, and the brace-anchored form missed it (lane B*).
71
+ if printf '%s' "$FLAT" | grep -qE '\$\{?PIPESTATUS[[{]' ; then
72
+ add "R1 \${PIPESTATUS[…]} is empty in zsh" \
73
+ "This shell is zsh; the bash array does not exist here, so the verdict expands to \"\". Use zsh's \`\$pipestatus[1]\` (1-indexed), or drop the pipe and read \$? directly."
74
+ fi
75
+
76
+ # ── R2 — display filter as the final stage, then a read of $?. ────────────────────────────────
77
+ # `||` is neutralized first: `a || b || echo 0` contains no pipeline, and reading it as one is
78
+ # how the sibling S5 probe produced 9 false positives before it was narrowed (2026-07-28).
79
+ # `set -o pipefail` in the same command makes `$?` after a pipeline correct — not a finding.
80
+ NORM=$(printf '%s' "$FLAT" | sed 's/||/__OR__/g')
81
+ if ! printf '%s' "$NORM" | grep -qE 'set -o pipefail|set -[a-zA-Z]*o[a-zA-Z]* pipefail'; then
82
+ if printf '%s' "$NORM" \
83
+ | grep -qE '\|[[:space:]]*(tail|head|cat|less|more)([[:space:]][^|;&]*)?[[:space:]]*[;&].*\$\?'; then
84
+ add "R2 \$? after a display filter" \
85
+ "\$? holds the filter's status (tail/head/cat almost always succeed), not the command's — a FAILED check reads as 0. Capture first: \`out=\$(cmd 2>&1); rc=\$?\` then print \"\$out\" | tail."
86
+ fi
87
+ fi
88
+
89
+ [ -n "$hits" ] || exit 0
90
+
91
+ printf '%s' "$hits" >&2
92
+ if [ "${FH_PIPE_VERDICT_BLOCK:-0}" = "1" ]; then exit 2; fi
93
+ exit 0
@@ -2,7 +2,13 @@
2
2
  # selfcheck.sh — mandatory-pass (deterministic) checks on FH's own executable surface.
3
3
  # Class: mandatory-pass (harness_6axis_framework.md §Axis 5 check classes) — blocks on fail.
4
4
  # Scope: executables shipped via npm files[] + the bash infra driving the FH gate chain.
5
- # Syntax-only (node --check / bash -n): zero side effects, no network, runs anywhere.
5
+ # NOT syntax-only any more, and this line used to say it was. Syntax checks (node --check / bash -n)
6
+ # are only the first section; behavioural lane suites follow and they DO have side effects and
7
+ # environment needs: temp dirs, a loopback HTTP stub on 127.0.0.1:18011, git, `timeout`, and — via
8
+ # the session-close lanes — an optional `gh` call that reaches GitHub when the binary is present.
9
+ # Corrected 2026-07-31 (cross-family review): the stale "zero side effects, no network" claim
10
+ # survived the additions that falsified it, which is how a reader ends up trusting the wrong
11
+ # invariant. No remote network is REQUIRED; some is possible.
6
12
  # Wiring: `npm test` for any session; `prepublishOnly` so a publish cannot ship a
7
13
  # syntactically broken executable.
8
14
  set -u
@@ -105,10 +111,25 @@ fi
105
111
  # absent from the tarball — including templates/predelete_check.sh, which CLAUDE.md instructs you
106
112
  # to run before a destructive op. Wired here in the same commit that created it, because the two
107
113
  # previous anchors this session shipped with zero callers.
108
- if [ -f scripts/package_coverage_check.sh ]; then
114
+ # Anchored 2026-07-31. Until then this was the ONE subject in this file exempt from the
115
+ # "subject present but anchor missing => FAIL" rule the eight blocks below enforce — and the
116
+ # exemption cost something real: its source-checkout predicate tested `-d .git`, so inside a git
117
+ # WORKTREE (where .git is a FILE) it printed SKIP and returned 0 without scanning. A worktree is
118
+ # how a fresh CI checkout gets approximated, so the check was absent from exactly the tree used
119
+ # to reason about CI. scripts/test_package_coverage_lanes.sh pins the predicate across all four
120
+ # tree shapes plus a known pair.
121
+ if [ ! -f scripts/package_coverage_check.sh ]; then
122
+ echo "SKIP test_package_coverage_lanes.sh (subject scripts/package_coverage_check.sh absent)"
123
+ elif [ -f scripts/test_package_coverage_lanes.sh ]; then
124
+ if ! bash scripts/test_package_coverage_lanes.sh; then
125
+ fail=1
126
+ fi
109
127
  if ! bash scripts/package_coverage_check.sh; then
110
128
  fail=1
111
129
  fi
130
+ else
131
+ echo "FAIL test_package_coverage_lanes.sh: package_coverage_check.sh present but its anchor is missing"
132
+ fail=1
112
133
  fi
113
134
 
114
135
  # memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
@@ -132,21 +153,6 @@ fi
132
153
  # prevent. test_card_drift_probe.sh had shipped with ZERO callers since it was written; wiring it
133
154
  # here closes that, and the anchors are added to files[] in the same change so package mode runs
134
155
  # them too rather than reporting a deleted anchor.
135
- # consent-class registry floor. Its subject decides whether standing consent may skip an approval
136
- # prompt, so an uncalibrated instrument there hands out autonomy the operator never granted. The
137
- # anchor was written into tests/ with ZERO callers first — the same defect this file already
138
- # records twice above; wiring it here is the fix, not a note about the fix.
139
- if [ ! -f scripts/consent_registry_check.sh ]; then
140
- echo "SKIP test_consent_registry.sh (subject scripts/consent_registry_check.sh absent)"
141
- elif [ -f scripts/test_consent_registry.sh ]; then
142
- if ! bash scripts/test_consent_registry.sh; then
143
- fail=1
144
- fi
145
- else
146
- echo "FAIL test_consent_registry.sh: consent_registry_check.sh present but its anchor is missing"
147
- fail=1
148
- fi
149
-
150
156
  # sidecar_wait stdin plumbing. Its subject is dispatched by auto-decorrelation / steel-quench /
151
157
  # sim-conductor / AGENTS.md as the REQUIRED wait form, so a regression there silently empties every
152
158
  # cross-family verification. The anchor's first version shipped in tests/ with ZERO callers — the
@@ -162,6 +168,72 @@ else
162
168
  fail=1
163
169
  fi
164
170
 
171
+ # fh_node_check.sh gets the same treatment, and for the same reason: three adversarial rounds on it
172
+ # produced defects that were ALL negative legs (floor N/A on a non-git install · another framework's
173
+ # hooks counted as ours · the Mode D applicability gate silencing its own flagship case), and each
174
+ # round's fix reverted a previous one because no anchor pinned it. Subject-present-but-anchor-absent
175
+ # is a FAIL, not a skip — that is how an anchor gets quietly dropped.
176
+ # Same treatment for the sidecar calibrator, same reason: its verdicts are all distinctions between
177
+ # states that look identical from outside ("the sidecar ran" vs "the model I pinned answered",
178
+ # "absent" vs "unmeasured"), and its lanes are hermetic stubs, so running them costs nothing.
179
+ if [ ! -f scripts/sidecar_calibrate.sh ]; then
180
+ echo "SKIP test_sidecar_calibrate_lanes.sh (subject scripts/sidecar_calibrate.sh absent)"
181
+ elif [ -f scripts/test_sidecar_calibrate_lanes.sh ]; then
182
+ if ! bash scripts/test_sidecar_calibrate_lanes.sh; then
183
+ fail=1
184
+ fi
185
+ else
186
+ echo "FAIL test_sidecar_calibrate_lanes.sh: sidecar_calibrate.sh present but its anchor is missing"
187
+ fail=1
188
+ fi
189
+
190
+ if [ ! -f scripts/fh_node_check.sh ]; then
191
+ echo "SKIP test_node_check_lanes.sh (subject scripts/fh_node_check.sh absent)"
192
+ elif [ -f scripts/test_node_check_lanes.sh ]; then
193
+ if ! bash scripts/test_node_check_lanes.sh; then
194
+ fail=1
195
+ fi
196
+ else
197
+ echo "FAIL test_node_check_lanes.sh: fh_node_check.sh present but its anchor is missing"
198
+ fail=1
199
+ fi
200
+
201
+ # Two guards that read the AUTHOR's own actions rather than the repo's files. Both were added
202
+ # 2026-07-31; the pipe-verdict lane shipped in PR #209 WITHOUT this wiring, which is itself the
203
+ # half-fix class the second guard exists to catch — found by running that guard on this repo.
204
+ if [ ! -f scripts/sidecar_calibrate.sh ]; then
205
+ echo "SKIP test_ollama_panel_lanes.sh (subject scripts/sidecar_calibrate.sh absent)"
206
+ elif [ -f scripts/test_ollama_panel_lanes.sh ]; then
207
+ if ! bash scripts/test_ollama_panel_lanes.sh; then
208
+ fail=1
209
+ fi
210
+ else
211
+ echo "FAIL test_ollama_panel_lanes.sh: sidecar_calibrate.sh present but its ollama-leg anchor is missing"
212
+ fail=1
213
+ fi
214
+
215
+ if [ ! -f scripts/pipe_verdict_guard.sh ]; then
216
+ echo "SKIP test_pipe_verdict_guard_lanes.sh (subject scripts/pipe_verdict_guard.sh absent)"
217
+ elif [ -f scripts/test_pipe_verdict_guard_lanes.sh ]; then
218
+ if ! bash scripts/test_pipe_verdict_guard_lanes.sh; then
219
+ fail=1
220
+ fi
221
+ else
222
+ echo "FAIL test_pipe_verdict_guard_lanes.sh: pipe_verdict_guard.sh present but its anchor is missing"
223
+ fail=1
224
+ fi
225
+
226
+ if [ ! -f scripts/halffix_propagation_scan.sh ]; then
227
+ echo "SKIP test_halffix_lanes.sh (subject scripts/halffix_propagation_scan.sh absent)"
228
+ elif [ -f scripts/test_halffix_lanes.sh ]; then
229
+ if ! bash scripts/test_halffix_lanes.sh; then
230
+ fail=1
231
+ fi
232
+ else
233
+ echo "FAIL test_halffix_lanes.sh: halffix_propagation_scan.sh present but its anchor is missing"
234
+ fail=1
235
+ fi
236
+
165
237
  for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh; do
166
238
  if [ ! -f scripts/session_close_check.sh ]; then
167
239
  echo "SKIP ${_anchor##*/} (subject scripts/session_close_check.sh absent)"
@@ -188,10 +260,31 @@ if [ -d ".claude/rules" ]; then
188
260
  # fail=0 → SELFCHECK: PASS. The check would have silently ceased to exist while still
189
261
  # reporting a pass — the same shape count_check.sh:71 already guards against with its
190
262
  # impossible-zero rule. fh-meta always has refs; zero means the instrument broke.
191
- _refs=$(grep -hoE '\`[^\` ]+\`' CLAUDE.md .claude/rules/*.md 2>/dev/null \
192
- | sed 's/\`//g' \
193
- | grep -E '^(knowledge|templates|scripts|docs|plugins|\.claude)/[^*{}<>$]+\.(md|sh|ya?ml|jsonc|json)$' \
194
- | sort -u)
263
+ # EXTRACTION MOVED OFF grep (2026-07-31, measured on the first real CI run). The pipeline used to
264
+ # be `grep -hoE` + sed + `grep -E` over CLAUDE.md, which is Korean-heavy. On macOS (BSD grep, UTF-8
265
+ # locale) it returned ~50 refs; on the ubuntu runner (GNU grep, LANG unset => C locale) it returned
266
+ # ZERO, and the impossible-zero guard below is the only reason that surfaced as a failure instead
267
+ # of "no refs, all clean". Same root cause as the card-drift probe failing its positive lanes in
268
+ # the same run: multibyte text through locale-dependent grep.
269
+ # python3 reads the files as UTF-8 explicitly, so this extractor no longer has a locale at all.
270
+ # It is already a hard dependency of selfcheck (validate_plugins/marketplace, memory_link_check),
271
+ # so this adds nothing to the requirement set. The regex is the same one, transcribed.
272
+ _refs=$(python3 - <<'REFPY' 2>/dev/null
273
+ import re, glob
274
+ pat = re.compile(r'^(knowledge|templates|scripts|docs|plugins|\.claude)/[^*{}<>$]+\.(md|sh|ya?ml|jsonc|json)$')
275
+ seen = set()
276
+ for f in ['CLAUDE.md'] + sorted(glob.glob('.claude/rules/*.md')):
277
+ try:
278
+ text = open(f, encoding='utf-8', errors='replace').read()
279
+ except OSError:
280
+ continue
281
+ for tok in re.findall(r'`([^` ]+)`', text):
282
+ if pat.match(tok):
283
+ seen.add(tok)
284
+ for p in sorted(seen):
285
+ print(p)
286
+ REFPY
287
+ )
195
288
  if [ -z "$_refs" ]; then
196
289
  echo "FAIL ref-path: extractor produced 0 refs — the scan broke, it did not pass"
197
290
  fail=1
@@ -146,7 +146,21 @@ fi
146
146
  # HONEST SCOPE: 카드 🔴/🟡 줄에서 부재-주장 키워드를 잡고, 그 줄의 이름/경로 토큰으로
147
147
  # 실물을 글롭 검색한다. 어휘가 안 겹치면 못 잡는다(무음 FN) — 앵커지 floor 가 아니다.
148
148
  # 방향은 advisory: 가역 표면에서 하드 블록은 --no-verify 를 학습시킨다(#165 HIGH-1 동일 원리).
149
- _ABSENCE_RE='미가동|산출물[^가-힣]*0|로그[^가-힣]*0|0건|부재|안 돌|미생성|not running|no output|zero output'
149
+ # COLLATION-FREE BY CONSTRUCTION (2026-07-31, diagnosed ON the runner after two local hypotheses
150
+ # were refuted). This regex used to contain the Hangul RANGE `[^가-힣]`. A multibyte range inside a
151
+ # bracket expression is collation-dependent, and GNU grep in the C locale — the GitHub runner's
152
+ # default — rejects it outright: `grep: Invalid collation character`.
153
+ # The failure mode is what makes it serious: grep writes that to stderr, exits 2, and emits NOTHING.
154
+ # Downstream this is indistinguishable from exit 1 "no match", so the pipeline produced zero lines
155
+ # and the probe concluded "no absence claims in the card" — CLEAN — on every input, positive and
156
+ # negative alike. Its 7 lanes had passed 3/3 on macOS for weeks because BSD grep accepts the range.
157
+ # Same shape as the pyyaml CI trigger caught earlier the same day: grep's ERROR status folded into
158
+ # its NO-MATCH branch. `not found != 0`, and neither is `could not look`.
159
+ # The replacement uses an ASCII-only class, which has no collation to be invalid. SEMANTIC SHIFT,
160
+ # stated rather than hidden: the original meant "산출물 then non-Hangul then 0" (keep the claim
161
+ # inside one clause); this means "산출물 then no DIGIT within 20 chars then 0". Slightly wider, and
162
+ # the lanes below are what pin that it did not become too wide.
163
+ _ABSENCE_RE='미가동|산출물[^0-9]{0,20}0|로그[^0-9]{0,20}0|0건|부재|안 돌|미생성|not running|no output|zero output'
150
164
  # 부정/정정 문맥 가드 — 부재-주장을 **인용하며** 정정하는 줄만 건너뛴다. 판별자는 debunk
151
165
  # 어휘 단독이 아니라 **부재-키워드가 인용부호 안에 있는가** — challenger A-1 실측: 살아있는
152
166
  # 주장 + 무관한 '정정 필요' 가 같은 줄이면 debunk-단독 가드가 진짜 경고를 무음 삼켰다(FN).
@@ -0,0 +1,275 @@
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
+ # ── Local OpenAI-compatible panel (Ollama) — a DIFFERENT anchor, on purpose. ──────────────────
185
+ # One host serves several model FAMILIES (measured 2026-07-31: an OpenAI-lineage open-weight, a
186
+ # Qwen, a Gemma, a Mistral on one endpoint), so the panel's unit here is the model, not the binary.
187
+ # Everything is local, so nothing leaves the machine — this is the only panel member a residency-
188
+ # constrained session may use on company-adjacent work.
189
+ #
190
+ # WHY THE IDENTITY PROBE IS REPLACED, NOT REUSED — the self-report anchor above is INVALID for this
191
+ # class. Measured: `gpt-oss:20b` asked which model it is answered "The underlying model is GPT-4
192
+ # (likely)". Open-weight models do not reliably know their own name, so a self-report anchor would
193
+ # mark every one of them UNTRUSTED-PIN and drop a genuinely exact pin from the panel. Applying an
194
+ # instrument that cannot separate a known-positive from a known-negative on this target is the
195
+ # calibration failure this whole file exists to prevent — so the anchor moves to the SERVER'S
196
+ # response envelope, which reports the model actually loaded. That is a server-side fact rather
197
+ # than a model's claim, i.e. strictly stronger than what codex/agy expose through their CLIs.
198
+ #
199
+ # HOST IS NEVER HARDCODED. A LAN/Tailscale address is an internal hostname, and this file is
200
+ # public-tracked; §Company residency keeps those out of committed content. Default is loopback;
201
+ # point FH_OLLAMA_HOST at a remote node from a local, gitignored place.
202
+ OLLAMA_HOST_URL="${FH_OLLAMA_HOST:-http://127.0.0.1:11434}"
203
+ # Measured floor, not a guess: at num_predict=64 a reasoning model spent the ENTIRE budget in its
204
+ # `thinking` field (780 chars) and returned an EMPTY response — which reads identically to "cannot
205
+ # carry a verdict". At 512 the same model answered `PASS`. Budget starvation and incapacity are
206
+ # different findings and must not be reported as one.
207
+ OLLAMA_NUM_PREDICT="${FH_OLLAMA_NUM_PREDICT:-512}"
208
+
209
+ probe_ollama() {
210
+ command -v curl >/dev/null 2>&1 || { echo "ollama ABSENT — curl missing (absence measured)"; return 0; }
211
+ curl -sf --max-time 10 "$OLLAMA_HOST_URL/api/version" >/dev/null 2>&1 || {
212
+ printf 'ollama ABSENT — no server at the configured host (absence measured, not assumed)\n'
213
+ printf ' set FH_OLLAMA_HOST to probe a remote node; default is loopback\n'
214
+ return 0
215
+ }
216
+
217
+ local models="${FH_OLLAMA_MODELS:-}"
218
+ if [ -z "$models" ]; then
219
+ models=$(curl -sf --max-time 15 "$OLLAMA_HOST_URL/api/tags" 2>/dev/null \
220
+ | python3 -c 'import json,sys
221
+ try: d=json.load(sys.stdin)
222
+ except Exception: raise SystemExit
223
+ print(",".join(m["name"] for m in d.get("models",[])[:6]))' 2>/dev/null)
224
+ fi
225
+ [ -n "$models" ] || { echo "ollama REACHABLE but no models listed"; return 0; }
226
+
227
+ # Control runs ONCE per host, not per model: it is a property of the server, and repeating it per
228
+ # model would just multiply the cost of a fact that cannot differ.
229
+ local ctl ctl_state
230
+ ctl=$(curl -s --max-time 20 "$OLLAMA_HOST_URL/api/generate" \
231
+ -d '{"model":"fh-calib-nonexistent:99b","prompt":"hi","stream":false}' 2>&1)
232
+ if printf '%s' "$ctl" | grep -qiE '"error"|not found'; then ctl_state="rejects-bogus"; else ctl_state="accepts-bogus"; fi
233
+
234
+ local IFS=,
235
+ for m in $models; do
236
+ [ -n "$m" ] || continue
237
+ local body out env_model resp compact pin v
238
+ body=$(python3 -c 'import json,sys; print(json.dumps({"model":sys.argv[1],"prompt":sys.argv[2],"stream":False,"options":{"num_predict":int(sys.argv[3]),"temperature":0}}))' \
239
+ "$m" "$VERDICT_PROMPT" "$OLLAMA_NUM_PREDICT")
240
+ out=$(curl -sf --max-time 240 "$OLLAMA_HOST_URL/api/generate" -d "$body" 2>/dev/null)
241
+ if [ -z "$out" ]; then
242
+ printf 'ollama %-22s UNREACHABLE-THIS-RUN (measured, not inferred)\n' "$m"; continue
243
+ fi
244
+ env_model=$(printf '%s' "$out" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("model",""))' 2>/dev/null)
245
+ resp=$(printf '%s' "$out" | python3 -c 'import json,sys; print((json.load(sys.stdin).get("response") or "").strip())' 2>/dev/null)
246
+ compact=$(printf '%s' "$resp" | tr -s '[:space:]' ' ' | sed 's/^ *//; s/ *$//')
247
+ if [ "$env_model" = "$m" ]; then pin="PIN-OK(envelope)"; else pin="UNTRUSTED-PIN"; fi
248
+ if [ "${#compact}" -le 12 ] && printf '%s' "$compact" | grep -qiE '^(pass|fail)[.!]?$'; then v="VERDICT-OK"; else v="VERDICT-UNPARSEABLE"; fi
249
+ printf 'ollama %-22s REACHABLE · %s · control: %s · %s\n' "$m" "$pin" "$ctl_state" "$v"
250
+ [ -z "$QUIET" ] && printf ' answered: %s\n' "$(printf '%s' "$compact" | cut -c1-60)"
251
+ if [ "$v" = "VERDICT-UNPARSEABLE" ] && [ -z "$compact" ]; then
252
+ printf ' ⚠️ EMPTY answer. Before recording this as "cannot carry a verdict", re-run with a\n'
253
+ printf ' larger FH_OLLAMA_NUM_PREDICT — a reasoning model can spend the whole budget\n'
254
+ printf ' thinking and return nothing, which looks identical from out here.\n'
255
+ fi
256
+ [ "$pin" = "PIN-OK(envelope)" ] && [ "$v" = "VERDICT-OK" ] && PANEL="${PANEL:+$PANEL, }ollama:$m"
257
+ done
258
+ }
259
+ # `--stub-model` means the caller is the hermetic CLI lane harness, which drives stub BINARIES and
260
+ # asserts on an empty panel. This leg talks to a SERVER, so on a developer machine with a local
261
+ # Ollama running it would populate the panel and break those lanes — which is exactly what happened
262
+ # when this was first wired (test_sidecar_calibrate_lanes lane6, "empty panel not stated", caught by
263
+ # the existing suite rather than by review). Skipping under --stub-model keeps each harness hermetic
264
+ # in its own way; this leg's own lanes drive a stub SERVER via FH_OLLAMA_HOST instead.
265
+ if [ -z "$STUB_MODEL" ] && { [ -z "$ONLY" ] || [ "$ONLY" = "ollama" ]; }; then
266
+ probe_ollama
267
+ fi
268
+
269
+ if [ -n "$PANEL" ]; then
270
+ echo "PANEL: $PANEL — usable different-family auditor(s), pin verified this run"
271
+ else
272
+ echo "PANEL: none — no runtime passed the identity probe on this machine, this run"
273
+ echo " State this, do not infer it: a marker's crossfamily leg may say 'none' but never stay silent."
274
+ fi
275
+ exit 0