@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.
@@ -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 -q /dev/null bash -c "SIDECAR_POLL=1 timeout 12 bash '$SW' '$TD/p9.out' 3 -- cat >'$TD/p9.txt' 2>&1; echo \$? > '$TD/p9rc'" >/dev/null 2>&1 < /dev/null
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
+ }