@chrono-meta/fh-gate 1.4.89 → 1.4.91
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 +202 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -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 +640 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
- package/package.json +23 -2
- 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/branch_claim.sh +266 -0
- 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/fh_env_delta_scan.sh +15 -0
- package/scripts/fh_session_load.sh +34 -1
- package/scripts/field_canon_preload.sh +129 -0
- package/scripts/hook_source_lib.sh +41 -0
- package/scripts/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +106 -0
- package/scripts/session_close_check.sh +134 -1
- package/scripts/test_branch_claim_lanes.sh +231 -0
- package/scripts/test_dispatch_log_lanes.sh +35 -1
- package/scripts/test_field_canon_lanes.sh +142 -0
- package/scripts/test_hook_source_gate_lanes.sh +81 -0
- 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_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/utterance_landing_check.sh +209 -0
- package/templates/.git-hooks/pre-commit +297 -13
- package/templates/settings.Compaction.snippet.json +56 -0
- package/templates/settings.FieldCanon.snippet.json +51 -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)
|
|
@@ -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
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# utterance_landing_check.sh — 마감 전, 운영자 발화가 기록에 착지했는지 grep 검증.
|
|
3
|
+
#
|
|
4
|
+
# WHY. 마감 체인이 `CONSISTENT` 를 내면서도 운영자 발화가 통째로 빠지는 일이 두 세션 연속
|
|
5
|
+
# 일어났다(2026-08-07 5건 · 2026-08-08 2건). close check 는 **형식**만 본다 — 카드가 로그보다
|
|
6
|
+
# 새로운가, 필수 아티팩트가 있는가. **내용이 착지했는지는 안 본다.**
|
|
7
|
+
#
|
|
8
|
+
# 가장 잘 빠지는 것은 잊은 발화가 아니라 **행동으로 대응한 발화**다. 답장에서 판단까지 내리고
|
|
9
|
+
# 나면 처리된 느낌이 남아서, 그 판단이 어디에도 안 적힌 채 세션이 끝난다. 2026-08-08 실측:
|
|
10
|
+
# "PMH 가 개인위키에 4축을 돌려 늦어졌다" 에 대해 과적용이라는 판정을 답장에 썼고 기록은 0건.
|
|
11
|
+
#
|
|
12
|
+
# ── 이 스크립트가 존재하는 두 번째 이유: 손으로 짜면 계기가 죽는다 ──
|
|
13
|
+
# 같은 검증을 손으로 짤 때마다 계기가 죽었다(N=3). 마지막 사망 원인은 **zsh word-split**:
|
|
14
|
+
#
|
|
15
|
+
# S="a.md b.md"; grep -l "$pat" $S # bash 는 분리, zsh 는 파일명 하나로 전달 → 전건 0
|
|
16
|
+
#
|
|
17
|
+
# 컨트롤이 없으면 그 0 이 "전부 미착지" 로 읽힌다. 그래서 이 스크립트는 (1) 파일을 배열로
|
|
18
|
+
# 다루고 (2) **컨트롤을 먼저 돌려 살아있음을 증명한 뒤에만** 타깃 결과를 출력한다.
|
|
19
|
+
# 컨트롤이 죽으면 타깃 결과를 아예 인쇄하지 않는다 — 죽은 계기의 출력은 데이터가 아니다.
|
|
20
|
+
#
|
|
21
|
+
# ── 사용 ──
|
|
22
|
+
# bash scripts/utterance_landing_check.sh <probes.tsv> <file> [<file>...]
|
|
23
|
+
# bash scripts/utterance_landing_check.sh --self-test
|
|
24
|
+
#
|
|
25
|
+
# probes.tsv 형식 (탭 구분):
|
|
26
|
+
# CONTROL<TAB><정규식><TAB><라벨> 착지가 확실한 것. 하나라도 0이면 HARNESS-ERROR
|
|
27
|
+
# TARGET<TAB><정규식><TAB><라벨> 검증 대상 발화
|
|
28
|
+
#
|
|
29
|
+
# ── ⚠️ 명명된 잔여: 프로브는 문구 변경에 깨진다 (2026-08-08 실측) ──
|
|
30
|
+
# 정규식을 **정확한 문구**에 걸면, 그 문구를 나중에 고치는 순간 프로브가 죽고 **착지한 항목이
|
|
31
|
+
# 미착지로 뜬다**. 실측: 프로브 `2건만|두 개에만` 이 카드 제목을 `2건만` → `2건 병렬` 로
|
|
32
|
+
# 편집하자 오탐을 냈다(내용은 그대로 있었다).
|
|
33
|
+
#
|
|
34
|
+
# 이건 `[[feedback_typed_verdict_channel]]` 이 이름 붙인 Grep-Collision Treadmill 의 반대편이다:
|
|
35
|
+
# 충돌은 없는데 **너무 좁아서** 깨진다. 두 방향 모두 "문자열을 재고 대상을 안 본다" 의 사례다.
|
|
36
|
+
#
|
|
37
|
+
# 완화(닫히지 않음):
|
|
38
|
+
# · 정규식은 **여러 표현을 OR** 로 — 한 문구에 걸지 마라 (`A|B|C`)
|
|
39
|
+
# · **미착지가 뜨면 먼저 손으로 열어 확인**하라. 프로브 노후가 첫 번째 의심 대상이다.
|
|
40
|
+
# rc=1 은 "기록하라"가 아니라 "확인하라"로 읽어야 한다.
|
|
41
|
+
# · 이 실패 방향은 **안전**하다 — 착지한 것을 미착지로 부르지, 미착지를 착지로 부르지 않는다.
|
|
42
|
+
# 과차단이지 fail-open 이 아니다. (반대였다면 이 도구는 못 쓴다.)
|
|
43
|
+
#
|
|
44
|
+
# ── 종료 코드 ──
|
|
45
|
+
# 0 전건 착지
|
|
46
|
+
# 1 미착지 있음 (라벨과 함께 출력)
|
|
47
|
+
# 10 HARNESS-ERROR — 컨트롤 사망 또는 입력 불량. **PASS 도 FAIL 도 아니다**
|
|
48
|
+
|
|
49
|
+
set -uo pipefail
|
|
50
|
+
|
|
51
|
+
self_test() {
|
|
52
|
+
local T f=0 n=0
|
|
53
|
+
# mktemp 실패를 안 보면 T 가 빈 문자열이 되고 이어지는 쓰기가 `/a.md` 로 나간다 — 루트에
|
|
54
|
+
# 쓰려다 실패하고, 그 실패가 '캘리브레이션 실패' 로 렌더된다. 계기 부재를 계기 결함으로
|
|
55
|
+
# 오진하는 경로라 여기서 갈라 놓는다. (cross-family 감사에서 실제 샌드박스가 이걸 밟았다.)
|
|
56
|
+
T=$(mktemp -d 2>/dev/null) || T=""
|
|
57
|
+
if [ -z "$T" ] || [ ! -d "$T" ]; then
|
|
58
|
+
echo "🟥 HARNESS-ERROR: mktemp -d 실패 — 캘리브레이션을 돌릴 수 없다(계기 결함이 아니라 환경 부재)."
|
|
59
|
+
return 10
|
|
60
|
+
fi
|
|
61
|
+
trap 'rm -rf "$T"' RETURN
|
|
62
|
+
printf 'alpha 라는 단어\n' > "$T/a.md"
|
|
63
|
+
printf 'beta 라는 단어\n' > "$T/b.md"
|
|
64
|
+
t() { n=$((n+1)); if [ "$2" = "$3" ]; then echo "✅ $1 → $3"; else echo "❌ $1 → $3 (기대 $2)"; f=1; fi; }
|
|
65
|
+
|
|
66
|
+
printf 'CONTROL\talpha\tctl\nTARGET\tbeta\thit\n' > "$T/p1.tsv"
|
|
67
|
+
bash "$0" "$T/p1.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1 && r=PASS || r=FAIL
|
|
68
|
+
t "컨트롤 생존 + 타깃 착지" PASS "$r"
|
|
69
|
+
|
|
70
|
+
printf 'CONTROL\talpha\tctl\nTARGET\tgamma\tmiss\n' > "$T/p2.tsv"
|
|
71
|
+
bash "$0" "$T/p2.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1; rc=$?
|
|
72
|
+
t "타깃 미착지 → rc=1" 1 "$rc"
|
|
73
|
+
|
|
74
|
+
# 핵심 레인: 컨트롤이 죽으면 타깃이 '전부 미착지'처럼 보이는데, 그때 1을 내면 안 된다.
|
|
75
|
+
printf 'CONTROL\tzzz없는단어\tctl\nTARGET\tbeta\thit\n' > "$T/p3.tsv"
|
|
76
|
+
bash "$0" "$T/p3.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1; rc=$?
|
|
77
|
+
t "컨트롤 사망 → rc=10 (1 아님)" 10 "$rc"
|
|
78
|
+
|
|
79
|
+
# zsh word-split 재현 방어: 파일 인자가 여러 개일 때 실제로 전부 읽히는가.
|
|
80
|
+
# ⚠️ 이 픽스처는 원래 TARGET 없이 컨트롤 2건만 두고 rc=0 을 기대했다. 그 형태가 곧
|
|
81
|
+
# "아무것도 안 쟀는데 합격" 을 **레인이 승인하는** 상태였다(아래 p6 이 그래서 신설됐다).
|
|
82
|
+
# word-split 을 재려면 컨트롤이 두 파일에 흩어져 있으면 충분하고, TARGET 은 있어야 한다.
|
|
83
|
+
printf 'CONTROL\talpha\tctl\nCONTROL\tbeta\tctl2\nTARGET\t라는\thit\n' > "$T/p4.tsv"
|
|
84
|
+
bash "$0" "$T/p4.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1; rc=$?
|
|
85
|
+
t "두 파일에 흩어진 컨트롤 2건 모두 생존" 0 "$rc"
|
|
86
|
+
|
|
87
|
+
printf 'TARGET\tbeta\thit\n' > "$T/p5.tsv"
|
|
88
|
+
bash "$0" "$T/p5.tsv" "$T/a.md" >/dev/null 2>&1; rc=$?
|
|
89
|
+
t "컨트롤 0개 → rc=10 (검증 불가)" 10 "$rc"
|
|
90
|
+
|
|
91
|
+
# 잰 발화가 0 건인데 초록을 내면 안 된다. rc=1(미착지)과도 **다른 값**이어야 한다 —
|
|
92
|
+
# 둘이 뭉치면 "기록하라" 와 "프로브를 안 썼다" 가 구분되지 않는다.
|
|
93
|
+
printf 'CONTROL\talpha\tctl\n' > "$T/p6.tsv"
|
|
94
|
+
bash "$0" "$T/p6.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1; rc=$?
|
|
95
|
+
t "TARGET 0개 → rc=10 (0 도 1 도 아님)" 10 "$rc"
|
|
96
|
+
|
|
97
|
+
# 깨진 정규식은 무매치가 아니다. grep 은 에러도 비영으로 주므로 순진하게 받으면
|
|
98
|
+
# **고장난 프로브가 '미착지' 로 렌더된다** = 있지도 않은 누락을 기록하게 만든다.
|
|
99
|
+
printf 'CONTROL\talpha\tctl\nTARGET\t[\tbroken\n' > "$T/p7.tsv"
|
|
100
|
+
bash "$0" "$T/p7.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1; rc=$?
|
|
101
|
+
t "깨진 정규식 → rc=10 (1 아님)" 10 "$rc"
|
|
102
|
+
|
|
103
|
+
# 대시로 시작하는 패턴이 grep 옵션으로 먹히면 계기가 통째로 오작동한다.
|
|
104
|
+
printf 'CONTROL\talpha\tctl\nTARGET\t-베타\tdashy\n' > "$T/p8.tsv"
|
|
105
|
+
bash "$0" "$T/p8.tsv" "$T/a.md" "$T/b.md" >/dev/null 2>&1; rc=$?
|
|
106
|
+
t "대시 시작 패턴이 옵션으로 안 먹힘 → rc=1(정상 미착지)" 1 "$rc"
|
|
107
|
+
|
|
108
|
+
echo; [ "$f" -eq 0 ] && echo "✅ 캘리브레이션 통과 ($n 쌍)" || echo "❌ 캘리브레이션 실패 ($n 쌍)"
|
|
109
|
+
return "$f"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[ "${1:-}" = "--self-test" ] && { self_test; exit $?; }
|
|
113
|
+
|
|
114
|
+
if [ "$#" -lt 2 ]; then
|
|
115
|
+
echo "usage: $0 <probes.tsv> <file> [<file>...]" >&2
|
|
116
|
+
echo " $0 --self-test" >&2
|
|
117
|
+
exit 10
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
PROBES="$1"; shift
|
|
121
|
+
FILES=("$@") # 배열 — word-split 에 의존하지 않는다
|
|
122
|
+
[ -f "$PROBES" ] || { echo "🟥 HARNESS-ERROR: probes 파일 없음: $PROBES"; exit 10; }
|
|
123
|
+
for f in "${FILES[@]}"; do
|
|
124
|
+
[ -f "$f" ] || { echo "🟥 HARNESS-ERROR: 대상 파일 없음: $f"; exit 10; }
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
# 단일 검색 경로. CONTROL 과 TARGET 이 **같은 함수**를 통과해야 한다 — 두 벌로 나누면 한쪽만
|
|
128
|
+
# 통과하는 입력이 다른 쪽에서 무음 드롭된다([[feedback_divergent_leniency_duplicate_normalizers]]).
|
|
129
|
+
# 반환: 0=매치 · 1=무매치 · 2=grep 에러(잘못된 정규식 등). grep 은 에러도 2 로 주는데, 그걸
|
|
130
|
+
# 무매치와 뭉치면 **깨진 프로브가 '미착지'로 렌더된다** — 계기 고장을 데이터로 파는 것이라
|
|
131
|
+
# 이 스크립트가 막으려는 바로 그 오독이다. `--` 는 `-foo` 같은 패턴이 옵션으로 먹히는 걸 막는다.
|
|
132
|
+
_probe_hits() {
|
|
133
|
+
local pat="$1"; shift
|
|
134
|
+
grep -lE -- "$pat" "$@" 2>/dev/null
|
|
135
|
+
return "${PIPESTATUS[0]:-$?}"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ── 1단계: 컨트롤. 살아있음을 증명하기 전에는 타깃을 인쇄하지 않는다 ──
|
|
139
|
+
CTL_TOTAL=0; CTL_DEAD=0; DEAD_LABELS=""; BAD_PAT=""
|
|
140
|
+
while IFS=$'\t' read -r kind pat label; do
|
|
141
|
+
[ "${kind:-}" = "CONTROL" ] || continue
|
|
142
|
+
CTL_TOTAL=$((CTL_TOTAL+1))
|
|
143
|
+
_probe_hits "$pat" "${FILES[@]}" >/dev/null; _rc=$?
|
|
144
|
+
if [ "$_rc" -ge 2 ]; then
|
|
145
|
+
BAD_PAT="${BAD_PAT:+$BAD_PAT, }CONTROL/${label:-$pat}"
|
|
146
|
+
elif [ "$_rc" -ne 0 ]; then
|
|
147
|
+
CTL_DEAD=$((CTL_DEAD+1)); DEAD_LABELS="${DEAD_LABELS:+$DEAD_LABELS, }${label:-$pat}"
|
|
148
|
+
fi
|
|
149
|
+
done < "$PROBES"
|
|
150
|
+
|
|
151
|
+
if [ "$CTL_TOTAL" -eq 0 ]; then
|
|
152
|
+
echo "🟥 HARNESS-ERROR: probes 에 CONTROL 이 없다."
|
|
153
|
+
echo " 컨트롤 없는 부재 측정은 근거가 아니다 — 0 이 '미착지'인지 '계기 사망'인지 구분 불가."
|
|
154
|
+
exit 10
|
|
155
|
+
fi
|
|
156
|
+
if [ "$CTL_DEAD" -gt 0 ]; then
|
|
157
|
+
echo "🟥 HARNESS-ERROR: 컨트롤 $CTL_DEAD/$CTL_TOTAL 사망 — [$DEAD_LABELS]"
|
|
158
|
+
echo " 착지가 확실한 문자열이 안 잡힌다 = 계기가 대상을 못 읽고 있다."
|
|
159
|
+
echo " 타깃 결과는 **인쇄하지 않는다** — 죽은 계기의 출력은 데이터가 아니라 소음이다."
|
|
160
|
+
echo " 흔한 원인: zsh word-split(\$FILES 인용 누락) · 경로 오류 · 인코딩 불일치."
|
|
161
|
+
exit 10
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# TARGET 이 0건이면 잰 것이 없다. 예전 판본은 여기서 `발화 0 건 전부 착지` 를 인쇄하고 **0 을
|
|
165
|
+
# 냈다** — 빈 프로브 세트가 초록 도장으로 환전됐다는 뜻이고, 그건 이 스크립트가 존재하는
|
|
166
|
+
# 이유와 정확히 반대 방향이다(미측정을 0 으로, 0 을 합격으로). cross-family 감사가 지목했고,
|
|
167
|
+
# self-test 의 p4 픽스처가 그 상태를 **기대값 0 으로 인코딩해** 굳히고 있었다 — 레인이 결함을
|
|
168
|
+
# 방어하는 대신 승인하고 있었던 셈이다([[feedback_not_found_is_not_zero_family]]).
|
|
169
|
+
if ! grep -qE '^TARGET'$'\t' "$PROBES"; then
|
|
170
|
+
echo "🟥 HARNESS-ERROR: probes 에 TARGET 이 없다 — 잰 발화가 0 건이다."
|
|
171
|
+
echo " '전부 착지' 가 아니라 '아무것도 안 쟀다' 다. 빈 프로브 세트는 합격이 아니다."
|
|
172
|
+
exit 10
|
|
173
|
+
fi
|
|
174
|
+
echo "계기 생존: 컨트롤 $CTL_TOTAL/$CTL_TOTAL"
|
|
175
|
+
echo
|
|
176
|
+
|
|
177
|
+
# ── 2단계: 타깃 ──
|
|
178
|
+
MISS=0; N=0
|
|
179
|
+
while IFS=$'\t' read -r kind pat label; do
|
|
180
|
+
[ "${kind:-}" = "TARGET" ] || continue
|
|
181
|
+
N=$((N+1))
|
|
182
|
+
hits=$(_probe_hits "$pat" "${FILES[@]}" | wc -l | tr -d ' '); _rc=${PIPESTATUS[0]}
|
|
183
|
+
if [ "$_rc" -ge 2 ]; then
|
|
184
|
+
printf " 🟥 %-46s 프로브 오류\n" "${label:-$pat}"
|
|
185
|
+
BAD_PAT="${BAD_PAT:+$BAD_PAT, }TARGET/${label:-$pat}"
|
|
186
|
+
elif [ "$hits" -gt 0 ]; then
|
|
187
|
+
printf " ✅ %-46s %s파일\n" "${label:-$pat}" "$hits"
|
|
188
|
+
else
|
|
189
|
+
printf " 🟥 %-46s 미착지\n" "${label:-$pat}"
|
|
190
|
+
MISS=$((MISS+1))
|
|
191
|
+
fi
|
|
192
|
+
done < "$PROBES"
|
|
193
|
+
|
|
194
|
+
echo
|
|
195
|
+
# 깨진 프로브는 '미착지' 가 아니다. 둘을 같은 rc 로 내면 "기록하라" 와 "프로브를 고쳐라" 가
|
|
196
|
+
# 구분되지 않고, 후자를 전자로 읽으면 있지도 않은 누락을 기록하게 된다.
|
|
197
|
+
if [ -n "$BAD_PAT" ]; then
|
|
198
|
+
echo "🟥 HARNESS-ERROR: 프로브 정규식 오류 — [$BAD_PAT]"
|
|
199
|
+
echo " grep 이 에러(rc≥2)를 냈다. 무매치가 아니라 계기 고장이다 — 정규식을 고쳐라."
|
|
200
|
+
exit 10
|
|
201
|
+
fi
|
|
202
|
+
if [ "$MISS" -eq 0 ]; then
|
|
203
|
+
echo "✅ 발화 $N 건 전부 착지"
|
|
204
|
+
exit 0
|
|
205
|
+
fi
|
|
206
|
+
echo "🟥 $N 건 중 $MISS 건 미착지 — 마감 전에 기록하라."
|
|
207
|
+
echo " ⚠️ 가장 잘 빠지는 것은 잊은 발화가 아니라 **행동으로 대응한 발화**다."
|
|
208
|
+
echo " 답장에서 판단까지 내리고 나면 처리된 느낌이 남고, 그 판단이 어디에도 안 적힌다."
|
|
209
|
+
exit 1
|