@chrono-meta/fh-gate 1.4.78 → 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-plugin/marketplace.json +2 -2
- package/knowledge/shared/harness-core/loop_engineering.md +1 -1
- package/package.json +9 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/fh_session_load.sh +42 -3
- package/scripts/halffix_propagation_scan.sh +126 -0
- package/scripts/package_coverage_check.sh +18 -1
- package/scripts/pipe_verdict_guard.sh +93 -0
- package/scripts/selfcheck.sh +84 -6
- package/scripts/session_close_check.sh +15 -1
- package/scripts/sidecar_calibrate.sh +85 -0
- package/scripts/test_card_drift_probe.sh +55 -0
- package/scripts/test_halffix_lanes.sh +170 -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_wait_stdin.sh +13 -1
- package/templates/.git-hooks/pre-commit +9 -0
- package/templates/settings.PreToolUse.snippet.json +49 -0
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "fh-meta",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.79",
|
|
15
15
|
"description": "Hub meta-operations toolkit — 35 skills + 7 agents. New in 1.4.53: `fh-codex-doctor` (npm bin) — Codex adapter drift scanner; reads the documented M1/M2/M3 skill tier map + skill/agent source and reports codex-native/adapter-required/claude-native/unclassified per unit, wired into `npm test`/`prepublishOnly` (fail-closed on unclassified Claude-native primitives). New in 1.4.49: steel-quench gains Step 0.6 Verdict-Invariance Probe (groundedness axis — a load-bearing judged gate's verdict must track behavior, not rubric phrasing; measured flip-count over cross-family paraphrases; arXiv:2605.06161 Policy Invariance anchor); multi_model_sidecar_strategy §Vendor-native harness (a model is strongest in its own vendor CLI — Claude/CC, GPT/codex, Gemini/Antigravity; a universal router degrades all of them, so it stays an autocomplete/QA sidecar, never orchestration); predelete_check.sh fail-closed rewrite; memory-hygiene A-TMA anchor. New in 1.4.48: phantom-quench + steel-quench gain external frontier anchors (arXiv:2607.02052 package-hallucination; arXiv:2607.02057 prompt-coverage-adequacy); README model-flat claim reframed from a per-release point-curve to structural invariants (operation flattens across tiers; depth tier-order fixed within a generation). New in 1.4.47: onboarding step ① surfaces the Mode D companion-store session-start load in the auto-read salience anchor (previously only in the local binding + rules, so a greeting could skip the load). New in 1.4.46: context-doctor command-output axis (route to rtk/proxy for verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce envs). New in 1.4.41: context-doctor 2026 trigger vocab (context engineering/rot/collapse) + phantom-citation hardening; hub measurement-integrity-checklist (cross-model measurement pre-flight: display-name pin/reps≥3/discriminating probe). New in 1.4.40: install-wizard queryable-wiki scaffold (INDEX + session-start read + R/W/C ingest). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment) + video-ingest (capability-routed video ingestion). New in 1.4.x: verify-axis check-class taxonomy (mandatory-pass/measured/judged), no-reinvention Tier-0 inventory, 7-class failure taxonomy, Destructive-Op Gate, Wave-T (Temper), tier-floor governance, Mode D Model Notice, FC consent lane, default-Sonnet guidance. New in 1.3.0: public-surface-audit, field-harvest Mode B auto-trigger, 4-axis gate scope ext. Validated cross-CLI: Claude Code, Codex, Gemini.",
|
|
16
16
|
"source": "./plugins/fh-meta"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fh-commons",
|
|
20
|
-
"version": "1.4.
|
|
20
|
+
"version": "1.4.79",
|
|
21
21
|
"description": "Project-agnostic utility skills — 4 skills (convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate) + 1 agent (quench-challenger). Domain-independent utilities transplantable into any project.",
|
|
22
22
|
"source": "./plugins/fh-commons"
|
|
23
23
|
}
|
|
@@ -67,7 +67,7 @@ instrument: the next slip finds its leg pre-diagnosed.
|
|
|
67
67
|
| Backlog item | Fires when (measured trigger) |
|
|
68
68
|
|---|---|
|
|
69
69
|
| ~~Close-chain ordered-checklist script~~ | **BUILT 2026-07-10** (`scripts/session_close_check.sh`) — operator strengthen-instruction; the miss class (card staleness) was already measured, only the build trigger was overridden (recorded, not silent) |
|
|
70
|
-
| Weekly-audit scaffold + data-gather script | a weekly audit missed or hand-gathered wrong window data |
|
|
70
|
+
| Weekly-audit scaffold + data-gather script — **TRIGGER FIRED 2026-07-31, half built** | a weekly audit missed or hand-gathered wrong window data. **It fired on both legs**: the audit was **50 days** overdue (cadence is 7), and the window data was then gathered by hand with an ad-hoc bucketing script written on the spot. What was built in response is the **detection** half only — a cadence line in the SessionStart hook, mirroring the frontier-digest one, because the diagnosis was that the two cadences differ in *instrumentation*, not in importance: the mechanized one went 50 days without missing a day while this one went 50 days unnoticed. The **scaffold + data-gather** half stays unbuilt on purpose; a missed audit is now visible, and whether hand-gathering is painful enough to mechanize is a second measurement, not an implication of the first. Re-fires if the next audit is again hand-gathered with friction worth recording. |
|
|
71
71
|
| harvest-loop Step 0-b/0-c evidence check | a harvest run misses completed items despite `fh_completed_*` existing |
|
|
72
72
|
| goal-quench mid-run checkpoint files (70/85/95%) | a /goal run blows through a threshold unnoticed |
|
|
73
73
|
| ~~Substrate-jump detector~~ | **BUILT 2026-07-10** (`scripts/substrate_jump_detector.sh`, SessionStart-wired) — same operator instruction; structure-enforcing class (out-of-context drift), permanent per the durable-mechanization criterion |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrono-meta/fh-gate",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.79",
|
|
4
4
|
"description": "FH runtime adapters — run FH governance, skills, and agents via Claude or Codex with machine-parseable gates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"scripts/count_check.sh",
|
|
65
65
|
"scripts/selfcheck.sh",
|
|
66
66
|
"scripts/package_coverage_check.sh",
|
|
67
|
+
"scripts/test_package_coverage_lanes.sh",
|
|
67
68
|
"scripts/test_fh_gate_regressions.sh",
|
|
68
69
|
"templates/local_fh_context.md",
|
|
69
70
|
"docs/ETHOS.md",
|
|
@@ -130,6 +131,12 @@
|
|
|
130
131
|
"templates/settings.SessionStart.snippet.json",
|
|
131
132
|
"scripts/test_node_check_lanes.sh",
|
|
132
133
|
"scripts/sidecar_calibrate.sh",
|
|
133
|
-
"scripts/test_sidecar_calibrate_lanes.sh"
|
|
134
|
+
"scripts/test_sidecar_calibrate_lanes.sh",
|
|
135
|
+
"scripts/pipe_verdict_guard.sh",
|
|
136
|
+
"scripts/test_pipe_verdict_guard_lanes.sh",
|
|
137
|
+
"templates/settings.PreToolUse.snippet.json",
|
|
138
|
+
"scripts/halffix_propagation_scan.sh",
|
|
139
|
+
"scripts/test_halffix_lanes.sh",
|
|
140
|
+
"scripts/test_ollama_panel_lanes.sh"
|
|
134
141
|
]
|
|
135
142
|
}
|
|
@@ -103,6 +103,22 @@ _fd_hit() { find "$1" -maxdepth 1 -name "frontier_digest_$(date +%Y_%m_%d)*.md"
|
|
|
103
103
|
# 계기의 스코프가 대상보다 좁았던 케이스 — 대상은 '오늘 digest 가 있나'지 '이 디스크에 있나'가 아니다.)
|
|
104
104
|
# 술어는 로컬과 **동일**(glob + -size +1k) — divergent-leniency 를 만들지 않는다.
|
|
105
105
|
_fd_ready() { _fd_hit "$FH/tracks/_meta" || { [ -n "$BE" ] && _fd_hit "$BE/tracks-meta"; }; }
|
|
106
|
+
# THE SECOND HALF OF THE SAME SCOPE BUG (2026-07-31). The comment above got the principle right —
|
|
107
|
+
# "대상은 '오늘 digest 가 있나'지 '이 디스크에 있나'가 아니다" — and then widened the predicate by
|
|
108
|
+
# exactly ONE surface (the companion store), leaving it file-only. There are TWO live producers:
|
|
109
|
+
# the launchd file runner AND an app routine that posts the digest as a comment on GitHub issue
|
|
110
|
+
# #102 (measured 2026-07-31: 47 comments, one per day, including today's at 09:09 KST). So on a day
|
|
111
|
+
# when the file runner fails, today's digest EXISTS and the hook said "부재가 아니라 실패다" — a
|
|
112
|
+
# true statement about this disk stated as a claim about the digest.
|
|
113
|
+
# The fix is NOT to call the network from a SessionStart hook (it must never block turn 0 and must
|
|
114
|
+
# never fail on an offline node). It is to stop over-claiming: report the scope actually measured
|
|
115
|
+
# ("this node's file output failed") and NAME the surface not measured, so the reader checks it in
|
|
116
|
+
# one step instead of re-running a job whose output already exists elsewhere.
|
|
117
|
+
_fd_issue_note() {
|
|
118
|
+
echo " ⓘ 파일만 본 판정이다 — 오늘치는 **GH issue #102**(앱 routine, 매일 ~09:09 KST)에 이미 있을 수 있다."
|
|
119
|
+
echo " 확인: gh issue view 102 --repo chrono-meta/forge-harness --comments | tail -40"
|
|
120
|
+
echo " → 파일 재생성이 필요한지는 그걸 보고 판단하라. '오늘은 뉴스 없음'으로 읽지 말 것."
|
|
121
|
+
}
|
|
106
122
|
if _fd_ready; then
|
|
107
123
|
# 로컬엔 없고 컴패니언에만 있으면 = 이 노드는 러너가 아니다. 침묵하면 토폴로지가 안 보이므로 한 줄 알린다.
|
|
108
124
|
if ! _fd_hit "$FH/tracks/_meta"; then
|
|
@@ -125,15 +141,38 @@ elif [ -f "$_FD_LOG" ]; then
|
|
|
125
141
|
if [ -n "$_FD_ALIVE" ]; then
|
|
126
142
|
echo "ℹ️ [frontier-digest] 잡이 아직 돌고 있는 중일 수 있다($_FD_STAMP, $_FD_ALIVE) — 실패 판정 보류, 나중에 재확인."
|
|
127
143
|
else
|
|
128
|
-
echo "⚠️ [frontier-digest]
|
|
144
|
+
echo "⚠️ [frontier-digest] **이 노드의 파일 산출**이 실패했다($_FD_STAMP) — 부재가 아니라 실패다."
|
|
129
145
|
echo " 마지막 로그: $(tail -1 "$_FD_LOG" 2>/dev/null | cut -c1-90)"
|
|
130
|
-
|
|
146
|
+
_fd_issue_note
|
|
131
147
|
fi
|
|
132
148
|
else
|
|
133
|
-
echo "⚠️ [frontier-digest] 스케줄(09:00) 지났는데 로그도 산출물도 없다($_FD_STAMP) — 잡이 아예 안 돌았을 수 있다(launchd 확인)."
|
|
149
|
+
echo "⚠️ [frontier-digest] 스케줄(09:00) 지났는데 로그도 파일 산출물도 없다($_FD_STAMP) — 이 노드에서 잡이 아예 안 돌았을 수 있다(launchd 확인)."
|
|
150
|
+
_fd_issue_note
|
|
134
151
|
fi
|
|
135
152
|
# (BE is resolved at the top of this script — the frontier-digest block above needs it too.)
|
|
136
153
|
|
|
154
|
+
# 1-b) Weekly-audit cadence — mechanical, for the same reason the frontier-digest block above is.
|
|
155
|
+
# MEASURED 2026-07-31: operations.md §Session start auto-detection (L1) promises "propose the audit
|
|
156
|
+
# if 7+ days elapsed", and the audit had not run for FIFTY days. The frontier-digest cadence, which
|
|
157
|
+
# is instrumented, never went a day unnoticed over the same window. The difference is not
|
|
158
|
+
# importance; it is that one cadence is a hook and the other is prose, and this repo's own N=3
|
|
159
|
+
# escalation rule says to instrument rather than to add a habit. One line in a hook that already
|
|
160
|
+
# runs, not a new mechanism. Advisory by design: an overdue audit is not an irreversible surface,
|
|
161
|
+
# so it surfaces and never blocks. Silent when current, and silent when the dir does not exist
|
|
162
|
+
# (a fresh clone has no audit history and must not be nagged about one).
|
|
163
|
+
_AUDIT_DIR="$FH/tracks/_audit"
|
|
164
|
+
if [ -d "$_AUDIT_DIR" ]; then
|
|
165
|
+
_LATEST_AUDIT="$(find "$_AUDIT_DIR" -maxdepth 1 -name 'weekly_audit_*.md' -print 2>/dev/null \
|
|
166
|
+
| sort | tail -1)"
|
|
167
|
+
if [ -n "$_LATEST_AUDIT" ]; then
|
|
168
|
+
_AUDIT_AGE_D=$(( ( $(date +%s) - $(_mtime "$_LATEST_AUDIT") ) / 86400 ))
|
|
169
|
+
if [ "$_AUDIT_AGE_D" -ge 7 ]; then
|
|
170
|
+
echo "🗓️ [weekly-audit] 마지막 감사가 ${_AUDIT_AGE_D}일 전이다($(basename "$_LATEST_AUDIT")) — 캐던스는 7일."
|
|
171
|
+
echo " → /harvest-loop (lightweight) 또는 operations.md §Weekly Improvement Cycle 수동 절차."
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
|
|
137
176
|
# Non-Mode-D / no companion store → silent no-op (this is the majority path for public users).
|
|
138
177
|
[ -d "$BE/.git" ] || exit 0
|
|
139
178
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# halffix_propagation_scan.sh — pre-commit advisory: this fix may have landed in only one copy.
|
|
3
|
+
#
|
|
4
|
+
# THE DEFECT — "반쪽-수리" (half-fix)
|
|
5
|
+
# A defect class lives in N sibling copies. The fix lands in ONE and nothing says so. Measured 3x
|
|
6
|
+
# in this project, and the shape is worse than the count: every one of the three occurred INSIDE
|
|
7
|
+
# an edit that was itself repairing an earlier half-fix. scripts/psa_scan_lib.sh's header records
|
|
8
|
+
# five such divergences found in a single 2026-07-26 audit — every confidentiality defect that
|
|
9
|
+
# audit found was a divergence between duplicated copies, not a flaw in the idea.
|
|
10
|
+
#
|
|
11
|
+
# WHAT IT DOES
|
|
12
|
+
# Takes the distinctive symbols and path literals touched by the staged diff, re-greps the tree,
|
|
13
|
+
# and NAMES the tracked files that carry the same token but are not staged. That is the whole
|
|
14
|
+
# contribution: the author already knows what they fixed; what they lose is the sibling.
|
|
15
|
+
#
|
|
16
|
+
# MARK, DO NOT BLOCK — this is mandated, not preferred. The spec for this debt is explicit:
|
|
17
|
+
# "표시(차단 아님 — 정당한 복제도 있다)". Legitimate duplication exists (templates/ ships a
|
|
18
|
+
# field-propagated copy of scripts/ ON PURPOSE, and selfcheck asserts they stay byte-identical).
|
|
19
|
+
# A detector that blocks on correct duplication is a detector that gets disabled.
|
|
20
|
+
#
|
|
21
|
+
# THE DISCRIMINATOR — if every copy is staged, the fix propagated and this stays SILENT.
|
|
22
|
+
# Without that, the scan fires loudest exactly when the author did the right thing. Two prior
|
|
23
|
+
# claims on this same mistake in this repo: S5 (9/9 false positives, narrowed 2026-07-28) and
|
|
24
|
+
# S6 (0 true positives on the planned surface, retargeted 2026-07-31). Lane N3 pins it.
|
|
25
|
+
#
|
|
26
|
+
# Usage: bash scripts/halffix_propagation_scan.sh # reads the staged diff of $PWD
|
|
27
|
+
# Opt out: put `noqa: half-fix` on any added line in the commit.
|
|
28
|
+
|
|
29
|
+
set -u
|
|
30
|
+
cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" || exit 0
|
|
31
|
+
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
32
|
+
|
|
33
|
+
# Deletions are excluded (ACM): a removed file's symbols surviving elsewhere is not a half-fix,
|
|
34
|
+
# it is the normal state of a deletion, and flagging it would be pure noise.
|
|
35
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null)
|
|
36
|
+
[ -n "$STAGED" ] || exit 0
|
|
37
|
+
|
|
38
|
+
DIFF=$(git diff --cached -U0 --diff-filter=ACM 2>/dev/null)
|
|
39
|
+
printf '%s' "$DIFF" | grep -qE '^\+.*noqa:?[[:space:]]*half-fix' && exit 0
|
|
40
|
+
|
|
41
|
+
# Anchor tokens, from CHANGED lines only (added and removed — a removed spelling is exactly what a
|
|
42
|
+
# sibling may still carry). Two shapes:
|
|
43
|
+
# · identifiers >= 10 chars — long enough that a collision is meaningful. Shell/py keywords and
|
|
44
|
+
# the everyday vocabulary (`then`, `echo`, `return`, `local`) are all shorter, so the length
|
|
45
|
+
# floor does the keyword filtering without a denylist to maintain. (Lane N6.)
|
|
46
|
+
# · path literals `a/b` — the spec names filenames as anchors alongside symbols. (Lane P7.)
|
|
47
|
+
# The ENCLOSING function counts as a changed symbol even when the edited line itself carries no
|
|
48
|
+
# distinctive token — and a half-fix is a function-level thing, so this is the common case, not an
|
|
49
|
+
# edge one. git already hands it over in the hunk header (`@@ -2 +2 @@ psa_low_allowlisted() {`),
|
|
50
|
+
# so the context comes for free rather than from a hand-rolled scope parser.
|
|
51
|
+
# Found by lane P1 failing: the fix edited only a `case` line, whose longest token was 9 chars, and
|
|
52
|
+
# the scan went silent on a textbook two-copy divergence.
|
|
53
|
+
CHANGED=$( { printf '%s\n' "$DIFF" | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)'
|
|
54
|
+
printf '%s\n' "$DIFF" | sed -n 's/^@@ .* @@ //p'
|
|
55
|
+
} )
|
|
56
|
+
TOKENS=$( { printf '%s\n' "$CHANGED" | grep -oE '[A-Za-z_][A-Za-z0-9_-]{9,}'
|
|
57
|
+
printf '%s\n' "$CHANGED" | grep -oE '[A-Za-z0-9_.-]+/[A-Za-z0-9_./-]+'
|
|
58
|
+
} | sort -u )
|
|
59
|
+
# NO early exit here. R1 (tokens) and R2 (whole-file copies) are INDEPENDENT rules, and an empty
|
|
60
|
+
# token set is the normal state for a short edit — `a() { :; }` → `a() { echo fixed; }` carries no
|
|
61
|
+
# 10-char anchor at all. An early `exit 0` on empty tokens silently disabled R2 for exactly the
|
|
62
|
+
# edits R2 exists to catch. (Caught by lane R2p, 2026-07-31, after the same shape had already
|
|
63
|
+
# passed 10/10 in the other lanes — a rule can be correct and unreachable.)
|
|
64
|
+
|
|
65
|
+
# Cap, and SAY SO when it bites. A silent truncation reads as "checked everything" when it did not.
|
|
66
|
+
MAX_TOKENS="${HALFFIX_MAX_TOKENS:-60}"
|
|
67
|
+
TOTAL=$(printf '%s\n' "$TOKENS" | grep -c .)
|
|
68
|
+
if [ "$TOTAL" -gt "$MAX_TOKENS" ]; then
|
|
69
|
+
echo " ℹ️ half-fix scan: $TOTAL anchor tokens in this diff, examining the first $MAX_TOKENS (raise with HALFFIX_MAX_TOKENS)." >&2
|
|
70
|
+
TOKENS=$(printf '%s\n' "$TOKENS" | head -n "$MAX_TOKENS")
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# A token in many files is framework vocabulary, not a duplicated fix site. (Lane N5.)
|
|
74
|
+
MAX_FILES="${HALFFIX_MAX_FILES:-8}"
|
|
75
|
+
staged_has() { printf '%s\n' "$STAGED" | grep -qxF "$1"; }
|
|
76
|
+
|
|
77
|
+
hits=""
|
|
78
|
+
while IFS= read -r tok; do
|
|
79
|
+
[ -n "$tok" ] || continue
|
|
80
|
+
files=$(git grep -l --fixed-strings -e "$tok" -- . 2>/dev/null)
|
|
81
|
+
[ -n "$files" ] || continue
|
|
82
|
+
n=$(printf '%s\n' "$files" | grep -c .)
|
|
83
|
+
[ "$n" -le "$MAX_FILES" ] || continue
|
|
84
|
+
others=""
|
|
85
|
+
while IFS= read -r f; do
|
|
86
|
+
[ -n "$f" ] || continue
|
|
87
|
+
staged_has "$f" || others="${others}${others:+, }$f"
|
|
88
|
+
done <<< "$files"
|
|
89
|
+
[ -n "$others" ] || continue # every copy staged → propagated → silent (lane N3)
|
|
90
|
+
hits="${hits} ⚠️ HALF-FIX \`$tok\` also lives in: $others
|
|
91
|
+
"
|
|
92
|
+
done <<< "$TOKENS"
|
|
93
|
+
|
|
94
|
+
# ── R2 — whole-file copy divergence. ─────────────────────────────────────────────────────────
|
|
95
|
+
# The token rule alone missed this repo's real duplicate pair: a script's NAME lives in 11–18 files
|
|
96
|
+
# here (docs, CATALOG, the manifest, selfcheck refs), so the ubiquity filter suppressed it, while an
|
|
97
|
+
# internal symbol like psa_low_allowlisted lives in 2. Measured 2026-07-31 — lanes 10/10 green, live
|
|
98
|
+
# probe silent. The lanes were necessary and not sufficient.
|
|
99
|
+
#
|
|
100
|
+
# Exact, not heuristic: the sibling was BYTE-IDENTICAL at HEAD and only one side is staged, so it is
|
|
101
|
+
# a divergence by construction and needs no threshold. Same-basename files that were never copies
|
|
102
|
+
# (CLAUDE.md vs templates/CLAUDE.md, the 40 SKILL.md files) stay silent — a basename rule would have
|
|
103
|
+
# flooded on exactly those.
|
|
104
|
+
while IFS= read -r sf; do
|
|
105
|
+
[ -n "$sf" ] || continue
|
|
106
|
+
base=$(basename "$sf")
|
|
107
|
+
head_blob=$(git rev-parse "HEAD:$sf" 2>/dev/null) || continue
|
|
108
|
+
while IFS= read -r cand; do
|
|
109
|
+
[ -n "$cand" ] && [ "$cand" != "$sf" ] || continue
|
|
110
|
+
staged_has "$cand" && continue
|
|
111
|
+
cand_blob=$(git rev-parse "HEAD:$cand" 2>/dev/null) || continue
|
|
112
|
+
[ "$cand_blob" = "$head_blob" ] || continue # were they the SAME file before this edit?
|
|
113
|
+
hits="${hits} ⚠️ HALF-FIX \`$sf\` was byte-identical to \`$cand\` at HEAD — only one side is staged
|
|
114
|
+
"
|
|
115
|
+
done <<< "$(git ls-files -- "*/$base" "$base" 2>/dev/null)"
|
|
116
|
+
done <<< "$STAGED"
|
|
117
|
+
|
|
118
|
+
[ -n "$hits" ] || exit 0
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
echo "⚠️ HALF-FIX PROPAGATION — symbols you changed also exist in files you did NOT stage."
|
|
122
|
+
echo " Not a verdict: templates/ ships deliberate copies of scripts/. Judge each, then proceed."
|
|
123
|
+
printf '%s' "$hits"
|
|
124
|
+
echo " Silence this commit with a \`noqa: half-fix\` comment on any added line."
|
|
125
|
+
} >&2
|
|
126
|
+
exit 0
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -177,6 +198,42 @@ else
|
|
|
177
198
|
fail=1
|
|
178
199
|
fi
|
|
179
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
|
+
|
|
180
237
|
for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh; do
|
|
181
238
|
if [ ! -f scripts/session_close_check.sh ]; then
|
|
182
239
|
echo "SKIP ${_anchor##*/} (subject scripts/session_close_check.sh absent)"
|
|
@@ -203,10 +260,31 @@ if [ -d ".claude/rules" ]; then
|
|
|
203
260
|
# fail=0 → SELFCHECK: PASS. The check would have silently ceased to exist while still
|
|
204
261
|
# reporting a pass — the same shape count_check.sh:71 already guards against with its
|
|
205
262
|
# impossible-zero rule. fh-meta always has refs; zero means the instrument broke.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
)
|
|
210
288
|
if [ -z "$_refs" ]; then
|
|
211
289
|
echo "FAIL ref-path: extractor produced 0 refs — the scan broke, it did not pass"
|
|
212
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).
|
|
@@ -181,6 +181,91 @@ probe_runtime() {
|
|
|
181
181
|
probe_runtime codex "gpt-5.6-sol"
|
|
182
182
|
probe_runtime agy "Gemini 3.1 Pro (High)"
|
|
183
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
|
+
|
|
184
269
|
if [ -n "$PANEL" ]; then
|
|
185
270
|
echo "PANEL: $PANEL — usable different-family auditor(s), pin verified this run"
|
|
186
271
|
else
|