@chrono-meta/fh-gate 1.4.87 → 1.4.89
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/CLAUDE.md +36 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +37 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +125 -0
- package/package.json +4 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/degrade_direction_scan.sh +34 -2
- package/scripts/gate_pathspec_check.sh +6 -1
- package/scripts/prepush_guard_check.sh +15 -0
- package/scripts/psa_scan_lib.sh +36 -0
- package/scripts/selfcheck.sh +170 -17
- package/scripts/session_close_check.sh +55 -5
- package/scripts/test_degrade_scan_shell_probes.sh +11 -2
- package/scripts/test_dispatch_log_lanes.sh +8 -1
- package/scripts/test_selfcheck_state_lanes.sh +103 -0
- package/scripts/test_session_close_chain_lanes.sh +79 -4
- package/scripts/test_version_lockstep_lanes.sh +82 -0
- package/scripts/universal_guard_check.sh +19 -6
- package/scripts/version_lockstep_check.sh +80 -0
- package/templates/.git-hooks/pre-commit +8 -0
- package/templates/.git-hooks/pre-push +14 -2
- package/templates/degrade_direction_scan.sh +34 -2
- package/templates/regression_guard.sh +25 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Known pairs for scripts/version_lockstep_check.sh.
|
|
3
|
+
#
|
|
4
|
+
# The lane this suite exists to prevent: on 2026-08-06 a 1.4.88 → 1.4.89 bump edited the FIRST
|
|
5
|
+
# marketplace plugin entry and left the SECOND at the old version. `test_tag_version_lanes.sh`
|
|
6
|
+
# reported 8/8 PASS through it — that lane compares the git tag to package.json and never opens
|
|
7
|
+
# marketplace.json. The drift was caught by an ad-hoc `sort | uniq -c`. Luck is not a floor.
|
|
8
|
+
#
|
|
9
|
+
# Every lane runs against a THROWAWAY fixture tree, never against this repo, so the suite is
|
|
10
|
+
# hermetic: a real bump in progress can neither turn it green nor turn it red.
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
CHECK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/version_lockstep_check.sh"
|
|
14
|
+
TMPROOT=$(mktemp -d "${TMPDIR:-/tmp}/fh_lockstep.XXXXXX")
|
|
15
|
+
trap 'rm -rf "$TMPROOT"' EXIT
|
|
16
|
+
PASS=0; FAIL=0
|
|
17
|
+
|
|
18
|
+
_fixture() { # $1=name $2=pkg_ver $3=meta_ver $4=commons_ver $5=mk_entry0 $6=mk_entry1
|
|
19
|
+
local T="$TMPROOT/$1"
|
|
20
|
+
mkdir -p "$T/.claude-plugin" "$T/plugins/fh-meta/.claude-plugin" "$T/plugins/fh-commons/.claude-plugin"
|
|
21
|
+
printf '{"name":"fh","version":"%s"}\n' "$2" > "$T/package.json"
|
|
22
|
+
printf '{"name":"fh-meta","version":"%s"}\n' "$3" > "$T/plugins/fh-meta/.claude-plugin/plugin.json"
|
|
23
|
+
printf '{"name":"fh-commons","version":"%s"}\n' "$4" > "$T/plugins/fh-commons/.claude-plugin/plugin.json"
|
|
24
|
+
printf '{"plugins":[{"name":"fh-meta","version":"%s"},{"name":"fh-commons","version":"%s"}]}\n' \
|
|
25
|
+
"$5" "$6" > "$T/.claude-plugin/marketplace.json"
|
|
26
|
+
printf '%s' "$T"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_expect() { # $1=label $2=expected_rc $3=repo ; also captures OUT
|
|
30
|
+
OUT=$(bash "$CHECK" "$3" 2>&1); local rc=$?
|
|
31
|
+
if [ "$rc" = "$2" ]; then echo " ✅ $1 (rc=$rc)"; PASS=$((PASS+1))
|
|
32
|
+
else echo " ❌ $1 — rc=$rc, expected=$2"; printf '%s\n' "$OUT" | sed 's/^/ │ /'; FAIL=$((FAIL+1)); fi
|
|
33
|
+
}
|
|
34
|
+
_says() { # $1=label $2=pattern $3=expected_hit(1|0)
|
|
35
|
+
local h=0; printf '%s\n' "$OUT" | grep -q -- "$2" && h=1
|
|
36
|
+
if [ "$h" = "$3" ]; then echo " ✅ $1"; PASS=$((PASS+1))
|
|
37
|
+
else echo " ❌ $1 — hit=$h, expected=$3"; printf '%s\n' "$OUT" | sed 's/^/ │ /'; FAIL=$((FAIL+1)); fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
echo "══ version lockstep known pairs ══"
|
|
41
|
+
|
|
42
|
+
# KN — everything aligned. The control: without it, a checker that always reported DRIFT would
|
|
43
|
+
# still pass every positive lane below.
|
|
44
|
+
_expect "KN all five strings aligned → exit 0" 0 "$(_fixture kn 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)"
|
|
45
|
+
_says "KN → names the count it actually checked" "4 shipped version string" 1
|
|
46
|
+
|
|
47
|
+
# KP-1 — the measured miss, byte for byte: second marketplace entry left behind.
|
|
48
|
+
_expect "KP-1 second marketplace entry stale → exit 1" 1 "$(_fixture kp1 1.4.89 1.4.89 1.4.89 1.4.89 1.4.88)"
|
|
49
|
+
_says "KP-1 → names the offending entry by index+name" "plugins\[1\] fh-commons" 1
|
|
50
|
+
_says "KP-1 → reports both versions, not just 'mismatch'" "1.4.88 (package.json = 1.4.89)" 1
|
|
51
|
+
|
|
52
|
+
# KP-2 — a plugin.json left behind. This is the shape the tag lane also cannot see.
|
|
53
|
+
_expect "KP-2 plugin.json stale → exit 1" 1 "$(_fixture kp2 1.4.89 1.4.88 1.4.89 1.4.89 1.4.89)"
|
|
54
|
+
_says "KP-2 → names the file" "plugins/fh-meta/.claude-plugin/plugin.json" 1
|
|
55
|
+
|
|
56
|
+
# KP-3 — the reverse direction: package.json bumped alone. Same defect, opposite author error.
|
|
57
|
+
_expect "KP-3 only package.json bumped → exit 1" 1 "$(_fixture kp3 1.5.0 1.4.89 1.4.89 1.4.89 1.4.89)"
|
|
58
|
+
_says "KP-3 → all four downstream strings flagged" "4 of 4" 1
|
|
59
|
+
|
|
60
|
+
# HARNESS-ERROR — an instrument that could not look has not looked. These must be a THIRD state,
|
|
61
|
+
# never folded into PASS: this gate guards `npm publish`, an irreversible surface.
|
|
62
|
+
T=$(_fixture he1 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); rm "$T/.claude-plugin/marketplace.json" "$T"/plugins/*/.claude-plugin/plugin.json
|
|
63
|
+
_expect "HE-1 no manifests at all → exit 2, NOT 0" 2 "$T"
|
|
64
|
+
_says "HE-1 → says alignment is UNKNOWN, not aligned" "UNKNOWN, not aligned" 1
|
|
65
|
+
|
|
66
|
+
T=$(_fixture he2 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); printf '{ broken' > "$T/.claude-plugin/marketplace.json"
|
|
67
|
+
_expect "HE-2 unparseable manifest → exit 2, NOT 0" 2 "$T"
|
|
68
|
+
|
|
69
|
+
T=$(_fixture he3 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); rm "$T/package.json"
|
|
70
|
+
_expect "HE-3 missing package.json → exit 2, NOT 0" 2 "$T"
|
|
71
|
+
|
|
72
|
+
# A manifest present but carrying no version at all is absence, not agreement.
|
|
73
|
+
T=$(_fixture he4 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); printf '{"name":"fh-meta"}\n' > "$T/plugins/fh-meta/.claude-plugin/plugin.json"
|
|
74
|
+
_expect "HE-4 manifest with no version → exit 2, NOT 0" 2 "$T"
|
|
75
|
+
_says "HE-4 → says which file carries none" "carries no version string" 1
|
|
76
|
+
|
|
77
|
+
echo
|
|
78
|
+
echo "──────────────────────────────────────────────"
|
|
79
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
80
|
+
echo "VERSION-LOCKSTEP LANES: FAIL ($PASS pass · $FAIL fail)"; exit 1
|
|
81
|
+
fi
|
|
82
|
+
echo "VERSION-LOCKSTEP LANES: PASS ($PASS/$PASS)"
|
|
@@ -196,8 +196,12 @@ run_case "placeholder BEFORE real, one line → BLOCK" \
|
|
|
196
196
|
# Each of these previously passed at commit time while BLOCKING at publish time — the two copies
|
|
197
197
|
# of this logic had diverged in leniency. run_state_case swaps the pattern source rather than the
|
|
198
198
|
# staged content, so it needs its own runner.
|
|
199
|
-
run_state_case() { # <name> <override-content|__NONE__> <defaults:keep|drop> <expect-exit: block|pass>
|
|
200
|
-
|
|
199
|
+
run_state_case() { # <name> <override-content|__NONE__> <defaults:keep|drop> <expect-exit: block|pass> [expect-verdict-substring]
|
|
200
|
+
# The 5th arg exists because exit code alone cannot see this defect class: a scan whose
|
|
201
|
+
# operator-literal layer never ran exits 0 and used to print the same `✅ PASS` as a scan where
|
|
202
|
+
# every layer ran. Identical verdicts for different amounts of measurement is the fail-open —
|
|
203
|
+
# so the anchor has to read the verdict LINE, not just the status.
|
|
204
|
+
local name="$1" ovc="$2" defmode="$3" expect="$4" want="${5:-}" out rc got
|
|
201
205
|
printf 'operator literal zzsynthoperator\n' > "$SANDBOX/README.md"
|
|
202
206
|
git -C "$SANDBOX" add -- README.md >/dev/null 2>&1
|
|
203
207
|
local ov="$SANDBOX/.psa_state_override"
|
|
@@ -208,17 +212,26 @@ run_state_case() { # <name> <override-content|__NONE__> <defaults:keep|drop> <e
|
|
|
208
212
|
if [ "$defmode" = drop ]; then mv "$defbak" "$SANDBOX/.claude/rules/.public-surface-patterns.defaults" 2>/dev/null; fi
|
|
209
213
|
git -C "$SANDBOX" rm -q --cached -- README.md >/dev/null 2>&1; rm -f "$SANDBOX/README.md" "$ov"
|
|
210
214
|
if [ "$rc" -ne 0 ]; then got=block; else got=pass; fi
|
|
211
|
-
if [ "$got"
|
|
212
|
-
echo " ✅ $name (expected $expect)"
|
|
213
|
-
else
|
|
215
|
+
if [ "$got" != "$expect" ]; then
|
|
214
216
|
echo " ❌ $name — expected $expect, got $got (exit $rc)"
|
|
215
217
|
printf '%s\n' "$out" | sed 's/^/ | /' | head -10
|
|
216
218
|
FAILED=1
|
|
219
|
+
elif [ -n "$want" ] && ! printf '%s' "$out" | grep -qF "$want"; then
|
|
220
|
+
echo " ❌ $name — exit was $got but the verdict line did not say: $want"
|
|
221
|
+
printf '%s\n' "$out" | grep -E 'PASS|PARTIAL|Confidentiality' | sed 's/^/ | /' | head -5
|
|
222
|
+
FAILED=1
|
|
223
|
+
else
|
|
224
|
+
echo " ✅ $name (expected $expect${want:+, verdict names \"$want\"})"
|
|
217
225
|
fi
|
|
218
226
|
}
|
|
219
227
|
run_state_case "no patterns at all (instrument down) → BLOCK" "__NONE__" drop block
|
|
220
228
|
run_state_case "malformed regex in override → BLOCK" "HIGH zzsynth[" keep block
|
|
221
|
-
|
|
229
|
+
# 2026-08-06 — this state still PASSES (a commit is reversible; blocking every fresh clone's first
|
|
230
|
+
# commit is what trains the override into a reflex), but it may no longer CLAIM a clean scan. Pinned
|
|
231
|
+
# as a pair so the label cannot drift back to a bare PASS, and so an unconditional PARTIAL — which
|
|
232
|
+
# would be just as wrong — is caught by the control below it.
|
|
233
|
+
run_state_case "empty override + defaults present → PASS " "" keep pass "PARTIAL"
|
|
234
|
+
run_state_case "control: override PRESENT, no hit → PASS " "HIGH zzunrelatedliteral" keep pass "✅ PASS"
|
|
222
235
|
|
|
223
236
|
# ── Pair 7: pattern-ROW malformations that are not invalid regex. Each of these used to be skipped
|
|
224
237
|
# in silence, i.e. a detector the author believed in that never existed, and a scan that certified
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# version_lockstep_check.sh — every shipped manifest must carry package.json's version.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists (measured 2026-08-06, while republishing 1.4.88 → 1.4.89):
|
|
5
|
+
# CLAUDE.md §④-b says to bump "package.json + every .claude-plugin/plugin.json + marketplace.json".
|
|
6
|
+
# That instruction reads as four files, but `.claude-plugin/marketplace.json` carries **one version
|
|
7
|
+
# per plugin entry** — five strings across four files. A bump that edited the first entry left the
|
|
8
|
+
# second at the old version, and `test_tag_version_lanes.sh` passed 8/8 anyway: that lane compares
|
|
9
|
+
# the git TAG to package.json and never opens marketplace.json. The stale entry was caught by an
|
|
10
|
+
# ad-hoc `sort | uniq -c`, i.e. by luck, not by an instrument.
|
|
11
|
+
#
|
|
12
|
+
# Codex caches plugins on the plugin.json version, so a stale entry ships a plugin that the other
|
|
13
|
+
# runtime believes it already has — the drift is silent on exactly the surface that cannot report it.
|
|
14
|
+
#
|
|
15
|
+
# Exit 0 = all aligned · exit 1 = drift (names every offending file:line) · exit 2 = harness error
|
|
16
|
+
# (a manifest is missing or unparseable — NOT a pass; an instrument that could not look has not
|
|
17
|
+
# looked, and this gate guards an irreversible surface).
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
21
|
+
|
|
22
|
+
command -v python3 >/dev/null 2>&1 || { echo "LOCKSTEP: HARNESS-ERROR — python3 unavailable"; exit 2; }
|
|
23
|
+
|
|
24
|
+
python3 - "$ROOT" <<'PY'
|
|
25
|
+
import json, sys, glob, os
|
|
26
|
+
root = sys.argv[1]
|
|
27
|
+
pkg_path = os.path.join(root, 'package.json')
|
|
28
|
+
try:
|
|
29
|
+
want = json.load(open(pkg_path))['version']
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"LOCKSTEP: HARNESS-ERROR — cannot read {pkg_path}: {e}")
|
|
32
|
+
sys.exit(2)
|
|
33
|
+
|
|
34
|
+
# (path, list-of-(label, version)) — every manifest that ships a version string.
|
|
35
|
+
targets = []
|
|
36
|
+
for p in sorted(glob.glob(os.path.join(root, 'plugins', '*', '.claude-plugin', 'plugin.json'))):
|
|
37
|
+
targets.append(p)
|
|
38
|
+
mk = os.path.join(root, '.claude-plugin', 'marketplace.json')
|
|
39
|
+
if os.path.exists(mk):
|
|
40
|
+
targets.append(mk)
|
|
41
|
+
|
|
42
|
+
if not targets:
|
|
43
|
+
# Absence is not alignment. If the manifests vanished, say so rather than reporting a clean run.
|
|
44
|
+
print("LOCKSTEP: HARNESS-ERROR — no plugin/marketplace manifest found; alignment is UNKNOWN, not aligned")
|
|
45
|
+
sys.exit(2)
|
|
46
|
+
|
|
47
|
+
drift, checked = [], 0
|
|
48
|
+
for p in targets:
|
|
49
|
+
try:
|
|
50
|
+
d = json.load(open(p))
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"LOCKSTEP: HARNESS-ERROR — cannot parse {p}: {e}")
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
rel = os.path.relpath(p, root)
|
|
55
|
+
# A manifest carries a version either at the top level (plugin.json) or once per plugin entry
|
|
56
|
+
# (marketplace.json). Walk both shapes so a new entry cannot be silently uncovered.
|
|
57
|
+
found = []
|
|
58
|
+
if isinstance(d.get('version'), str):
|
|
59
|
+
found.append(('(top-level)', d['version']))
|
|
60
|
+
for key in ('plugins', 'entries'):
|
|
61
|
+
for i, ent in enumerate(d.get(key) or []):
|
|
62
|
+
if isinstance(ent, dict) and isinstance(ent.get('version'), str):
|
|
63
|
+
found.append((f"{key}[{i}] {ent.get('name', '?')}", ent['version']))
|
|
64
|
+
if not found:
|
|
65
|
+
print(f"LOCKSTEP: HARNESS-ERROR — {rel} carries no version string; expected at least one")
|
|
66
|
+
sys.exit(2)
|
|
67
|
+
for label, got in found:
|
|
68
|
+
checked += 1
|
|
69
|
+
if got != want:
|
|
70
|
+
drift.append(f" {rel} :: {label} = {got} (package.json = {want})")
|
|
71
|
+
|
|
72
|
+
if drift:
|
|
73
|
+
print(f"LOCKSTEP: DRIFT — {len(drift)} of {checked} shipped version string(s) do not match package.json")
|
|
74
|
+
print("\n".join(drift))
|
|
75
|
+
print("Bump every string above in the same commit — Codex caches plugins on this version,")
|
|
76
|
+
print("so a stale entry ships as 'already installed' on the runtime that cannot report it.")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
print(f"LOCKSTEP: PASS — {checked} shipped version string(s) all at {want}")
|
|
80
|
+
PY
|
|
@@ -303,6 +303,14 @@ else
|
|
|
303
303
|
echo " → '~' or '{project}'), or PUBLIC_SURFACE_OK=1 git commit … for a reviewed mention."
|
|
304
304
|
FAILED=1
|
|
305
305
|
fi
|
|
306
|
+
elif [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
|
|
307
|
+
# NOT `✅ PASS`. The scan found nothing, but one whole layer of it — the operator/company
|
|
308
|
+
# literals — never ran. A missing measurement is not a zero (CLAUDE.md §Instrument
|
|
309
|
+
# Calibration), and a verdict that reads identically whether a layer ran or not is the
|
|
310
|
+
# fail-open this repo names. The commit is still allowed: a commit is reversible, so the
|
|
311
|
+
# degrade here is advisory by the Surface-Class Degrade Invariant. Push and publish, which
|
|
312
|
+
# are the acts that make content public, block this same state.
|
|
313
|
+
echo " ⚠️ PARTIAL — no hit in the committed defaults; company/companion literals UNMEASURED."
|
|
306
314
|
else
|
|
307
315
|
echo " ✅ PASS"
|
|
308
316
|
fi
|
|
@@ -298,9 +298,21 @@ else
|
|
|
298
298
|
_pp_why=""
|
|
299
299
|
[ "$_PP_APPLICABLE" -eq 1 ] && [ "$PSA_DEFAULTS_OK" -eq 0 ] && _pp_why="committed pattern defaults unreadable/empty"
|
|
300
300
|
[ "$_PP_APPLICABLE" -eq 1 ] && [ "$PSA_BAD_ROWS" -gt 0 ] && _pp_why="${_pp_why:+$_pp_why; }$PSA_BAD_ROWS unusable pattern row(s)"
|
|
301
|
+
# 2026-08-06 — the 07-26 warn stays for the case it was right about, and is SCOPED, not reverted.
|
|
302
|
+
# An absent override is a legitimate configuration gap in a fresh clone / CI runner / worktree (the
|
|
303
|
+
# file is gitignored, so it is absent there by construction) → WARN, exactly as 07-26 concluded.
|
|
304
|
+
# In an operator-configured checkout the same state is missing evidence, not a missing config, and
|
|
305
|
+
# this is an irreversible surface → BLOCK, matching what `public_surface_scan_files.sh` already does
|
|
306
|
+
# at `npm publish`. Two irreversible surfaces degrading in opposite directions on the same state was
|
|
307
|
+
# the actual defect; git push was the lenient one.
|
|
308
|
+
psa_detect_operator_context "$REPO_ROOT"
|
|
301
309
|
if [ "$_PP_APPLICABLE" -eq 1 ] && [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
if [ "$PSA_OPERATOR_CONTEXT" -eq 1 ]; then
|
|
311
|
+
_pp_why="${_pp_why:+$_pp_why; }operator-literal override absent in an operator-configured checkout (company/companion literals NOT scanned)"
|
|
312
|
+
else
|
|
313
|
+
echo " ⚠️ operator-literal override absent — only the committed defaults (home paths + credential"
|
|
314
|
+
echo " shapes) are active. Populate .claude/rules/.public-surface-patterns for company literals."
|
|
315
|
+
fi
|
|
304
316
|
fi
|
|
305
317
|
if [ -n "$_pp_why" ]; then
|
|
306
318
|
if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
|
|
@@ -164,9 +164,41 @@ for f in "${FILES[@]}"; do
|
|
|
164
164
|
# yields "9\n0" and the `-gt` guard goes silent. Verified as a known pair (both directions) in
|
|
165
165
|
# scripts/test_degrade_scan_shell_probes.sh; narrowing without that anchor would just trade a
|
|
166
166
|
# noisy probe for a blind one.
|
|
167
|
+
#
|
|
168
|
+
# WIDENED 2026-08-04 — the narrowing had gone one step too far, and the proof is that this
|
|
169
|
+
# probe was BLIND to the live instance that bit us the day before. PR #251 fixed
|
|
170
|
+
# `grep -c … | tr -d ' ' || echo 0` in session_close_check.sh ④-e; running this scanner over
|
|
171
|
+
# the pre-#251 file produced ZERO hits (re-measured: fixed rule = 2, old rule = 0).
|
|
172
|
+
#
|
|
173
|
+
# THE ANCHOR WAS ON THE WRONG PROPERTY. The old regex asked "is there an upstream pipe" and
|
|
174
|
+
# required the counter to be the LAST stage. Neither is the discriminator. The discriminator
|
|
175
|
+
# is: **does the failing side still EMIT?** `grep -c` prints "0" and exits 1 on no-match, so it
|
|
176
|
+
# disarms with no upstream pipe at all, and any trailing stage that passes that "0" through
|
|
177
|
+
# keeps the "0\n0" intact.
|
|
178
|
+
#
|
|
179
|
+
# A first fix widened to a NAMED filter list (tr|head|sed|…). Cross-family review (gpt-5.5,
|
|
180
|
+
# 2026-08-04) refuted it with reproduced counter-examples, and the refutation held on
|
|
181
|
+
# measurement in BOTH directions:
|
|
182
|
+
# · still missed, all verified to yield "0\n0": `grep -Ec …`, `grep --count …`, `grep -Fcx …`
|
|
183
|
+
# (combined/long flag forms the `-c` literal never matched) and `| cat`.
|
|
184
|
+
# · newly over-matched: `| tail -n +2` after the counter yields ONE line — not a disarm.
|
|
185
|
+
# Name-based approximation produced under-match and over-match simultaneously. So the trailing
|
|
186
|
+
# chain is no longer enumerated: the rule keys on the COUNTER (grep with a `c` in any flag
|
|
187
|
+
# cluster or `--count`; or `wc` behind a real pipe) plus the `|| echo 0` fallback, and accepts
|
|
188
|
+
# any chain between them.
|
|
189
|
+
#
|
|
190
|
+
# PRESERVED from the 2026-07-28 narrowing, still pinned as known-negatives: (a) `a || b ||
|
|
191
|
+
# echo 0` has no pipeline, and (b) a stage that emits NOTHING on failure (`| jq -r … ||
|
|
192
|
+
# echo 0`) — there the fallback supplies the only line, as intended.
|
|
193
|
+
# KNOWN RESIDUAL, stated not hidden: a trailing stage that can swallow the counter's output
|
|
194
|
+
# (`| tail -n +2`, `| sed -n '/[1-9]/p'`) is now flagged though it is not a disarm. That is a
|
|
195
|
+
# deliberate recall-over-precision trade on an ADVISORY probe, taken because the measured cost
|
|
196
|
+
# of the other direction was a real defect shipping. Revisit if non-fixture FPs appear.
|
|
197
|
+
# Known pair: scripts/test_degrade_scan_shell_probes.sh (9 positives / negatives silent).
|
|
198
|
+
# Reverting this line turns the positive lane red — checked by applying the revert.
|
|
167
199
|
while IFS= read -r m; do
|
|
168
|
-
emit "$f" "${m%%:*}" "S5:pipefail-fallback(sh)" "\`|| echo 0\` fallback
|
|
169
|
-
done < <(grep -nE '[^|]\|[[:space:]]*([a-z]+[[:space:]]+)*(grep[
|
|
200
|
+
emit "$f" "${m%%:*}" "S5:pipefail-fallback(sh)" "\`|| echo 0\` fallback after a COUNTER (grep with -c / --count, or wc) — the counter PRINTS \"0\" and exits non-zero on no-match, so the fallback appends a SECOND line, the value becomes \"0\\n0\", the integer test dies with 'integer expression expected' (stderr only) and the guard goes silent; split it and sanitize: n=\$(...); n=\${n:-0}; case \$n in *[!0-9]*) n=0;; esac. VERIFY the trailing stage can actually emit — if it swallows the count (| tail -n +2), this is a known false positive"
|
|
201
|
+
done < <(grep -nE '(([^|]\|[[:space:]]*([a-z]+[[:space:]]+)*wc[^|]*)|(grep([[:space:]]+-[A-Za-z]*c[A-Za-z]*|[[:space:]]+--count)[^|]*))([^|]*\|[^|]*)*\|\|[[:space:]]*echo[[:space:]]+[\"'"'"']?0' "$f" 2>/dev/null \
|
|
170
202
|
| grep -vE '^[0-9]+:[[:space:]]*#' \
|
|
171
203
|
| grep -vE '#[[:space:]]*noqa[:[:space:]]*degrade')
|
|
172
204
|
fi
|
|
@@ -117,6 +117,31 @@ GUARD_PATHSPEC=(
|
|
|
117
117
|
'AGENTS.md'
|
|
118
118
|
'docs/*.md'
|
|
119
119
|
'templates/*.md'
|
|
120
|
+
# ── Added 2026-08-04 — the pathspec had drifted from the 4-axis ASSET LIST it exists to gate.
|
|
121
|
+
# Measured that day by mechanically walking every asset class in CLAUDE.md §FH Improvement
|
|
122
|
+
# 4-Axis against this array: four classes had NO covering pattern, and two of them are this
|
|
123
|
+
# repo's own mechanical floor — `templates/.git-hooks/*` (the hook that hard-blocks commits)
|
|
124
|
+
# and `templates/*.sh` (THIS FILE — the guard could not guard itself). The pre-commit HEAVY
|
|
125
|
+
# classifier already listed `^scripts/.*\.sh$`, so a scripts-only change entered the heavy
|
|
126
|
+
# path and then met Axis 1 returning `SKIP (not-checked, NOT a pass)`. Observed three times
|
|
127
|
+
# on 2026-08-04 alone, the third being the S5 fix (PR #253) — a change to a verdict-surface
|
|
128
|
+
# scanner that shipped with zero Axis-1 coverage.
|
|
129
|
+
#
|
|
130
|
+
# Calibrated before shipping, both directions (a widened copy, run against real history):
|
|
131
|
+
# · 12 consecutive historical commits touching scripts/*.sh → M=0 S=0 (no FP storm), and a
|
|
132
|
+
# control confirmed the files were actually SELECTED (4 files checked) rather than the
|
|
133
|
+
# zero coming from an inert pathspec — the unwired-instrument reading of a clean 0.
|
|
134
|
+
# · known-positive: truncating a shipped .sh by 60% → M-TIER BLOCK (`BLOCK` token dropped
|
|
135
|
+
# 100%, 60% line reduction). The md-shaped checks (F1 frontmatter, F2 sections, F3 fences)
|
|
136
|
+
# are inert on shell files; F4 keyword / F5 xref / F6 line-loss / F7 syntax are the ones
|
|
137
|
+
# that carry, and they are exactly the regressions that matter in a gate script.
|
|
138
|
+
# NOTE on globbing: git pathspec `*` crosses `/` (verified here — `templates/*.md` matches
|
|
139
|
+
# `templates/.claude/rules/session.md`), so these single-star forms are recursive.
|
|
140
|
+
'scripts/*.sh'
|
|
141
|
+
'templates/*.sh'
|
|
142
|
+
'templates/.git-hooks/*'
|
|
143
|
+
'plugins/*/agents/*.md'
|
|
144
|
+
'.claude/agents/*.md'
|
|
120
145
|
)
|
|
121
146
|
|
|
122
147
|
# 계기 무결: base ref 가 안 풀리면 diff 실패가 2>/dev/null 로 삼켜져 CHANGED 공백 →
|