@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.
- package/.claude/rules/fh_4axis_gate.md +63 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +96 -260
- package/CLAUDE.md +2 -7
- package/docs/codex-compat.md +4 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +233 -0
- package/knowledge/shared/harness-core/loop_engineering.md +1 -1
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +14 -0
- package/knowledge/shared/rules/operational_adaptation.md +1 -130
- package/package.json +14 -5
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +88 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +117 -3
- package/scripts/fh_node_check.sh +184 -0
- package/scripts/fh_session_load.sh +101 -31
- package/scripts/halffix_propagation_scan.sh +126 -0
- package/scripts/package_coverage_check.sh +40 -1
- package/scripts/pipe_verdict_guard.sh +93 -0
- package/scripts/selfcheck.sh +114 -21
- package/scripts/session_close_check.sh +15 -1
- package/scripts/sidecar_calibrate.sh +275 -0
- package/scripts/test_card_drift_probe.sh +55 -0
- package/scripts/test_halffix_lanes.sh +170 -0
- package/scripts/test_node_check_lanes.sh +179 -0
- package/scripts/test_ollama_panel_lanes.sh +120 -0
- package/scripts/test_package_coverage_lanes.sh +250 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +96 -0
- package/scripts/test_sidecar_calibrate_lanes.sh +218 -0
- package/scripts/test_sidecar_wait_stdin.sh +13 -1
- package/templates/.git-hooks/pre-commit +9 -0
- package/templates/settings.PreToolUse.snippet.json +49 -0
- package/templates/settings.SessionStart.snippet.json +54 -0
- package/scripts/consent_registry_check.sh +0 -390
- package/scripts/test_consent_registry.sh +0 -255
- 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
|
-
|
|
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
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|