@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.
Files changed (45) hide show
  1. package/.claude/judgment_circuits.txt +14 -0
  2. package/.claude/rules/fh_4axis_gate.md +7 -0
  3. package/.claude-plugin/marketplace.json +2 -2
  4. package/AGENTS.md +25 -0
  5. package/CLAUDE.md +215 -12
  6. package/knowledge/shared/harness-core/claude_md_gate_details.md +37 -0
  7. package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
  8. package/knowledge/shared/harness-core/fh_three_layer_canon.md +165 -0
  9. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
  10. package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
  11. package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
  12. package/knowledge/shared/learnings/subagent_invocations_log.yaml +601 -0
  13. package/package.json +19 -3
  14. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  16. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
  17. package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
  18. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
  19. package/scripts/chamber_run.sh +64 -2
  20. package/scripts/chamber_witness.sh +439 -0
  21. package/scripts/compaction_probe.sh +456 -0
  22. package/scripts/digest_landing_check.sh +385 -0
  23. package/scripts/directional_diff_gate.sh +459 -0
  24. package/scripts/judgment_circuit_lint.sh +239 -0
  25. package/scripts/novelty_claim_check.sh +193 -0
  26. package/scripts/prepush_guard_check.sh +15 -0
  27. package/scripts/psa_scan_lib.sh +36 -0
  28. package/scripts/relay_channel.sh +645 -0
  29. package/scripts/reviewer_capability_corpus.tsv +124 -0
  30. package/scripts/selfcheck.sh +204 -17
  31. package/scripts/session_close_check.sh +55 -5
  32. package/scripts/test_marker_crossfamily_lanes.sh +132 -0
  33. package/scripts/test_marker_floor_lanes.sh +9 -8
  34. package/scripts/test_relay_channel_lanes.sh +583 -0
  35. package/scripts/test_reviewer_capability_conformance.sh +173 -0
  36. package/scripts/test_selfcheck_state_lanes.sh +103 -0
  37. package/scripts/test_session_close_chain_lanes.sh +79 -4
  38. package/scripts/test_version_lockstep_lanes.sh +82 -0
  39. package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
  40. package/scripts/universal_guard_check.sh +19 -6
  41. package/scripts/utterance_landing_check.sh +209 -0
  42. package/scripts/version_lockstep_check.sh +80 -0
  43. package/templates/.git-hooks/pre-commit +261 -13
  44. package/templates/.git-hooks/pre-push +14 -2
  45. 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: needs a real upstream, so build a bare remote
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
- _g=0; [ "$BADSCHEMA_HITS" -gt 0 ] && _g=1
152
- _gap "valid-JSON / invalid-SCHEMA snippet written, reported as success, node hook absent" "$_g" \
153
- "$BADSCHEMA_HITS of $BADSCHEMA_TOTAL cases:$DETAIL. exit 0 and 'node-check SessionStart hook \
154
- registered ->' are printed while fh_node_check is NOT in the file that was just written. The success \
155
- message is unconditional it does not re-read what it wrote. One-line fix: after the write, assert \
156
- fh_node_check appears in the serialized result, else exit non-zero."
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
- _g=0; { [ "$FIRST_RC" = 0 ] && [ "$SECOND_RC" != 0 ]; } && _g=1
164
- _gap "the bad write detonates on the NEXT run, not the run that made it" "$_g" \
165
- "run1 exit=$FIRST_RC (reported success), run2 exit=$SECOND_RC (AttributeError in the kept-loop: \
166
- g.get on a str). The user's settings.json is now un-mergeable and the traceback points at the \
167
- survivor filter, not at the snippet that caused it."
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