@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_pipe_verdict_guard_lanes.sh — known pairs for scripts/pipe_verdict_guard.sh
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS FILE EXISTS, AND WHY IT IS WRITTEN BEFORE THE DETECTOR
|
|
5
|
+
# 2026-07-30 harvest #1: across a 5-round challenger run, three rounds contained a fix that
|
|
6
|
+
# reverted an earlier fix — and the rounds that wrote the lane BEFORE touching the code had
|
|
7
|
+
# zero such regressions. Convergence came from the ORDER, not the patch. So: lanes first.
|
|
8
|
+
#
|
|
9
|
+
# WHAT IS BEING GUARDED
|
|
10
|
+
# Reading a verdict from `$?`/`${PIPESTATUS[…]}` after a pipeline, which yields the status of
|
|
11
|
+
# the LAST stage. When the last stage is a display filter (`tail`, `head`, `cat`), that status
|
|
12
|
+
# is the filter's — a FAILING gate reads as exit 0. Degrade direction: toward PASS.
|
|
13
|
+
#
|
|
14
|
+
# R1 (deterministic, zero-FP): `${PIPESTATUS[...]}` under zsh. zsh spells it `$pipestatus[1]`
|
|
15
|
+
# and 1-indexes it; the bash array expands to the EMPTY STRING. Any verdict read from it is
|
|
16
|
+
# not merely wrong, it is absent. Measured 6× in this project's ad-hoc invocations.
|
|
17
|
+
# R2 (heuristic, narrowed): pipeline whose FINAL stage is a display filter, followed by a read
|
|
18
|
+
# of `$?`. Narrowed to display filters on purpose — see the FP lanes below.
|
|
19
|
+
#
|
|
20
|
+
# WHY A REPO-FILE LINTER IS THE WRONG SURFACE (measured 2026-07-31)
|
|
21
|
+
# The prescription on the session card was "add an S6 class to degrade_direction_scan.sh".
|
|
22
|
+
# Hand-verifying every `pipe + $?` hit in this repo's scripts returned 7 hits, 7 of them correct
|
|
23
|
+
# (the last stage WAS the command under test in all 7). True positives in shipped files: 0.
|
|
24
|
+
# All 6 measured recurrences were in interactively-composed commands, which no file scanner
|
|
25
|
+
# reads. Shipping S6 would have been a 0-true-positive probe — exactly the failure S5's own
|
|
26
|
+
# comment records ("100% FP trains dismissal of the one hit that will matter").
|
|
27
|
+
|
|
28
|
+
set -u
|
|
29
|
+
G="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pipe_verdict_guard.sh"
|
|
30
|
+
pass=0; fail=0
|
|
31
|
+
|
|
32
|
+
# expect <label> <expected: HIT|CLEAN> <command-string>
|
|
33
|
+
expect() {
|
|
34
|
+
local label="$1" want="$2" cmd="$3" out got
|
|
35
|
+
out=$(printf '%s' "$cmd" | bash "$G" --stdin-raw 2>&1)
|
|
36
|
+
if printf '%s' "$out" | grep -q 'PIPE-VERDICT'; then got=HIT; else got=CLEAN; fi
|
|
37
|
+
if [ "$got" = "$want" ]; then
|
|
38
|
+
printf ' ✅ %-52s %s (expected %s)\n' "$label" "$got" "$want"; pass=$((pass+1))
|
|
39
|
+
else
|
|
40
|
+
printf ' ❌ %-52s %s (expected %s)\n' "$label" "$got" "$want"; fail=$((fail+1))
|
|
41
|
+
printf ' cmd: %s\n out: %s\n' "$cmd" "$out"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
echo "[pipe-verdict-guard] known pairs"
|
|
46
|
+
echo "-- R1: PIPESTATUS under zsh (deterministic) --"
|
|
47
|
+
# The exact shape emitted 6× in this project, including twice on 2026-07-31.
|
|
48
|
+
expect "R1 the measured shape" HIT 'bash x.sh | tail -5; echo "exit=${PIPESTATUS[0]}"'
|
|
49
|
+
expect "R1 any index" HIT 'a | b; rc=${PIPESTATUS[1]}'
|
|
50
|
+
expect "R1 inside a larger command" HIT 'cd /r && npm t | tail; E=${PIPESTATUS[0]}; echo $E'
|
|
51
|
+
# zsh's own spelling is correct here and must never be flagged.
|
|
52
|
+
expect "R1 zsh spelling is CLEAN" CLEAN 'a | b; rc=$pipestatus[1]'
|
|
53
|
+
|
|
54
|
+
echo "-- R2: display-filter final stage, then \$? --"
|
|
55
|
+
expect "R2 tail then \$?" HIT 'bash gate.sh | tail -20; echo "exit=$?"'
|
|
56
|
+
expect "R2 head then \$?" HIT 'make test | head -40; rc=$?'
|
|
57
|
+
expect "R2 cat then \$?" HIT 'run.sh | cat; if [ $? -ne 0 ]; then echo bad; fi'
|
|
58
|
+
|
|
59
|
+
echo "-- R2 false-positive lanes: the 7 shapes this repo actually ships --"
|
|
60
|
+
# Every one of these was hand-verified on 2026-07-31 as CORRECT: the final stage IS the command
|
|
61
|
+
# whose status is wanted. A detector that flags these is noise, and noise trains dismissal.
|
|
62
|
+
expect "FP grep -q is the test itself" CLEAN 'echo "$pos" | grep -qE "$re"; rc=$?'
|
|
63
|
+
expect "FP grep compiles the regex" CLEAN "printf '' | grep -E \"\$re\" >/dev/null 2>&1; rc=\$?"
|
|
64
|
+
# Path deliberately generic: naming a real unshipped script here would make this lane a shipped
|
|
65
|
+
# doc pointing at a file the package omits (caught by selfcheck's package-coverage rule).
|
|
66
|
+
expect "FP last stage is the script" CLEAN "printf '%s' \"\$S\" | bash some-filter.sh - ; echo EXIT:\$?"
|
|
67
|
+
expect "FP command substitution assign" CLEAN 'out=$(printf x | ( cd "$r" && bash hook 2>&1 )); rc=$?'
|
|
68
|
+
expect "FP no pipe at all" CLEAN 'bash gate.sh; echo "exit=$?"'
|
|
69
|
+
expect "FP || fallback, not a pipe" CLEAN 'stat -c %Y f || stat -f %m f || echo 0'
|
|
70
|
+
expect "FP pipefail set, explicit" CLEAN 'set -o pipefail; bash gate.sh | tail -5; rc=$?'
|
|
71
|
+
|
|
72
|
+
echo "-- adversarial (found by the Axis-2 pass on this guard, 2026-07-31) --"
|
|
73
|
+
# A: grep is line-oriented, so `.*` never spanned a newline and every MULTI-LINE command missed.
|
|
74
|
+
# This mattered more than the single-line case: the invocations that actually recur in this
|
|
75
|
+
# project are multi-line. Caught by attacking the guard, not by the happy-path lanes.
|
|
76
|
+
expect "A multi-line pipe then \$?" HIT 'bash gate.sh | tail -20
|
|
77
|
+
echo "exit=$?"'
|
|
78
|
+
expect "A multi-line, three statements" HIT 'cd /r
|
|
79
|
+
make test | head -40
|
|
80
|
+
rc=$?'
|
|
81
|
+
expect "A multi-line stays CLEAN if legit" CLEAN 'cd /r
|
|
82
|
+
echo x | grep -q y
|
|
83
|
+
rc=$?'
|
|
84
|
+
# B: zsh accepts `$PIPESTATUS[0]` without braces; the brace-anchored regex missed it.
|
|
85
|
+
expect "B PIPESTATUS without braces" HIT 'a | b; rc=$PIPESTATUS[0]'
|
|
86
|
+
expect "B braced form still caught" HIT 'a | b; rc=${PIPESTATUS[0]}'
|
|
87
|
+
|
|
88
|
+
echo "-- opt-out --"
|
|
89
|
+
expect "noqa suppresses" CLEAN 'bash g.sh | tail; rc=$? # noqa: pipe-verdict'
|
|
90
|
+
|
|
91
|
+
echo
|
|
92
|
+
if [ "$fail" -eq 0 ]; then
|
|
93
|
+
echo "[pipe-verdict-guard] ✅ all $pass known pairs hold"; exit 0
|
|
94
|
+
else
|
|
95
|
+
echo "[pipe-verdict-guard] ❌ $fail/$((pass+fail)) lanes failed"; exit 1
|
|
96
|
+
fi
|
|
@@ -143,7 +143,19 @@ if command -v script >/dev/null 2>&1; then
|
|
|
143
143
|
# The inner shell writes its rc to a FILE. Parsing `script`'s own stdout fails: it emits ^D and
|
|
144
144
|
# CRLF, and the first version of this lane read rc as empty and reported a defect that did not
|
|
145
145
|
# exist — the target was fine, the instrument was not.
|
|
146
|
-
script
|
|
146
|
+
# `script(1)` HAS TWO INCOMPATIBLE CLIs and this lane only ever spoke one of them. BSD/macOS takes
|
|
147
|
+
# `script -q <file> <cmd> [args...]`; util-linux (every Linux runner) takes `script -q -c "<cmd>"
|
|
148
|
+
# <file>`. Given the BSD form, util-linux treats `bash` and `-c` as stray operands, never runs the
|
|
149
|
+
# inner command, and writes no rc file — so the lane read rc='<unread>' and reported a defect in
|
|
150
|
+
# sidecar_wait that did not exist. Measured on the first CI run, 2026-07-31; it had been invisible
|
|
151
|
+
# because this suite had only ever executed on macOS. Same shape as the Hangul-range collation bug
|
|
152
|
+
# found in the same run: a TOOL INTERFACE that differs by platform, silently.
|
|
153
|
+
_P9CMD="SIDECAR_POLL=1 timeout 12 bash '$SW' '$TD/p9.out' 3 -- cat >'$TD/p9.txt' 2>&1; echo \$? > '$TD/p9rc'"
|
|
154
|
+
if script --version 2>&1 | grep -qi util-linux; then
|
|
155
|
+
script -q -c "$_P9CMD" /dev/null >/dev/null 2>&1 < /dev/null
|
|
156
|
+
else
|
|
157
|
+
script -q /dev/null bash -c "$_P9CMD" >/dev/null 2>&1 < /dev/null
|
|
158
|
+
fi
|
|
147
159
|
rc9=$(tr -dc '0-9' < "$TD/p9rc" 2>/dev/null)
|
|
148
160
|
if [ -n "$rc9" ] && [ "$rc9" != "124" ] && grep -q 'SIDECAR_VERDICT=' "$TD/p9.txt" 2>/dev/null; then
|
|
149
161
|
ok "P9 the tty path runs and emits a verdict (UNCALIBRATED — see note; does NOT bind the branch)"
|
|
@@ -362,6 +362,15 @@ if [ -n "$EXEC_STAGED" ] && [ -z "$DOC_STAGED" ]; then
|
|
|
362
362
|
echo ""
|
|
363
363
|
fi
|
|
364
364
|
|
|
365
|
+
# ── Half-fix propagation (advisory — MARK, never block) ──────────────────────
|
|
366
|
+
# The operator's spec for this debt is explicit: 표시(차단 아님 — 정당한 복제도 있다).
|
|
367
|
+
# templates/ ships deliberate copies of scripts/, so blocking on duplication would block correct
|
|
368
|
+
# work; and a gate that fires when the author did the right thing is a gate that gets disabled.
|
|
369
|
+
# Failing to RUN is likewise never a finding: a missing/erroring scan is silent here, because this
|
|
370
|
+
# surface is a commit (reversible) — the fail-closed direction belongs to publish and delete.
|
|
371
|
+
HALFFIX="$REPO_ROOT/scripts/halffix_propagation_scan.sh"
|
|
372
|
+
[ -f "$HALFFIX" ] && bash "$HALFFIX" 2>&1 || true
|
|
373
|
+
|
|
365
374
|
# ── Axis 1 — Regression Guard (always required) ──────────────────────────────
|
|
366
375
|
echo "[Axis 1] Regression Guard..."
|
|
367
376
|
GUARD="$REPO_ROOT/templates/regression_guard.sh"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": [
|
|
3
|
+
"PreToolUse(Bash) hook snippet — TRACKED, so it arrives with a clone. Every .claude/settings*.json",
|
|
4
|
+
"path in this repo is gitignored (confirm with `git check-ignore -v .claude/settings.json`), so a",
|
|
5
|
+
"hook registration cannot itself be tracked. This is the tracked SOURCE that install-wizard merges",
|
|
6
|
+
"into the user's local settings. Shipping the file is not the same proposition as wiring it — that",
|
|
7
|
+
"gap is exactly what #204 found (the SessionStart hook existed from 07-05 and the wizard had never",
|
|
8
|
+
"installed it, for every new user).",
|
|
9
|
+
"",
|
|
10
|
+
"WHAT IT GUARDS: reading a verdict from the wrong end of a pipe. `cmd | tail -5; echo $?` reports",
|
|
11
|
+
"TAIL's status, so a FAILED gate reads as 0 — a degrade toward PASS, the direction that never",
|
|
12
|
+
"announces itself. Measured 6x in this project 2026-07-29..07-31.",
|
|
13
|
+
"",
|
|
14
|
+
"WHY A HOOK AND NOT A FILE LINTER: the plan of record was an S6 class in degrade_direction_scan.sh.",
|
|
15
|
+
"Hand-verifying every `pipe + $?` in this repo's scripts returned 7 hits, 7 of them CORRECT. True",
|
|
16
|
+
"positives in shipped files: 0. All 6 recurrences were in interactively-composed commands, which no",
|
|
17
|
+
"repo scanner reads. The guard belongs where the defect occurs — the Bash call itself.",
|
|
18
|
+
"",
|
|
19
|
+
"ADVISORY BY DESIGN: it warns and exits 0. It never blocks a Bash call, because a mis-read verdict",
|
|
20
|
+
"is re-runnable, and a false block on a developer's own shell trains the --no-verify reflex on the",
|
|
21
|
+
"hooks that DO guard irreversible surfaces (pre-push destructive-op, pre-commit confidentiality).",
|
|
22
|
+
"Escalate deliberately with FH_PIPE_VERDICT_BLOCK=1 if a project wants it to bite.",
|
|
23
|
+
"",
|
|
24
|
+
"Carries no private path, so it belongs in .claude/settings.json (project-local), not settings.local.json.",
|
|
25
|
+
"Merge, do not overwrite: preserve any PreToolUse entries the user already has; replace only the FH",
|
|
26
|
+
"one, keyed by script name.",
|
|
27
|
+
"",
|
|
28
|
+
"Verify after wiring (known pair, both directions):",
|
|
29
|
+
" bash scripts/test_pipe_verdict_guard_lanes.sh # 15 pairs, expect all hold",
|
|
30
|
+
" printf '%s' '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"g.sh | tail; rc=$?\"}}' \\",
|
|
31
|
+
" | bash scripts/pipe_verdict_guard.sh # expect a PIPE-VERDICT R2 warning"
|
|
32
|
+
],
|
|
33
|
+
"project_settings_json": {
|
|
34
|
+
"hooks": {
|
|
35
|
+
"PreToolUse": [
|
|
36
|
+
{
|
|
37
|
+
"matcher": "Bash",
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/scripts/pipe_verdict_guard.sh\"",
|
|
42
|
+
"timeout": 5
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|