@chrono-meta/fh-gate 1.4.88 → 1.4.90
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/judgment_circuits.txt +14 -0
- package/.claude/rules/fh_4axis_gate.md +7 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +25 -0
- package/CLAUDE.md +215 -12
- package/knowledge/shared/harness-core/claude_md_gate_details.md +37 -0
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +165 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
- package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +601 -0
- package/package.json +19 -3
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
- package/scripts/chamber_run.sh +64 -2
- package/scripts/chamber_witness.sh +439 -0
- package/scripts/compaction_probe.sh +456 -0
- package/scripts/digest_landing_check.sh +385 -0
- package/scripts/directional_diff_gate.sh +459 -0
- package/scripts/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/prepush_guard_check.sh +15 -0
- package/scripts/psa_scan_lib.sh +36 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +204 -17
- package/scripts/session_close_check.sh +55 -5
- package/scripts/test_marker_crossfamily_lanes.sh +132 -0
- package/scripts/test_marker_floor_lanes.sh +9 -8
- package/scripts/test_relay_channel_lanes.sh +583 -0
- package/scripts/test_reviewer_capability_conformance.sh +173 -0
- 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/test_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/universal_guard_check.sh +19 -6
- package/scripts/utterance_landing_check.sh +209 -0
- package/scripts/version_lockstep_check.sh +80 -0
- package/templates/.git-hooks/pre-commit +261 -13
- package/templates/.git-hooks/pre-push +14 -2
- package/templates/settings.Compaction.snippet.json +56 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_reviewer_capability_conformance.sh — runs the SHARED corpus against THIS repo's
|
|
3
|
+
# reviewer-capability implementation and reports every disagreement.
|
|
4
|
+
#
|
|
5
|
+
# THE CONTRACT, and why it is shaped this way. Three repos judge "can this model be an
|
|
6
|
+
# adversarial reviewer?" in three languages (bash hook · shell probe · python backend).
|
|
7
|
+
#
|
|
8
|
+
# Be precise about WHY they do not share a library, because the first draft of this comment
|
|
9
|
+
# overstated it and a cross-family review caught the inversion: residency blocks the
|
|
10
|
+
# PRIVATE→PUBLIC direction (internal model ids must never reach a public repo). It does NOT
|
|
11
|
+
# stop a private repo from vendoring a sanitized PUBLIC library. The actual reasons are
|
|
12
|
+
# (a) three languages/runtimes, and (b) the classifier is two regexes — a shared library
|
|
13
|
+
# would add more coupling than the thing it shares. Residency governs the CORPUS's contents,
|
|
14
|
+
# not the decision to share data instead of code.
|
|
15
|
+
#
|
|
16
|
+
# What they share is `scripts/reviewer_capability_corpus.tsv`
|
|
17
|
+
# — pure data, residency-safe. Each side keeps its own implementation and vendors this
|
|
18
|
+
# script plus the corpus; drift then surfaces as a red test in whichever repo drifted,
|
|
19
|
+
# instead of as a silent difference nobody measures.
|
|
20
|
+
#
|
|
21
|
+
# Measured before this existed: 19 ids, 10 disagreements between two implementations, 9 of
|
|
22
|
+
# them the private side reading INCAPABLE as CAPABLE. That drift had shipped and was
|
|
23
|
+
# invisible — nothing compared the two.
|
|
24
|
+
#
|
|
25
|
+
# ── PORTING THIS TO ANOTHER REPO (the adapter is the only thing you write) ──────
|
|
26
|
+
# Copy this file + the corpus, then replace classify() with a call into your own code:
|
|
27
|
+
# shell : classify(){ your_probe.sh --check-model "$1" >/dev/null 2>&1 && echo CAPABLE || echo INCAPABLE; }
|
|
28
|
+
# python: classify(){ python3 -c 'import sys;from x import is_reviewer_capable;
|
|
29
|
+
# print("CAPABLE" if is_reviewer_capable(sys.argv[1]) else "INCAPABLE")' "$1"; }
|
|
30
|
+
# Everything else — corpus parsing, verdict comparison, the UNDECIDABLE rule — stays.
|
|
31
|
+
#
|
|
32
|
+
# ── THE UNDECIDABLE ROWS ARE THE POINT ─────────────────────────────────────────
|
|
33
|
+
# An implementation with only a denylist answers CAPABLE for an unrecognised id. That is
|
|
34
|
+
# fail-open: a denylist cannot know `voyage-3` is an embedding model. Such an implementation
|
|
35
|
+
# FAILS these rows, and that failure is correct — it is telling you to add a default-deny
|
|
36
|
+
# leg, not to edit the corpus. A repo that genuinely cannot express UNDECIDABLE may set
|
|
37
|
+
# ALLOW_UNDECIDABLE_AS_INCAPABLE=1, which is strictly-safer and reported as a named
|
|
38
|
+
# deviation rather than a silent pass.
|
|
39
|
+
#
|
|
40
|
+
# Usage: bash scripts/test_reviewer_capability_conformance.sh Exit: 0 = conforms; 1 = drift.
|
|
41
|
+
|
|
42
|
+
set -uo pipefail
|
|
43
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
44
|
+
CORPUS="${CORPUS:-$REPO_ROOT/scripts/reviewer_capability_corpus.tsv}"
|
|
45
|
+
HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
|
|
46
|
+
LENIENT="${ALLOW_UNDECIDABLE_AS_INCAPABLE:-0}"
|
|
47
|
+
|
|
48
|
+
[ -f "$CORPUS" ] || { echo "🟥 HARNESS-ERROR: corpus not found at $CORPUS"; exit 10; }
|
|
49
|
+
|
|
50
|
+
# ── Contract pin: am I reading THE corpus, or a drifted local copy? ─────────────
|
|
51
|
+
# The single shared artifact lives in N repos as N copies, so the first failure mode is not
|
|
52
|
+
# a wrong verdict — it is each side quietly grading itself against its own stale copy while
|
|
53
|
+
# every instrument reports healthy. Verdict on mismatch is HARNESS_ERROR (exit 10), which is
|
|
54
|
+
# neither PASS nor FAIL: nothing was measured against the contract.
|
|
55
|
+
PINNED=$(grep -m1 '^#CORPUS-SHA256' "$CORPUS" | awk '{print $2}')
|
|
56
|
+
ACTUAL=$(grep -E '^[^#[:space:]]' "$CORPUS" | shasum -a 256 2>/dev/null | cut -c1-16)
|
|
57
|
+
[ -z "$ACTUAL" ] && ACTUAL=$(grep -E '^[^#[:space:]]' "$CORPUS" | sha256sum 2>/dev/null | cut -c1-16)
|
|
58
|
+
if [ -z "$PINNED" ] || [ -z "$ACTUAL" ]; then
|
|
59
|
+
echo "🟥 HARNESS-ERROR: cannot verify the corpus pin (pinned='$PINNED' actual='$ACTUAL')."
|
|
60
|
+
echo " An unverifiable pin is not a passing pin — refusing to grade against an unknown corpus."
|
|
61
|
+
exit 10
|
|
62
|
+
fi
|
|
63
|
+
if [ "$PINNED" != "$ACTUAL" ]; then
|
|
64
|
+
echo "🟥 HARNESS-ERROR: corpus drift — pinned=$PINNED actual=$ACTUAL"
|
|
65
|
+
echo " This copy's data rows differ from the contract it names. Either re-sync from the"
|
|
66
|
+
echo " canonical corpus, or bump #CORPUS-VERSION + #CORPUS-SHA256 in EVERY repo that"
|
|
67
|
+
echo " vendors it. A local-only edit is how the shared contract silently stops being shared."
|
|
68
|
+
exit 10
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# ── Namespace declaration: what THIS consumer owns (OPA-bundle `roots` shape) ───
|
|
72
|
+
# This consumer composes PANELS, so it judges sidecar/CLI names and model ids alike.
|
|
73
|
+
# A consumer that only classifies model ids declares NS="model-id" and its skipped rows are
|
|
74
|
+
# REPORTED below, never silently counted as passing.
|
|
75
|
+
NS="${NS:-cli model-id}"
|
|
76
|
+
|
|
77
|
+
# ── THIS repo's adapter: the two regexes inside validate_crossfamily_leg ────────
|
|
78
|
+
# Sourced FROM the hook rather than restated, so this test cannot drift from the thing it
|
|
79
|
+
# is testing. A restated copy would be a fourth implementation — the exact defect class
|
|
80
|
+
# this file exists to measure.
|
|
81
|
+
DENY=$(grep -m1 -oE "grep -qiE 'embed\|[^']*'" "$HOOK" | sed -E "s/^grep -qiE '//; s/'$//")
|
|
82
|
+
ALLOW=$(grep -m1 -oE "grep -qiE 'codex\|[^']*'" "$HOOK" | sed -E "s/^grep -qiE '//; s/'$//")
|
|
83
|
+
if [ -z "$DENY" ] || [ -z "$ALLOW" ]; then
|
|
84
|
+
echo "🟥 HARNESS-ERROR: could not extract the classifier from $HOOK"
|
|
85
|
+
echo " (deny='$DENY' allow='$ALLOW') — refusing to measure against an empty pattern,"
|
|
86
|
+
echo " which would report every row as agreeing."
|
|
87
|
+
exit 10
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
classify() { # $1 = model id → CAPABLE | INCAPABLE | UNDECIDABLE
|
|
91
|
+
# Ineligible-first: the overlap rows depend on this order and exist to catch its inversion.
|
|
92
|
+
if printf '%s' "$1" | grep -qiE "$DENY"; then echo INCAPABLE
|
|
93
|
+
elif printf '%s' "$1" | grep -qiE "$ALLOW"; then echo CAPABLE
|
|
94
|
+
else echo UNDECIDABLE; fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ── Instrument calibration: prove the adapter separates a known pair BEFORE trusting it ──
|
|
98
|
+
# The pair is DERIVED FROM THE CORPUS within this consumer's own namespace, never hardcoded.
|
|
99
|
+
# A hardcoded pair is namespace-blind: the first draft pinned `codex`, and a consumer that
|
|
100
|
+
# classifies model ids (and correctly has no opinion on CLI names) failed calibration for a
|
|
101
|
+
# reason that had nothing to do with its classifier. A calibration that can fail for the
|
|
102
|
+
# wrong reason cannot certify anything.
|
|
103
|
+
kp_pos=$(sed 's/\t/|/g' "$CORPUS" | awk -F'|' -v ns="$NS" \
|
|
104
|
+
'$2=="CAPABLE" && $3 ~ /^ns:/ { split($3,a,"/"); n=substr(a[1],4); if (index(" " ns " ", " " n " ")) { print $1; exit } }')
|
|
105
|
+
kp_neg=$(sed 's/\t/|/g' "$CORPUS" | awk -F'|' -v ns="$NS" \
|
|
106
|
+
'$2=="INCAPABLE" && $3 ~ /^ns:/ { split($3,a,"/"); n=substr(a[1],4); if (index(" " ns " ", " " n " ")) { print $1; exit } }')
|
|
107
|
+
if [ -z "$kp_pos" ] || [ -z "$kp_neg" ]; then
|
|
108
|
+
echo "🟥 HARNESS-ERROR: corpus has no known pair inside namespace [$NS]"
|
|
109
|
+
echo " (pos='$kp_pos' neg='$kp_neg'). Without a pair that this consumer OWNS, nothing"
|
|
110
|
+
echo " below is calibrated — refusing to grade."
|
|
111
|
+
exit 10
|
|
112
|
+
fi
|
|
113
|
+
gp=$(classify "$kp_pos"); gn=$(classify "$kp_neg")
|
|
114
|
+
# Separate "the instrument is broken" from "the instrument works and disagrees". Collapsing
|
|
115
|
+
# them makes a real, gradeable divergence abort as HARNESS-ERROR and go unmeasured — the
|
|
116
|
+
# finding disappears into the error channel. A classifier that says CAPABLE to a known
|
|
117
|
+
# NEGATIVE is not discriminating at all → unmeasurable. One that is merely too strict on the
|
|
118
|
+
# positive still separates, so grade it and report the strictness as drift.
|
|
119
|
+
if [ "$gn" = CAPABLE ]; then
|
|
120
|
+
echo "🟥 HARNESS-ERROR: adapter does not discriminate — known NEGATIVE $kp_neg → CAPABLE."
|
|
121
|
+
echo " It cannot separate a case whose answer is already known. Aborting, not reporting."
|
|
122
|
+
exit 10
|
|
123
|
+
fi
|
|
124
|
+
if [ "$gp" != CAPABLE ]; then
|
|
125
|
+
echo "⚠️ calibration: known POSITIVE $kp_pos → $gp (expected CAPABLE)."
|
|
126
|
+
echo " The classifier discriminates (negative held) but is stricter than the contract."
|
|
127
|
+
echo " Grading continues — this is drift to report, not an unmeasurable instrument."
|
|
128
|
+
else
|
|
129
|
+
echo "calibrated on [$NS]: $kp_pos → $gp · $kp_neg → $gn"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
N=0; BAD=0; DEV=0; SKIP=0
|
|
133
|
+
printf "%-32s %-12s %-12s %s\n" "id" "expected" "actual" ""
|
|
134
|
+
# IFS=$'\t' collapses CONSECUTIVE tabs (POSIX: repeated whitespace-class IFS chars are one
|
|
135
|
+
# delimiter), so an aligned row with an empty field shifts every later field left and the
|
|
136
|
+
# verdict column silently reads someone else's value. Split on a sentinel instead.
|
|
137
|
+
while IFS='|' read -r id want cls why; do
|
|
138
|
+
case "$id" in ''|'#'*|' '*|$'\t'*) continue ;; esac
|
|
139
|
+
# Verdict field must be one of the three tokens. A continuation line of a multi-line note
|
|
140
|
+
# otherwise parses as a row and reports a phantom disagreement — measured on this file's
|
|
141
|
+
# own first draft, which is exactly the "instrument, not target" failure it must not have.
|
|
142
|
+
case "${want:-}" in CAPABLE|INCAPABLE|UNDECIDABLE) : ;; *) continue ;; esac
|
|
143
|
+
row_ns=$(printf '%s' "${cls:-}" | sed -E 's|^ns:([^/]+)/.*|\1|')
|
|
144
|
+
case " $NS " in *" $row_ns "*) : ;; *) SKIP=$((SKIP+1)); continue ;; esac
|
|
145
|
+
N=$((N+1)); got=$(classify "$id")
|
|
146
|
+
if [ "$got" = "$want" ]; then
|
|
147
|
+
printf "%-32s %-12s %-12s ✅\n" "$id" "$want" "$got"
|
|
148
|
+
elif { [ "$want" = UNDECIDABLE ] && [ "$got" = INCAPABLE ]; } \
|
|
149
|
+
|| { [ "$want" = INCAPABLE ] && [ "$got" = UNDECIDABLE ]; }; then
|
|
150
|
+
# Outcome-safety grading, not label identity. Both verdicts BLOCK under default-deny, so
|
|
151
|
+
# the operational result is identical and this is a reported deviation, never a failure.
|
|
152
|
+
# Failing it would punish a pattern classifier for an opaque id it cannot possibly read
|
|
153
|
+
# (`voyage-3`, `bge-m3`) and make DELETING THE ROW the rational maintenance move — the
|
|
154
|
+
# corpus would then shrink toward whatever the weakest implementation happens to catch.
|
|
155
|
+
printf "%-32s %-12s %-12s ⚠️ deviation — same outcome (both block)\n" "$id" "$want" "$got"
|
|
156
|
+
DEV=$((DEV+1))
|
|
157
|
+
else
|
|
158
|
+
printf "%-32s %-12s %-12s ❌ %s\n" "$id" "$want" "$got" "$cls"
|
|
159
|
+
# Name the direction: a permissive miss is the one that ships broken panels.
|
|
160
|
+
{ [ "$want" = INCAPABLE ] || [ "$want" = UNDECIDABLE ]; } && [ "$got" = CAPABLE ] \
|
|
161
|
+
&& echo " ↑ PERMISSIVE drift — this id would be counted as a reviewer"
|
|
162
|
+
BAD=$((BAD+1))
|
|
163
|
+
fi
|
|
164
|
+
done < <(sed 's/\t/|/g' "$CORPUS")
|
|
165
|
+
|
|
166
|
+
echo
|
|
167
|
+
echo "corpus pin: $PINNED · namespaces: [$NS]"
|
|
168
|
+
echo "rows graded: $N · disagreements: $BAD · allowed deviations: $DEV · skipped (other namespace): $SKIP"
|
|
169
|
+
if [ "$BAD" -eq 0 ]; then echo "✅ conforms to the shared corpus"; else
|
|
170
|
+
echo "❌ DRIFT — this repo's classifier disagrees with the shared corpus on $BAD row(s)."
|
|
171
|
+
echo " Fix the implementation, or change the corpus in ALL repos that vendor it."
|
|
172
|
+
fi
|
|
173
|
+
exit $([ "$BAD" -eq 0 ] && echo 0 || echo 1)
|
|
@@ -93,6 +93,109 @@ else
|
|
|
93
93
|
echo " ⏭️ package.json absent — premise unchecked (not a pass)"
|
|
94
94
|
fi
|
|
95
95
|
|
|
96
|
+
echo ""
|
|
97
|
+
echo "── _show_failure: a FAILING suite's evidence must survive to the reader ──"
|
|
98
|
+
# WHY THIS LANE EXISTS (2026-08-05): the four lane blocks in selfcheck.sh used to decide on a
|
|
99
|
+
# discarded run (`>/dev/null`) and then RE-RUN to print. On a non-deterministic suite the re-run can
|
|
100
|
+
# pass, so CI printed a FAIL verdict above a PASSING transcript and the real failure was destroyed —
|
|
101
|
+
# measured in run 30955950695. The repair captures once; this lane is the mechanical anchor for the
|
|
102
|
+
# half that actually makes a failure readable. Without it the repair is unverifiable: reverting to
|
|
103
|
+
# `tail -20` leaves CI green, which is exactly [[feedback_built_but_not_wired]] / anchor-is-decorative.
|
|
104
|
+
# LIFTED, not re-spelled — same reason as the discriminator above.
|
|
105
|
+
FN=$(sed -n '/^_show_failure() {/,/^}$/p' "$SELFCHECK")
|
|
106
|
+
if [ -z "$FN" ]; then
|
|
107
|
+
echo "FAIL _show_failure is no longer defined in selfcheck.sh — this lane cannot verify what it claims."
|
|
108
|
+
echo " If the helper was renamed or removed, update the lane WITH the subject."
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
eval "$FN"
|
|
112
|
+
|
|
113
|
+
# Fixture: a long transcript whose ONLY failing line sits far above any tail window, plus a
|
|
114
|
+
# summary banner at the end that still says something failed. This is the shape that fooled the
|
|
115
|
+
# reader in the CI run above.
|
|
116
|
+
_LONG=$(for i in $(seq 1 40); do echo " ✅ lane L$i ok"; done; echo " ❌ lane L41 tripped — THE EVIDENCE"; for i in $(seq 42 96); do echo " ✅ lane L$i ok"; done; echo "════ lanes: 96 passed · 1 failed ════")
|
|
117
|
+
|
|
118
|
+
_OUT=$(_show_failure "$_LONG")
|
|
119
|
+
printf '%s' "$_OUT" | grep -q 'THE EVIDENCE'; chk $? "the failing line survives (it is 56 lines above the end)"
|
|
120
|
+
printf '%s' "$_OUT" | grep -q '1 failed' ; chk $? "the summary banner is still shown"
|
|
121
|
+
|
|
122
|
+
# CONTROL — the old form must FAIL this same fixture. Without this, the lane could pass for a
|
|
123
|
+
# reason unrelated to the repair (e.g. a fixture short enough that any tail window catches it).
|
|
124
|
+
printf '%s\n' "$_LONG" | tail -20 | grep -q 'THE EVIDENCE'; [ $? -ne 0 ]
|
|
125
|
+
chk $? "CONTROL: the pre-repair form (tail -20) does NOT surface it — the fixture discriminates"
|
|
126
|
+
|
|
127
|
+
# Degenerate inputs: silence must not read as evidence, and a suite that dies before printing any
|
|
128
|
+
# ❌ must say so rather than showing a blank.
|
|
129
|
+
_OUT=$(_show_failure "")
|
|
130
|
+
printf '%s' "$_OUT" | grep -q 'no output captured'; chk $? "empty output is named, not shown as a blank line"
|
|
131
|
+
_OUT=$(_show_failure "some early crash text
|
|
132
|
+
Traceback: boom")
|
|
133
|
+
printf '%s' "$_OUT" | grep -q 'died early'; chk $? "output with no ❌/FAIL falls back and says why"
|
|
134
|
+
|
|
135
|
+
# ── THE DEFECT ITSELF: decide-and-print must be ONE execution ────────────────
|
|
136
|
+
# An earlier version of this lane block tested only the _show_failure HELPER, in isolation, via eval.
|
|
137
|
+
# An adversarial round then reverted a lane block to the original run-twice form — decide on a
|
|
138
|
+
# discarded run, re-run to capture — and this suite still returned PASS (16/16, measured). The anchor
|
|
139
|
+
# was guarding the thing the repair BUILT and not the thing the repair FIXED. That is
|
|
140
|
+
# [[feedback_anchor_can_be_decorative]] with the reversal actually applied, which is the only check
|
|
141
|
+
# that distinguishes the two.
|
|
142
|
+
# The invariant that discriminates: a suite must be EXECUTED EXACTLY ONCE per selfcheck run. The
|
|
143
|
+
# run-twice form necessarily names its subject twice. Keying on the subject path (not on a variable
|
|
144
|
+
# name or a pipe shape) also removes the earlier grep's escape hatch — renaming `_out` no longer
|
|
145
|
+
# evades it, and adding a fifth lane block does not require editing a hardcoded count.
|
|
146
|
+
for _subj in test_tag_version_lanes test_dispatch_log_lanes test_selfcheck_state_lanes sync_from_be_lanes; do
|
|
147
|
+
_n=$(grep -c "bash scripts/${_subj}\.sh" "$SELFCHECK" || true)
|
|
148
|
+
[ "$_n" -eq 1 ]
|
|
149
|
+
chk $? "${_subj}.sh is executed exactly once (found $_n) — 2 means the run-twice form is back"
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
# WIRING — every lane block must route its captured output through the helper. Secondary to the
|
|
153
|
+
# once-only invariant above (this one IS evadable by renaming), kept because it names the intent.
|
|
154
|
+
_CALLS=$(grep -c '_show_failure "\$_out"' "$SELFCHECK")
|
|
155
|
+
[ "$_CALLS" -ge 4 ]; chk $? "every lane block routes failure output through _show_failure (found $_CALLS, expected ≥4)"
|
|
156
|
+
_TAILS=$(grep -c '"\$_out" | tail -' "$SELFCHECK" || true)
|
|
157
|
+
[ "$_TAILS" -eq 0 ]; chk $? "no lane block still truncates with a raw tail (found $_TAILS, expected 0)"
|
|
158
|
+
|
|
159
|
+
# The one non-lane caller that also destroys its evidence at the CALL SITE (not inside check()).
|
|
160
|
+
# `check "..." bash -c '... >/dev/null'` discards the subject's stdout, and fh-codex-doctor writes
|
|
161
|
+
# 100% of its diagnostics to stdout (measured: 686 B stdout / 0 B stderr) — so a strict-mode failure
|
|
162
|
+
# would print a bare FAIL line with zero diagnosis. Anchored here because the fix is one line at the
|
|
163
|
+
# call site and does NOT require touching check() itself.
|
|
164
|
+
_CD=$(grep -c "fh-codex-doctor.js --strict >/dev/null" "$SELFCHECK" || true)
|
|
165
|
+
[ "$_CD" -eq 0 ]; chk $? "fh-codex-doctor's stdout is not discarded at the call site (found $_CD, expected 0)"
|
|
166
|
+
|
|
167
|
+
# Byte-hostile input: a lane emitting invalid UTF-8 must not be reported as "no output". The
|
|
168
|
+
# `tr -d '[:space:]'` form this guard originally used aborts on BSD with "Illegal byte sequence"
|
|
169
|
+
# and emits nothing, so the emptiness check concluded empty while evidence was present.
|
|
170
|
+
# NOTE ON THIS FIXTURE — it deliberately contains NO ❌. The first draft included one, which routed
|
|
171
|
+
# the call into the failing-lines branch, so the emptiness guard (an `elif`) was never reached and the
|
|
172
|
+
# lane passed against the very defect it was written for. Verified by applying the reversion and
|
|
173
|
+
# confirming the diff landed: the lane went green anyway. A fixture that cannot reach the branch
|
|
174
|
+
# under test measures nothing.
|
|
175
|
+
_BAD=$(printf ' \xff\xfe garbage\n crashed before any lane ran BYTE_EVIDENCE\n')
|
|
176
|
+
_OUT=$(_show_failure "$_BAD")
|
|
177
|
+
printf '%s' "$_OUT" | grep -q 'BYTE_EVIDENCE'; chk $? "invalid UTF-8 in the stream does not swallow the evidence"
|
|
178
|
+
printf '%s' "$_OUT" | grep -qv 'no output captured'; chk $? "…and it is not mis-reported as empty output"
|
|
179
|
+
|
|
180
|
+
# The banner must not be suppressed by a line the reader never saw (a failing line past the head cut
|
|
181
|
+
# that merely quotes the banner text).
|
|
182
|
+
_MANY=$(for i in $(seq 1 26); do echo " ❌ f$i"; done; echo " ❌ f27 quoting ════ lanes: 9 passed · 1 failed ════"; echo "════ lanes: 9 passed · 1 failed ════")
|
|
183
|
+
_OUT=$(_show_failure "$_MANY")
|
|
184
|
+
[ "$(printf '%s\n' "$_OUT" | grep -c '════ lanes: 9 passed')" -ge 1 ]
|
|
185
|
+
chk $? "the summary banner still prints when a truncated-away line quotes it"
|
|
186
|
+
|
|
187
|
+
# ── SCOPE OF THIS ANCHOR — stated so it is not over-trusted ───────────────────
|
|
188
|
+
# These lanes catch REVERSION (the run-twice form coming back, the helper being gutted, the
|
|
189
|
+
# call-site redirect returning). They do NOT catch deliberate EVASION: a cross-family round
|
|
190
|
+
# demonstrated three forms that satisfy every check above while still destroying evidence —
|
|
191
|
+
# `_out=$(bash suite >/dev/null 2>&1)` (executed once, captured nothing), a wrapper function
|
|
192
|
+
# (`run_lane() { bash scripts/X.sh; }`, literal appears once, runs twice), and redirect spellings
|
|
193
|
+
# the greps do not enumerate (`1>/dev/null`, a variable). Chasing those with more patterns is the
|
|
194
|
+
# Grep-Collision Treadmill this repo has already logged as P10 — each added regex relocates the
|
|
195
|
+
# evasion instead of closing it. It is bounded rather than escalated: an evading form still routes
|
|
196
|
+
# through _show_failure, whose empty branch prints "(no output captured)" at runtime, so the failure
|
|
197
|
+
# is loud rather than silent. Regression is anchored; evasion is a named residual, not a solved one.
|
|
198
|
+
|
|
96
199
|
echo ""
|
|
97
200
|
if [ "$FAILED" -ne 0 ]; then
|
|
98
201
|
echo "SELFCHECK STATE LANES: FAIL — a discriminator would mis-route"
|
|
@@ -85,6 +85,18 @@ _repo() { # $1=dirname ; makes a git repo with one commit dated $2 (default now
|
|
|
85
85
|
else
|
|
86
86
|
git commit -qm seed
|
|
87
87
|
fi
|
|
88
|
+
# A real repo HAS an upstream. Without one the close check now (correctly) reports
|
|
89
|
+
# `① UNMEASURED — no upstream`, which is not the state any of these lanes means to express —
|
|
90
|
+
# a fixture with no upstream cannot assert "clean tree, nothing unpushed" because the second
|
|
91
|
+
# half of that sentence is genuinely unknown. Every lane below therefore runs on a pushed
|
|
92
|
+
# baseline; upstream ABSENCE is measured on purpose by its own lane (KP-2).
|
|
93
|
+
git init -q --bare "$TMPROOT/$1.git"
|
|
94
|
+
git remote add origin "$TMPROOT/$1.git"
|
|
95
|
+
# `-c core.hooksPath=` : a fixture push must never execute the HOST's git hooks. This repo sets
|
|
96
|
+
# core.hooksPath locally (so a temp repo does not inherit it) but a machine that sets it GLOBALLY
|
|
97
|
+
# would run FH's own pre-push Destructive-Op gate against a throwaway fixture — the suite's result
|
|
98
|
+
# would then depend on the operator's git config rather than on the code under test.
|
|
99
|
+
git -c core.hooksPath= push -q -u origin HEAD
|
|
88
100
|
) >/dev/null 2>&1
|
|
89
101
|
mkdir -p "$T/tracks/_meta"
|
|
90
102
|
printf '%s' "$T"
|
|
@@ -121,13 +133,10 @@ _line "①-N uncommitted path → ⚠️ fires" 'uncommitted path'
|
|
|
121
133
|
_line "①-N uncommitted path → clean line absent" '✅ ① working tree clean' 0 "$OUT"
|
|
122
134
|
_rc "①-N uncommitted is ADVISORY, not blocking" "$RC" 0
|
|
123
135
|
|
|
124
|
-
# unpushed:
|
|
136
|
+
# unpushed: _repo already pushed the seed, so one extra commit is exactly one unpushed commit
|
|
125
137
|
T=$(_repo one_unpushed); _artifacts "$T"
|
|
126
138
|
(
|
|
127
139
|
cd "$T" || exit 1
|
|
128
|
-
git init -q --bare "$TMPROOT/one_unpushed.git" 2>/dev/null
|
|
129
|
-
git remote add origin "$TMPROOT/one_unpushed.git"
|
|
130
|
-
git push -q -u origin HEAD 2>/dev/null
|
|
131
140
|
echo more > second.txt && git add -A && git commit -qm second
|
|
132
141
|
) >/dev/null 2>&1
|
|
133
142
|
_run "$T"
|
|
@@ -141,6 +150,72 @@ _gap "① non-repo reports CLEAN" "$_g" \
|
|
|
141
150
|
"git is unavailable/not a repo → DIRTY=0, UNPUSHED=0 → the check reports '✅ working tree clean'. \
|
|
142
151
|
An instrument that could not look is not a clean result (not found ≠ 0). Should say UNSCANNED."
|
|
143
152
|
|
|
153
|
+
# ── ① not-found ≠ 0 : the five states where git CANNOT answer ────────────────────
|
|
154
|
+
# Origin: the fix that introduced these five guards (2026-08-06) listed all six known pairs in its
|
|
155
|
+
# COMMIT MESSAGE and shipped none of them as a lane — the code changed, the suite did not, and CI
|
|
156
|
+
# went red on the two lanes the change broke rather than on the five it left unmeasured. Prose in a
|
|
157
|
+
# commit message is not a regression anchor: nothing re-runs it. Each pair below is
|
|
158
|
+
# known-positive (the instrument is blind) + a paired control (the clean line must NOT appear),
|
|
159
|
+
# because "the warning fired" and "the warning fired INSTEAD of a false all-clear" are two claims.
|
|
160
|
+
# KP-1 (the healthy case) is the ①-P lane at the top of this section.
|
|
161
|
+
|
|
162
|
+
# KP-2 upstream absent — `@{u}..` fails, prints 0 lines, and `wc -l` counts that 0 as "nothing
|
|
163
|
+
# unpushed". A commit that never left the machine reads as pushed. `--unset-upstream` (not
|
|
164
|
+
# `remote remove`) keeps a remote present, so the all-branch scan still runs: this isolates the
|
|
165
|
+
# upstream leg instead of quietly testing two things at once.
|
|
166
|
+
T=$(_repo kp2_no_upstream); _artifacts "$T"
|
|
167
|
+
git -C "$T" branch --unset-upstream >/dev/null 2>&1
|
|
168
|
+
_run "$T"
|
|
169
|
+
_line "KP-2 no upstream → UNMEASURED, not zero" 'unpushed count is UNKNOWN' 1 "$OUT"
|
|
170
|
+
_line "KP-2 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
171
|
+
_rc "KP-2 → advisory, not blocking" "$RC" 0
|
|
172
|
+
|
|
173
|
+
# KP-3 git status itself fails — a corrupt index makes `status` exit non-zero with EMPTY output,
|
|
174
|
+
# and `| wc -l` renders that emptiness as "0 dirty paths" = clean.
|
|
175
|
+
T=$(_repo kp3_broken_index); _artifacts "$T"
|
|
176
|
+
printf 'garbage' > "$T/.git/index"
|
|
177
|
+
_run "$T"
|
|
178
|
+
_line "KP-3 corrupt index → cleanliness UNKNOWN" 'cleanliness is UNKNOWN' 1 "$OUT"
|
|
179
|
+
_line "KP-3 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
180
|
+
# Measured while writing this lane: a corrupt index makes `ls-files -v` exit 128 too, so the
|
|
181
|
+
# assume-unchanged probe (MASKED) silently reads 0 — the same not-found-≠-0 shape, one layer in.
|
|
182
|
+
# It is NOT a false all-clear (DIRTY_KNOWN=0 already suppresses the clean line), so it is recorded
|
|
183
|
+
# as a residual rather than patched here. `rev-parse @{u}` still exits 0 under a corrupt index,
|
|
184
|
+
# which is what keeps this lane measuring cleanliness and not accidentally re-measuring KP-2.
|
|
185
|
+
|
|
186
|
+
# KP-4 measured scope ≠ claimed scope — `@{u}..` reads the CURRENT branch only, while the message
|
|
187
|
+
# says "nothing unpushed" about the repo. An unpushed commit parked on another local branch is
|
|
188
|
+
# invisible. The current branch stays clean and pushed on purpose: only the other branch is dirty,
|
|
189
|
+
# so a green here would be the exact false all-clear.
|
|
190
|
+
T=$(_repo kp4_other_branch); _artifacts "$T"
|
|
191
|
+
(
|
|
192
|
+
cd "$T" || exit 1
|
|
193
|
+
git checkout -q -b side
|
|
194
|
+
echo side > side.txt && git add -A && git commit -qm side
|
|
195
|
+
git checkout -q -
|
|
196
|
+
) >/dev/null 2>&1
|
|
197
|
+
_run "$T"
|
|
198
|
+
_line "KP-4 unpushed on ANOTHER branch → ⚠️ fires" 'never pushed anywhere' 1 "$OUT"
|
|
199
|
+
_line "KP-4 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
200
|
+
|
|
201
|
+
# KP-5 `status.showUntrackedFiles=no` — git succeeds and stays SILENT (exit 0, empty output), so
|
|
202
|
+
# the exit-code guard of KP-3 cannot catch this one. Only `--untracked-files=all` overrides it.
|
|
203
|
+
T=$(_repo kp5_untracked_off); _artifacts "$T"
|
|
204
|
+
git -C "$T" config status.showUntrackedFiles no
|
|
205
|
+
echo hidden > "$T/hidden.txt"
|
|
206
|
+
_run "$T"
|
|
207
|
+
_line "KP-5 showUntrackedFiles=no → still counted" 'uncommitted path' 1 "$OUT"
|
|
208
|
+
_line "KP-5 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
209
|
+
|
|
210
|
+
# KP-6 assume-unchanged / skip-worktree — edits to a marked TRACKED file never reach porcelain at
|
|
211
|
+
# all, so `-uall` does not help either. A separate instrument (`ls-files -v`) has to surface it.
|
|
212
|
+
T=$(_repo kp6_assume_unchanged); _artifacts "$T"
|
|
213
|
+
git -C "$T" update-index --assume-unchanged unrelated.txt
|
|
214
|
+
echo edited >> "$T/unrelated.txt"
|
|
215
|
+
_run "$T"
|
|
216
|
+
_line "KP-6 assume-unchanged edit → surfaced" 'INVISIBLE here' 1 "$OUT"
|
|
217
|
+
_line "KP-6 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
218
|
+
|
|
144
219
|
echo
|
|
145
220
|
echo "══ ①-b open-PR sweep ══"
|
|
146
221
|
_ghstub() { # $1=repo $2=stdout $3=exit
|
|
@@ -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)"
|
|
@@ -129,6 +129,93 @@ _eq "MG-T → the user's file is left byte-identical" "$(cat "$H/.claude/setti
|
|
|
129
129
|
|| _pass "MG-T → no misleading backup artifact left behind"
|
|
130
130
|
|
|
131
131
|
echo
|
|
132
|
+
# ══ MULTI-SNIPPET DISCOVERY (regression for the 2026-08-08 high review, finding #1) ══
|
|
133
|
+
# The merge block used to hardcode settings.SessionStart.snippet.json, so every OTHER shipped snippet
|
|
134
|
+
# was structurally unregisterable — the compaction hooks shipped with a README reciting the
|
|
135
|
+
# shipping-is-not-wiring lesson while reproducing it, and PreToolUse was already in the same hole.
|
|
136
|
+
# These lanes fail if anyone reintroduces a single-snippet path.
|
|
137
|
+
echo "══ multi-snippet discovery ══"
|
|
138
|
+
|
|
139
|
+
_hub2() { # $1=name → hub with the shipped SessionStart snippet PLUS a second, unrelated snippet
|
|
140
|
+
local h="$TMPROOT/$1"; mkdir -p "$h/templates" "$h/.claude"
|
|
141
|
+
cp "$SNIPPET" "$h/templates/settings.SessionStart.snippet.json"
|
|
142
|
+
cat > "$h/templates/settings.Zzz.snippet.json" <<'EOF'
|
|
143
|
+
{
|
|
144
|
+
"_README": ["fixture — a snippet the merge code has never heard of"],
|
|
145
|
+
"project_settings_json": {
|
|
146
|
+
"hooks": {
|
|
147
|
+
"PreCompact": [
|
|
148
|
+
{ "matcher": "", "hooks": [ { "type": "command", "command": "bash \"$HUB/scripts/zzz_probe.sh\" seal" } ] }
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
EOF
|
|
154
|
+
echo "$h"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
H=$(_hub2 multi); _merge "$H"
|
|
158
|
+
_rc "MS-1 merge with two snippets exits 0" "$RC" 0
|
|
159
|
+
python3 -c "
|
|
160
|
+
import json,sys
|
|
161
|
+
d=json.load(open('$H/.claude/settings.json'))
|
|
162
|
+
h=d.get('hooks',{})
|
|
163
|
+
ss=json.dumps(h.get('SessionStart',[]))
|
|
164
|
+
pc=json.dumps(h.get('PreCompact',[]))
|
|
165
|
+
print('OK' if ('fh_node_check' in ss and 'zzz_probe' in pc) else 'MISS')
|
|
166
|
+
" > "$TMPROOT/ms.out" 2>/dev/null
|
|
167
|
+
_eq "MS-2 BOTH snippets registered (unknown snippet needs no code edit)" "$(cat "$TMPROOT/ms.out")" "OK"
|
|
168
|
+
|
|
169
|
+
# The registered-event set must come from the snippets, not from a hardcoded list.
|
|
170
|
+
_eq "MS-3 a non-SessionStart event is registered" \
|
|
171
|
+
"$(python3 -c "import json;print('YES' if json.load(open('$H/.claude/settings.json')).get('hooks',{}).get('PreCompact') else 'NO')" 2>/dev/null)" "YES"
|
|
172
|
+
|
|
173
|
+
# Idempotence: re-running must not duplicate either snippet's hooks.
|
|
174
|
+
_merge "$H"
|
|
175
|
+
_eq "MS-4 re-run is idempotent (no duplicate zzz_probe entry)" \
|
|
176
|
+
"$(python3 -c "import json;print(json.dumps(json.load(open('$H/.claude/settings.json')).get('hooks',{}).get('PreCompact',[])).count('zzz_probe'))" 2>/dev/null)" "1"
|
|
177
|
+
|
|
178
|
+
# A user's own hook in the same event must survive the merge (hook-level, not group-level).
|
|
179
|
+
python3 - "$H" <<'PY2'
|
|
180
|
+
import json,collections,os,sys
|
|
181
|
+
t=sys.argv[1]+"/.claude/settings.json"
|
|
182
|
+
d=json.load(open(t),object_pairs_hook=collections.OrderedDict)
|
|
183
|
+
d["hooks"].setdefault("PreCompact",[]).append({"matcher":"","hooks":[{"type":"command","command":"bash my_own.sh"}]})
|
|
184
|
+
json.dump(d,open(t,"w"),indent=2,ensure_ascii=False)
|
|
185
|
+
PY2
|
|
186
|
+
_merge "$H"
|
|
187
|
+
_eq "MS-5 user's own hook in the same event survives" \
|
|
188
|
+
"$(python3 -c "import json;print('YES' if 'my_own.sh' in json.dumps(json.load(open('$H/.claude/settings.json')).get('hooks',{}).get('PreCompact',[])) else 'NO')" 2>/dev/null)" "YES"
|
|
189
|
+
|
|
190
|
+
# ══ RE-REVIEW REPAIRS (2026-08-08 round 2) ══
|
|
191
|
+
# 두 건 다 **직전 라운드의 수리가 만든 결함**이다. 레인 없이 고치면 같은 자리로 돌아온다.
|
|
192
|
+
echo "══ re-review repairs ══"
|
|
193
|
+
|
|
194
|
+
# ZS — 스니펫 0개는 조용한 성공이 아니라 시끄러운 실패여야 한다 (#1)
|
|
195
|
+
ZH="$TMPROOT/zerosnip"; mkdir -p "$ZH/templates" "$ZH/.claude" # templates/ 는 있고 스니펫만 없다
|
|
196
|
+
_merge "$ZH"
|
|
197
|
+
_rc "ZS-1 스니펫 0개 → non-zero (조용한 성공 금지)" "$RC" 1
|
|
198
|
+
case "$OUT" in *"NO SNIPPETS"*) _pass "ZS-2 무엇이 없는지 이름을 말한다" ;; *) _fail "ZS-2 실패 사유가 불명" ;; esac
|
|
199
|
+
|
|
200
|
+
# KC — 파생 키가 베이스네임이면 남의 훅을 지운다 (#2)
|
|
201
|
+
KH=$(_hub2 keycollide)
|
|
202
|
+
python3 - "$KH" <<'PY2'
|
|
203
|
+
import json,collections,os,sys
|
|
204
|
+
t=sys.argv[1]+"/.claude/settings.json"
|
|
205
|
+
d=collections.OrderedDict()
|
|
206
|
+
if os.path.exists(t): d=json.load(open(t),object_pairs_hook=collections.OrderedDict)
|
|
207
|
+
h=d.setdefault("hooks",collections.OrderedDict())
|
|
208
|
+
# 사용자 자기 훅 — 파일명은 겹치지만 **경로가 다르다**
|
|
209
|
+
h.setdefault("PreCompact",[]).append({"matcher":"","hooks":[
|
|
210
|
+
{"type":"command","command":"bash ~/tools/zzz_probe.sh --mine"}]})
|
|
211
|
+
json.dump(d,open(t,"w"),indent=2,ensure_ascii=False)
|
|
212
|
+
PY2
|
|
213
|
+
_merge "$KH"
|
|
214
|
+
_eq "KC-1 파일명만 겹치는 사용자 훅은 살아남는다" \
|
|
215
|
+
"$(python3 -c "import json;print('YES' if 'tools/zzz_probe.sh' in json.dumps(json.load(open('$KH/.claude/settings.json')).get('hooks',{}).get('PreCompact',[])) else 'NO')" 2>/dev/null)" "YES"
|
|
216
|
+
_eq "KC-2 FH 자기 훅은 여전히 교체된다 (중복 없음)" \
|
|
217
|
+
"$(python3 -c "import json;print(json.dumps(json.load(open('$KH/.claude/settings.json')).get('hooks',{}).get('PreCompact',[])).count('scripts/zzz_probe.sh'))" 2>/dev/null)" "1"
|
|
218
|
+
|
|
132
219
|
echo "══ ⓘ GAP lanes ══"
|
|
133
220
|
|
|
134
221
|
# GAP 1 — the ONLY validation is what `kept + entry` incidentally requires: that entry is a list.
|
|
@@ -148,23 +235,29 @@ for name in emptylist nocommand wrongscript juststring; do
|
|
|
148
235
|
BADSCHEMA_HITS=$((BADSCHEMA_HITS+1)); DETAIL="$DETAIL $name"
|
|
149
236
|
fi
|
|
150
237
|
done
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
238
|
+
# PROMOTED 2026-08-08 (was a GAP): the merge block now shape-validates before writing and re-reads
|
|
239
|
+
# what it wrote, so a bad-schema snippet can no longer be written-and-reported-as-success.
|
|
240
|
+
# NOTE the fixture nuance: the `wrongscript` case is NOT malformed — it is a valid snippet naming a
|
|
241
|
+
# different script, which the discovery-based merge correctly registers. `_registered` only greps for
|
|
242
|
+
# fh_node_check, so it reads as "not registered". Assert the property that actually matters instead:
|
|
243
|
+
# a run that skips a snippet must not exit 0 (fail-closed), which is what makes the old GAP dead.
|
|
244
|
+
_eq "BS-1 malformed snippet never yields a zero exit (fail-closed)" \
|
|
245
|
+
"$(H=$(_hub bs1 '{"project_settings_json":{"hooks":{"SessionStart":["not even an object"]}}}'); _merge "$H"; [ "$RC" != 0 ] && echo CLOSED || echo OPEN)" "CLOSED"
|
|
246
|
+
# (GAP 1 retired — promoted to BS-1 above, 2026-08-08)
|
|
157
247
|
|
|
158
248
|
# GAP 2 — the corruption from GAP 1 is LATENT: the bad value lands in the user's settings.json and
|
|
159
249
|
# detonates on the NEXT wizard run, in the kept-loop, far from where it was introduced.
|
|
160
250
|
H=$(_hub latent '{"project_settings_json":{"hooks":{"SessionStart":["not even an object"]}}}')
|
|
161
251
|
_merge "$H"; FIRST_RC=$RC
|
|
162
252
|
_merge "$H"; SECOND_RC=$RC
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
253
|
+
# PROMOTED 2026-08-08 (was a GAP): a bad snippet is skipped with a visible SKIP line instead of
|
|
254
|
+
# corrupting settings.json, so run 2 no longer detonates on run 1's write.
|
|
255
|
+
# The old GAP was: run 1 reports success, run 2 detonates on run 1's write. The promoted property is
|
|
256
|
+
# DETERMINISM — both runs must reach the same verdict, so a failure is attributable to the snippet
|
|
257
|
+
# that caused it rather than surfacing later inside the survivor filter.
|
|
258
|
+
_eq "LT-1 verdict is deterministic across runs (no latent detonation)" \
|
|
259
|
+
"$([ "$FIRST_RC" = "$SECOND_RC" ] && echo DETERMINISTIC || echo LATENT)" "DETERMINISTIC"
|
|
260
|
+
# (GAP 2 retired — promoted to LT-1 above, 2026-08-08)
|
|
168
261
|
|
|
169
262
|
# GAP 3 — INSTRUMENT COVERAGE, measured not asserted. Build the exact post-failure state (companion
|
|
170
263
|
# hook registered, node hook absent) and run install-doctor's registration check on it. The
|