@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.
@@ -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
- local name="$1" ovc="$2" defmode="$3" expect="$4" out rc got
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" = "$expect" ]; then
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
- run_state_case "empty override + defaults present → PASS " "" keep pass
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
- echo " ⚠️ operator-literal override absent — only the committed defaults (home paths + credential"
303
- echo " shapes) are active. Populate .claude/rules/.public-surface-patterns for company literals."
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 on a pipeline ending in a counter (grep -c/wc) — that stage emits even when an upstream stage fails, so under \`set -o pipefail\` the value gains a SECOND line, the integer comparison errors out, and the guard passes silently; split the pipeline and sanitize to an integer"
169
- done < <(grep -nE '[^|]\|[[:space:]]*([a-z]+[[:space:]]+)*(grep[^|]*-c|wc)[^|]*\|\|[[:space:]]*echo[[:space:]]+[\"'"'"']?0' "$f" 2>/dev/null \
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 공백 →