@chrono-meta/fh-gate 1.4.87 → 1.4.88
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/learnings/subagent_invocations_log.yaml +52 -0
- package/package.json +1 -1
- 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/selfcheck.sh +32 -0
- package/scripts/test_degrade_scan_shell_probes.sh +11 -2
- package/scripts/test_dispatch_log_lanes.sh +8 -1
- package/templates/degrade_direction_scan.sh +34 -2
- package/templates/regression_guard.sh +25 -0
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "fh-meta",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.88",
|
|
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.88",
|
|
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
|
}
|
|
@@ -944,3 +944,55 @@
|
|
|
944
944
|
entry per the "counts and outcomes, not one stub per dispatch" rule.
|
|
945
945
|
Instrument note: the invocation form matters — `| codex exec -` (stdin) as recorded in
|
|
946
946
|
[[feedback_sidecar_liveness_not_passive_wait]]; the argv form hangs nondeterministically.
|
|
947
|
+
|
|
948
|
+
- date: '2026-08-04'
|
|
949
|
+
agent: codex/gpt-5.5 (cross-family sidecar, auto-decorrelation)
|
|
950
|
+
context: >-
|
|
951
|
+
FH self-dev — two load-bearing gate changes (PR #253 degrade-scan S5 rule, PR #254/#255 Axis-1
|
|
952
|
+
pathspec parity + anchor wiring). Pre-push gate demanded a cross-family leg on both.
|
|
953
|
+
dispatches: '3 (2 completed, 1 killed on timeout and re-run)'
|
|
954
|
+
outcome: accepted
|
|
955
|
+
evidence: >-
|
|
956
|
+
Round 1 (PR #253): REFUTED the author's own fix. The first repair widened the S5 rule with a
|
|
957
|
+
NAMED filter list; codex produced reproduced counter-examples in BOTH directions — still-missed
|
|
958
|
+
real disarms (`grep -Ec`, `grep --count`, `grep -Fcx`, `| cat`) and a NEW false positive the
|
|
959
|
+
repair introduced (`| tail -n +2` yields one line, not a disarm). It also caught that a fixture
|
|
960
|
+
labelled known-NEGATIVE was in fact a positive, because its upstream succeeded and therefore
|
|
961
|
+
never exercised the failure path it claimed to pin. All four re-run locally before acceptance.
|
|
962
|
+
Round 2 (PR #255 wiring): no blocker; one residual raised ("a distribution that SHOULD be
|
|
963
|
+
complete but drops a subject reports SKIP, not FAIL"), which was then TESTED and confirmed
|
|
964
|
+
uncovered by any existing anchor. Recorded as a WEAK signal — the auditor stated it could not
|
|
965
|
+
trace F1-F3 because those implementations were not in the diff it was given, and that residual
|
|
966
|
+
was closed locally by measurement, not by the auditor.
|
|
967
|
+
note: >-
|
|
968
|
+
Third consecutive session where the cross-family leg produced net-new findings the same-family
|
|
969
|
+
round walked past. New this session: the two rounds differed in SIGNAL STRENGTH (refutation vs
|
|
970
|
+
nothing-found), and both markers record which one it was — recording them at equal weight would
|
|
971
|
+
launder a weak pass into a strong one.
|
|
972
|
+
Instrument note: the FIRST attempt derailed into its own sidecar-availability exploration and
|
|
973
|
+
timed out (exit 124, ~7 min). Re-running with an explicit "Do NOT run shell commands, do NOT use
|
|
974
|
+
tools, answer ONLY from the text below" preamble fixed it. Stdin form `codex exec -m gpt-5.5
|
|
975
|
+
--skip-git-repo-check -` as recorded in [[feedback_sidecar_liveness_not_passive_wait]].
|
|
976
|
+
These were CLI sidecars, not Agent-tool spawns, so the SubagentStop hook tallied ZERO — the
|
|
977
|
+
④-e check passed on a true zero while three real dispatches had happened. Named gap: the tally
|
|
978
|
+
is blind to CLI-invoked sidecars.
|
|
979
|
+
|
|
980
|
+
- date: '2026-08-05'
|
|
981
|
+
agent: (unattributed — SubagentStop tally only)
|
|
982
|
+
context: >-
|
|
983
|
+
Close chain of the 2026-08-04 session, which crossed midnight. The tally file recorded ONE
|
|
984
|
+
dispatch dated 2026-08-05; this session made no Agent-tool spawn that it is aware of.
|
|
985
|
+
dispatches: '1 (tallied, origin not attributable)'
|
|
986
|
+
outcome: sustained
|
|
987
|
+
evidence: >-
|
|
988
|
+
Recorded rather than guessed. The substantive sidecar work of this session is the 2026-08-04
|
|
989
|
+
entry above (3 codex CLI dispatches). MEASURED here, after the claim had already been written
|
|
990
|
+
into a commit message and the session card: a `codex exec` call does NOT increment the tally
|
|
991
|
+
(before=1, after=1 on `grep -c '^2026-08-05$'`), and the hook matcher is `SubagentStop`, i.e.
|
|
992
|
+
Agent-tool spawns only. So the CLI-blindness claim holds — but its evidence arrived AFTER
|
|
993
|
+
publication, which is the ordering CLAUDE.md §Instrument-Calibration forbids.
|
|
994
|
+
note: >-
|
|
995
|
+
What produced the 4 tallies dated 2026-08-04 and this 1 dated 2026-08-05 is UNKNOWN. Not
|
|
996
|
+
invented: a fabricated outcome would poison the 60/40 promotion gate worse than a missing one,
|
|
997
|
+
which is the stated reason the hook only tallies and never writes an entry. Open question for
|
|
998
|
+
the next session that touches the tally: which tool events actually fire SubagentStop here.
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -107,7 +107,12 @@ spec_matches() { # $1 = path
|
|
|
107
107
|
for pair in \
|
|
108
108
|
"plugins/fh-meta/skills/frontier-digest/SKILL_detail.md|tracks/_meta/x.md|PATHSPEC covers SKILL_detail.md" \
|
|
109
109
|
"plugins/fh-meta/skills/frontier-digest/SKILL.md|README.md|PATHSPEC still covers SKILL.md" \
|
|
110
|
-
"CLAUDE.md|CLAUDE.local.md|PATHSPEC covers CLAUDE.md but not the local override"
|
|
110
|
+
"CLAUDE.md|CLAUDE.local.md|PATHSPEC covers CLAUDE.md but not the local override" \
|
|
111
|
+
"scripts/selfcheck.sh|README.md|PATHSPEC covers scripts/*.sh (seam #1 — the HEAVY term already gated it, this array did not)" \
|
|
112
|
+
"scripts/sub/nested.sh|package.json|PATHSPEC covers scripts/ recursively (git/bash globs cross '/')" \
|
|
113
|
+
"templates/regression_guard.sh|package-lock.json|PATHSPEC covers templates/*.sh — i.e. the guard can guard ITSELF" \
|
|
114
|
+
"templates/.git-hooks/pre-commit|.gitignore|PATHSPEC covers the git-hook floor (the hook that hard-blocks commits)" \
|
|
115
|
+
"plugins/fh-meta/agents/challenger.md|tracks/_meta/y.md|PATHSPEC covers agent definitions (seam #3)"
|
|
111
116
|
do
|
|
112
117
|
IFS='|' read -r pos neg label <<< "$pair"
|
|
113
118
|
ok=1
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -224,6 +224,38 @@ else
|
|
|
224
224
|
fail=1
|
|
225
225
|
fi
|
|
226
226
|
|
|
227
|
+
# gate-pathspec anchor — wired here 2026-08-04. It was reachable ONLY from templates/.git-hooks/
|
|
228
|
+
# pre-commit, i.e. only in a clone where the operator had run `git config core.hooksPath`. Every
|
|
229
|
+
# other clone, every CI run, and the npm package carried the anchor file and never executed it —
|
|
230
|
+
# the built-but-not-wired shape, one layer up: the anchor for the gate had no anchor of its own.
|
|
231
|
+
# That mattered the same day: PR #254 added five known-pairs to it, all of which would have been
|
|
232
|
+
# unexecuted outside the author's machine.
|
|
233
|
+
# Subject = the two implementations it reads (the hook's HEAVY term and the guard's GUARD_PATHSPEC).
|
|
234
|
+
# Absent subject → package/partial surface → legitimate SKIP; present subject with the anchor gone
|
|
235
|
+
# → FAIL, same shape as every block above.
|
|
236
|
+
# NAMED RESIDUAL (cross-family, gpt-5.5, 2026-08-04): if a distribution that SHOULD be complete
|
|
237
|
+
# accidentally drops one subject, this reports SKIP, not FAIL — silent non-coverage. Measured the
|
|
238
|
+
# same day: removing `templates/.git-hooks` from package.json `files[]` and running
|
|
239
|
+
# scripts/package_coverage_check.sh still PASSED, so no existing anchor catches that omission
|
|
240
|
+
# either. Deliberately NOT patched with a stricter branch: the only discriminator available
|
|
241
|
+
# ("templates/ exists but the hook does not") would be built on an UNMEASURED assumption about how
|
|
242
|
+
# a narrower package is shaped, and this repo's rule is not to build before the constraint is
|
|
243
|
+
# measured. What is cheap and honest is naming WHICH subject is missing, so a SKIP is diagnosable
|
|
244
|
+
# instead of opaque. Revisit when a real partial distribution is observed.
|
|
245
|
+
_gps_missing=""
|
|
246
|
+
[ -f templates/.git-hooks/pre-commit ] || _gps_missing="templates/.git-hooks/pre-commit"
|
|
247
|
+
[ -f templates/regression_guard.sh ] || _gps_missing="${_gps_missing:+$_gps_missing, }templates/regression_guard.sh"
|
|
248
|
+
if [ -n "$_gps_missing" ]; then
|
|
249
|
+
echo "SKIP gate_pathspec_check.sh (subject absent: $_gps_missing) — not-checked, NOT a pass"
|
|
250
|
+
elif [ -f scripts/gate_pathspec_check.sh ]; then
|
|
251
|
+
if ! bash scripts/gate_pathspec_check.sh; then
|
|
252
|
+
fail=1
|
|
253
|
+
fi
|
|
254
|
+
else
|
|
255
|
+
echo "FAIL gate_pathspec_check.sh: the gate implementations are present but their coverage anchor is missing"
|
|
256
|
+
fail=1
|
|
257
|
+
fi
|
|
258
|
+
|
|
227
259
|
if [ ! -f scripts/ablation_calibrate.sh ]; then
|
|
228
260
|
echo "SKIP test_ablation_calibrate_lanes.sh (subject scripts/ablation_calibrate.sh absent)"
|
|
229
261
|
elif [ -f scripts/test_ablation_calibrate_lanes.sh ]; then
|
|
@@ -163,6 +163,15 @@ cat > "$TMP/s5_positive.sh" <<'EOF'
|
|
|
163
163
|
set -uo pipefail
|
|
164
164
|
N=$(find /nope . -maxdepth 1 2>/dev/null | grep -c . || echo 0)
|
|
165
165
|
M=$(git log --oneline 2>/dev/null | wc -l || echo 0)
|
|
166
|
+
# WIDENED 2026-08-04. Every line below was INVISIBLE to the narrowed rule, and each was verified to
|
|
167
|
+
# actually produce "0\n0" before being pinned here (line count measured, not assumed):
|
|
168
|
+
P=$(cat /etc/hosts | grep -c . | tr -d ' ' || echo 0) # transparent filter after the counter
|
|
169
|
+
Q=$(grep -c "^nosuchline$" /etc/hosts 2>/dev/null | tr -d ' ' || echo 0) # the PR #251 shape
|
|
170
|
+
R=$(grep -Ec "^nosuchline$" /etc/hosts || echo 0) # combined flag cluster -Ec
|
|
171
|
+
S=$(grep --count "^nosuchline$" /etc/hosts || echo 0) # long option
|
|
172
|
+
T=$(grep -Fcx "nosuchline" /etc/hosts || echo 0) # -Fcx
|
|
173
|
+
U=$(false | grep -c . | cat || echo 0) # trailing stage that always emits
|
|
174
|
+
V=$(false | grep -c . | grep -v nosuch || echo 0) # trailing grep whose pattern misses the "0"
|
|
166
175
|
EOF
|
|
167
176
|
cat > "$TMP/s5_negative.sh" <<'EOF'
|
|
168
177
|
#!/usr/bin/env bash
|
|
@@ -174,8 +183,8 @@ J=$(printf '%s' "$x" | jq -r '.a // 0' 2>/dev/null || echo 0)
|
|
|
174
183
|
# A comment describing the defect must not be scored as the defect: cmd | grep -c . || echo 0
|
|
175
184
|
EOF
|
|
176
185
|
n=$(s_hits "$TMP/s5_positive.sh")
|
|
177
|
-
[ "$n" -eq
|
|
178
|
-
|
|
186
|
+
[ "$n" -eq 9 ] && ok "S5 known-positive: 9/9 — incl. -Ec/--count/-Fcx flag clusters, trailing \`| cat\`/\`| grep -v\`, and the no-upstream-pipe form" \
|
|
187
|
+
|| bad "S5 known-positive: expected 9 S-hits, got $n — 2 = the pre-2026-08-04 rule (pipe-presence anchor); 4 = the first widening, which still missed every combined flag cluster (\`-Ec\`, \`--count\`, \`-Fcx\`) and any trailing stage outside a hardcoded name list"
|
|
179
188
|
|
|
180
189
|
n=$(s_hits "$TMP/s5_negative.sh")
|
|
181
190
|
[ "$n" -eq 0 ] && ok "S5 known-negative: \`||\` chains, empty-on-failure pipelines and comments stay silent" \
|
|
@@ -75,7 +75,14 @@ PY
|
|
|
75
75
|
[ "$ok" -eq 0 ] ; chk $? "hook ends in exit 0 — a non-zero hook exit discards its stdout SILENTLY"
|
|
76
76
|
# run it against a scratch HUB and confirm it appends today's date
|
|
77
77
|
CLAUDE_PROJECT_DIR="$T/hub" bash -c "$cmd" >/dev/null 2>&1
|
|
78
|
-
|
|
78
|
+
# Split + sanitize, not `grep -c … || echo 0`: on no-match `grep -c` PRINTS "0" and exits 1, so
|
|
79
|
+
# the fallback appends a SECOND line and `[ -ge ]` dies with "integer expression expected".
|
|
80
|
+
# Here that error happens to land on the FAIL branch — honest scope: this was never a live
|
|
81
|
+
# fail-open, it was a verdict reached by a bash error instead of a comparison, one refactor
|
|
82
|
+
# away from flipping. Found by the S5 sweep after the rule was widened (2026-08-04).
|
|
83
|
+
_tally_n=$(grep -c "$(date +%Y-%m-%d)" "$T/hub/tracks/_meta/.subagent_dispatch_tally" 2>/dev/null); _tally_n=${_tally_n:-0}
|
|
84
|
+
case "$_tally_n" in (*[!0-9]*|'') _tally_n=0 ;; esac
|
|
85
|
+
[ "$_tally_n" -ge 1 ]
|
|
79
86
|
chk $? "hook actually appends a dated line when run (not merely present)"
|
|
80
87
|
fi
|
|
81
88
|
else
|
|
@@ -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 공백 →
|