@chrono-meta/fh-gate 3.0.0 → 3.1.0
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/regression/probes_live.yaml +137 -0
- package/.claude/rules/.residency-patterns.defaults +7 -0
- package/.claude/rules/fh_4axis_gate.md +50 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +12 -2
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +37 -5
- package/README.zh.md +45 -8
- package/docs/STANDARDS_ALIGNMENT.md +61 -0
- package/docs/USER_GUIDE.md +3 -0
- package/docs/USE_CASES.md +50 -0
- package/docs/model_tier_expectations.md +60 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
- package/package.json +40 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/README.md +4 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
- package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
- package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
- package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
- package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
- package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
- package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
- package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +43 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-qp/README.md +71 -0
- package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
- package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
- package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
- package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
- package/plugins/fh-qp/qp_profile.example.yaml +29 -0
- package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
- package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
- package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
- package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
- package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
- package/scripts/chamber_run.sh +14 -5
- package/scripts/com.forge-harness.live-eval.plist +84 -0
- package/scripts/compaction_probe.sh +9 -35
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/map_postprocess.py +90 -0
- package/scripts/outbound_query_guard.sh +131 -0
- package/scripts/outbound_query_hook.sh +373 -0
- package/scripts/package_coverage_check.sh +55 -16
- package/scripts/pipe_verdict_guard.sh +41 -1
- package/scripts/probe_live_eval.sh +240 -0
- package/scripts/probe_live_eval_lib.py +579 -0
- package/scripts/proposal_hook.sh +120 -17
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/selfcheck.sh +41 -1
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +98 -2
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_marker_affected_lanes.sh +93 -0
- package/scripts/test_marker_crossfamily_lanes.sh +90 -6
- package/scripts/test_marker_oracle_lanes.sh +136 -0
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +3 -3
- package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
- package/scripts/test_probe_live_eval_lanes.sh +437 -0
- package/scripts/test_proposal_hook_lanes.sh +22 -1
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_sim_isolated_run_lanes.sh +119 -0
- package/scripts/test_utterance_intake_lanes.sh +414 -0
- package/scripts/test_worktree_reclaim_lanes.sh +70 -0
- package/scripts/transcript_utterances.py +222 -0
- package/scripts/utterance_intake.sh +424 -0
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +353 -1
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +65 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_probe_live_eval_lanes.sh — regression lanes for scripts/probe_live_eval.sh's SCORER and
|
|
3
|
+
# SELECTOR (scripts/probe_live_eval_lib.py). Never calls `claude` and spawns no live session —
|
|
4
|
+
# this pins the scoring logic and the probes.md/probes_live.yaml cross-reference against fixture
|
|
5
|
+
# text, exactly the split ablation_calibrate.sh's own header argues for ("the pair gates the
|
|
6
|
+
# runner; the runner never gates itself" — applied here to the scorer instead of a sim runner).
|
|
7
|
+
#
|
|
8
|
+
# LANE CLASSES
|
|
9
|
+
# score-pair known-pair calibration of score_probe(): PASS / FAIL / UNCALIBRATED(present) /
|
|
10
|
+
# UNCALIBRATED(absent) / FAILED-TO-RUN, both empty-string and missing-file shapes
|
|
11
|
+
# select-guard the mechanical selection rule (class filter, utterance-shape, INERT-ANCHOR,
|
|
12
|
+
# CLI-event exclude) reproduces the 12-selected / 21-excluded split against the
|
|
13
|
+
# REAL probes.md + probes_live.yaml shipped in this repo
|
|
14
|
+
# dead-pointer probes_live.yaml naming an id absent from probes.md is caught (nonzero exit),
|
|
15
|
+
# not silently ignored — this IS the "id 가 probes.md 에 실재하는지" guard the
|
|
16
|
+
# dispatching session's task named explicitly
|
|
17
|
+
# dry-run `--dry-run` on the real files exits 0 and touches nothing under run/ (no live
|
|
18
|
+
# network call, no OUTDIR created)
|
|
19
|
+
#
|
|
20
|
+
# exit: 0 = all lanes as expected · 1 = regression · 10 = harness error (setup failed, not a verdict)
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
25
|
+
LIB="$REPO_ROOT/scripts/probe_live_eval_lib.py"
|
|
26
|
+
RUNNER="$REPO_ROOT/scripts/probe_live_eval.sh"
|
|
27
|
+
PROBES_MD="$REPO_ROOT/.claude/regression/probes.md"
|
|
28
|
+
PROBES_LIVE="$REPO_ROOT/.claude/regression/probes_live.yaml"
|
|
29
|
+
|
|
30
|
+
command -v python3 >/dev/null 2>&1 || { echo "❌ HARNESS-ERROR — python3 missing"; exit 10; }
|
|
31
|
+
[ -f "$LIB" ] || { echo "❌ HARNESS-ERROR — $LIB missing"; exit 10; }
|
|
32
|
+
[ -f "$RUNNER" ] || { echo "❌ HARNESS-ERROR — $RUNNER missing"; exit 10; }
|
|
33
|
+
|
|
34
|
+
T="$(mktemp -d)" || { echo "❌ HARNESS-ERROR — mktemp failed"; exit 10; }
|
|
35
|
+
trap 'rm -rf "$T"' EXIT
|
|
36
|
+
|
|
37
|
+
FAIL=0; N=0
|
|
38
|
+
_lane() { # $1=id $2=class $3=desc $4=expected $5=actual
|
|
39
|
+
N=$((N + 1))
|
|
40
|
+
if [ "$4" = "$5" ]; then
|
|
41
|
+
printf ' ✅ %-8s [%-14s] %s\n' "$1" "$2" "$3"
|
|
42
|
+
else
|
|
43
|
+
printf ' ❌ %-8s [%-14s] %s — expected=%s actual=%s\n' "$1" "$2" "$3" "$4" "$5"
|
|
44
|
+
FAIL=1
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ── score-pair: known-pair calibration of score_probe() ────────────────────────────────────────
|
|
49
|
+
# $1=primary $2=control $3=polarity $4=expect_re -> prints "VERDICT PRIMARY_HIT CONTROL_HIT"
|
|
50
|
+
# `__NONE__` maps to Python None (FAILED-TO-RUN fixture — a file that was never written, distinct
|
|
51
|
+
# from an empty string, which is a file that WAS written with 0 bytes; both must score the same).
|
|
52
|
+
_score() {
|
|
53
|
+
python3 -c "
|
|
54
|
+
import sys
|
|
55
|
+
sys.path.insert(0, '$REPO_ROOT/scripts')
|
|
56
|
+
from probe_live_eval_lib import score_probe
|
|
57
|
+
primary = None if sys.argv[1] == '__NONE__' else sys.argv[1]
|
|
58
|
+
control = None if sys.argv[2] == '__NONE__' else sys.argv[2]
|
|
59
|
+
v, ph, ch = score_probe(primary, control, sys.argv[3], sys.argv[4])
|
|
60
|
+
print('%s %s %s' % (v, ph, ch))
|
|
61
|
+
" "$1" "$2" "$3" "$4"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
echo "── score-pair ────────────────────────────────────────────────────"
|
|
65
|
+
|
|
66
|
+
# P1: polarity=present, pattern fires on primary, does not fire on control → PASS
|
|
67
|
+
r="$(_score '🐿️ Welcome to FH' 'the weather is nice today' present '🐿️')"
|
|
68
|
+
_lane P1 score-pair "present polarity, control clean -> PASS" "PASS True False" "$r"
|
|
69
|
+
|
|
70
|
+
# P2: polarity=present, pattern absent from primary -> FAIL
|
|
71
|
+
r="$(_score 'sure, here is the weather' 'the weather is nice today' present '🐿️')"
|
|
72
|
+
_lane P2 score-pair "present polarity, primary silent -> FAIL" "FAIL False False" "$r"
|
|
73
|
+
|
|
74
|
+
# P3: polarity=present, pattern ALSO fires on control -> UNCALIBRATED (the instrument cannot
|
|
75
|
+
# discriminate — this is the known-pair's whole point: a probe whose control leaks the pattern
|
|
76
|
+
# must not be scored as if it discriminated)
|
|
77
|
+
r="$(_score '🐿️ Welcome to FH' 'random text with 🐿️ in it too' present '🐿️')"
|
|
78
|
+
_lane P3 score-pair "present polarity, control ALSO hits -> UNCALIBRATED" "UNCALIBRATED True True" "$r"
|
|
79
|
+
|
|
80
|
+
# P4: polarity=absent, pattern absent from primary, present on control -> PASS
|
|
81
|
+
r="$(_score 'here are the dependencies' '🐿️ Welcome to FH' absent '🐿️')"
|
|
82
|
+
_lane P4 score-pair "absent polarity, control fires -> PASS" "PASS False True" "$r"
|
|
83
|
+
|
|
84
|
+
# P5: polarity=absent, pattern LEAKS into primary too -> FAIL (a real regression: onboarding
|
|
85
|
+
# leaking into an explicit task-utterance response)
|
|
86
|
+
r="$(_score '🐿️ Welcome to FH — here are the dependencies' '🐿️ Welcome to FH' absent '🐿️')"
|
|
87
|
+
_lane P5 score-pair "absent polarity, primary leaks -> FAIL" "FAIL True True" "$r"
|
|
88
|
+
|
|
89
|
+
# P6: polarity=absent, control ALSO never fires -> UNCALIBRATED (control failed to prove the
|
|
90
|
+
# pattern can appear at all — primary's silence is not evidence of anything)
|
|
91
|
+
r="$(_score 'here are the dependencies' 'also just dependencies' absent '🐿️')"
|
|
92
|
+
_lane P6 score-pair "absent polarity, control never fires -> UNCALIBRATED" "UNCALIBRATED False False" "$r"
|
|
93
|
+
|
|
94
|
+
# P7/P8: FAILED-TO-RUN — missing file (None) and empty-string both collapse to the same verdict,
|
|
95
|
+
# never silently read as a FAIL (CLAUDE.md not-found-is-not-zero discipline).
|
|
96
|
+
r="$(_score '__NONE__' 'the weather is nice' present '🐿️')"
|
|
97
|
+
_lane P7 score-pair "primary file missing -> FAILED-TO-RUN" "FAILED-TO-RUN False False" "$r"
|
|
98
|
+
r="$(_score '' 'the weather is nice' present '🐿️')"
|
|
99
|
+
_lane P8 score-pair "primary file empty -> FAILED-TO-RUN" "FAILED-TO-RUN False False" "$r"
|
|
100
|
+
|
|
101
|
+
# ── reason-pair: FAILED-TO-RUN rows carry a `reason` explaining WHY (2026-09-05) ────────────────
|
|
102
|
+
# WHY: the 2026-09-05 launchd incident scored 12/12 FAILED-TO-RUN with no clue why in the report
|
|
103
|
+
# itself — a human had to go dig through stderr files by hand. score_run() now attaches a `reason`
|
|
104
|
+
# to any FAILED-TO-RUN row: the failing arm's own stderr first line when there is one, else an
|
|
105
|
+
# rc/bytes fallback parsed from the runner's console log. This never touches score_probe() itself
|
|
106
|
+
# (unchanged, still tested above) — reason is a diagnostic label on top of the same verdict.
|
|
107
|
+
# $1=run_root $2=id -> "VERDICT<TAB>REASON"
|
|
108
|
+
_reason_row() {
|
|
109
|
+
python3 -c "
|
|
110
|
+
import sys
|
|
111
|
+
sys.path.insert(0, '$REPO_ROOT/scripts')
|
|
112
|
+
from probe_live_eval_lib import score_run
|
|
113
|
+
live = [{'id': sys.argv[2], 'polarity': 'present', 'expect_re': '🐿️'}]
|
|
114
|
+
res = score_run(live, sys.argv[1], [sys.argv[2]], 0.8, 'sonnet')
|
|
115
|
+
row = res['rows'][0]
|
|
116
|
+
sys.stdout.write('%s\t%s' % (row['verdict'], row.get('reason', '')))
|
|
117
|
+
" "$1" "$2"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
echo ""
|
|
121
|
+
echo "── reason-pair ──────────────────────────────────────────────────"
|
|
122
|
+
|
|
123
|
+
# R-F1: primary's own stderr has a real line (the launchd incident's actual error text) -> reason
|
|
124
|
+
# quotes it verbatim, prefixed by which arm it came from.
|
|
125
|
+
RROOT1="$T/reason_f1"; mkdir -p "$RROOT1/X1"
|
|
126
|
+
printf 'scripts/sim_isolated_run.sh: line 517: timeout: command not found\n' > "$RROOT1/X1/primary_r1.stderr.txt"
|
|
127
|
+
printf 'unused control text' > "$RROOT1/X1/control_r1.txt"
|
|
128
|
+
out="$(_reason_row "$RROOT1" X1)"
|
|
129
|
+
_lane RF1 reason-pair "primary stderr line -> reason quotes it, prefixed 'primary:'" \
|
|
130
|
+
"FAILED-TO-RUN primary: scripts/sim_isolated_run.sh: line 517: timeout: command not found" "$out"
|
|
131
|
+
|
|
132
|
+
# R-F2: stderr file absent/empty -> falls back to rc/bytes, rc parsed from the runner's own
|
|
133
|
+
# console log text (the shape sim_isolated_run.sh itself prints: "(rc=<n>, ...)").
|
|
134
|
+
RROOT2="$T/reason_f2"; mkdir -p "$RROOT2/X2"
|
|
135
|
+
printf ' UNMEASURED (rc=127, 0 bytes) - timeout or crash, NOT a negative result\n' > "$RROOT2/X2/_runner_primary.log"
|
|
136
|
+
printf 'unused' > "$RROOT2/X2/control_r1.txt"
|
|
137
|
+
out="$(_reason_row "$RROOT2" X2)"
|
|
138
|
+
_lane RF2 reason-pair "no stderr line -> rc/bytes fallback parsed from the runner log" \
|
|
139
|
+
"FAILED-TO-RUN primary: rc=127 bytes=0" "$out"
|
|
140
|
+
|
|
141
|
+
# R-F3: the CONTROL side is the one missing (primary present) -> reason is prefixed 'control:',
|
|
142
|
+
# not 'primary:' — proves the label is attributed to the arm that actually failed.
|
|
143
|
+
RROOT3="$T/reason_f3"; mkdir -p "$RROOT3/X3"
|
|
144
|
+
printf '🐿️ Welcome to FH' > "$RROOT3/X3/primary_r1.txt"
|
|
145
|
+
printf 'boom: control side stderr text\n' > "$RROOT3/X3/control_r1.stderr.txt"
|
|
146
|
+
out="$(_reason_row "$RROOT3" X3)"
|
|
147
|
+
_lane RF3 reason-pair "control-side failure -> reason prefixed 'control:', not 'primary:'" \
|
|
148
|
+
"FAILED-TO-RUN control: boom: control side stderr text" "$out"
|
|
149
|
+
|
|
150
|
+
# R-F4 known-negative: a normal PASS row must carry an EMPTY reason — otherwise RF1-RF3 could be
|
|
151
|
+
# passing because `reason` is always non-empty garbage, not because it discriminates on verdict
|
|
152
|
+
# ([[feedback_control_presence_is_not_discrimination]]).
|
|
153
|
+
RROOT4="$T/reason_f4"; mkdir -p "$RROOT4/X4"
|
|
154
|
+
printf '🐿️ Welcome to FH' > "$RROOT4/X4/primary_r1.txt"
|
|
155
|
+
printf 'the weather is nice' > "$RROOT4/X4/control_r1.txt"
|
|
156
|
+
out="$(_reason_row "$RROOT4" X4)"
|
|
157
|
+
_lane RF4 reason-pair "control — a PASS row carries no reason (field is FAILED-TO-RUN-only)" \
|
|
158
|
+
"PASS " "$out"
|
|
159
|
+
|
|
160
|
+
# ── select-guard: mechanical rule reproduces the real 12/21 split ──────────────────────────────
|
|
161
|
+
echo ""
|
|
162
|
+
echo "── select-guard ──────────────────────────────────────────────────"
|
|
163
|
+
|
|
164
|
+
SELECT_JSON="$T/select.json"
|
|
165
|
+
SPEC_DIR="$T/spec"
|
|
166
|
+
sel_out="$(python3 "$LIB" select --probes-md "$PROBES_MD" --probes-live "$PROBES_LIVE" \
|
|
167
|
+
--json-out "$SELECT_JSON" --spec-dir "$SPEC_DIR" 2>&1)"
|
|
168
|
+
sel_rc=$?
|
|
169
|
+
_lane S1 select-guard "real probes.md/probes_live.yaml -> selector exits 0 (no dead pointer)" "0" "$sel_rc"
|
|
170
|
+
|
|
171
|
+
if [ -f "$SELECT_JSON" ]; then
|
|
172
|
+
sel_count=$(python3 -c "import json; print(len(json.load(open('$SELECT_JSON'))['selected']))")
|
|
173
|
+
exc_count=$(python3 -c "import json; print(len(json.load(open('$SELECT_JSON'))['excluded']))")
|
|
174
|
+
else
|
|
175
|
+
sel_count="ERR"; exc_count="ERR"
|
|
176
|
+
fi
|
|
177
|
+
# 🟥 12/21 -> 11/22 on 2026-09-06. These numbers are PINNED on purpose — a derived count would pass
|
|
178
|
+
# no matter what the selector did, which is the one thing this lane exists to prevent. So a change to
|
|
179
|
+
# the selection rule is SUPPOSED to turn these red and force an author to say why. It just did:
|
|
180
|
+
# G-TRIG-03 moved into `ARM_CAPABILITY_EXCLUDE` (probe_live_eval_lib.py) because its probes.md
|
|
181
|
+
# rationale points at a CLAUDE.md table row that the 2026-07-17 row diet deleted, and the behavior was
|
|
182
|
+
# delegated to a skill `description` the arm cannot see (it runs with Read,Grep,Glob and no Skill tool
|
|
183
|
+
# — confirmed by a known-pair: "list every Skill available to you" -> NO-SKILL-TOOL while the
|
|
184
|
+
# tool-listing control answered correctly). It failed 0/5 for that reason alone.
|
|
185
|
+
# 🟥 The exclusion does NOT mean the delegation works — that question is now UNMEASURED, not answered.
|
|
186
|
+
_lane S2 select-guard "selected count == 11 (12 minus G-TRIG-03, ARM_CAPABILITY_EXCLUDE)" "11" "$sel_count"
|
|
187
|
+
_lane S3 select-guard "excluded count == 22 (33 probes.md rows - 11 selected)" "22" "$exc_count"
|
|
188
|
+
|
|
189
|
+
# --subset and --ids filters
|
|
190
|
+
sub_out="$(python3 "$LIB" select --probes-md "$PROBES_MD" --probes-live "$PROBES_LIVE" --subset 3 \
|
|
191
|
+
--spec-dir "$T/spec_subset" 2>&1)"
|
|
192
|
+
sub_count=$(wc -l < "$T/spec_subset/selected_ids.txt" 2>/dev/null | tr -d ' ')
|
|
193
|
+
_lane S4 select-guard "--subset 3 yields exactly 3 selected ids" "3" "${sub_count:-ERR}"
|
|
194
|
+
|
|
195
|
+
ids_out="$(python3 "$LIB" select --probes-md "$PROBES_MD" --probes-live "$PROBES_LIVE" \
|
|
196
|
+
--ids "G-GREET-01,G-TRIG-02" --spec-dir "$T/spec_ids" 2>&1)"
|
|
197
|
+
ids_count=$(wc -l < "$T/spec_ids/selected_ids.txt" 2>/dev/null | tr -d ' ')
|
|
198
|
+
_lane S5 select-guard "--ids G-GREET-01,G-TRIG-02 yields exactly 2" "2" "${ids_count:-ERR}"
|
|
199
|
+
|
|
200
|
+
unk_out="$(python3 "$LIB" select --probes-md "$PROBES_MD" --probes-live "$PROBES_LIVE" \
|
|
201
|
+
--ids "G-NOT-A-REAL-ID-99" --spec-dir "$T/spec_unk" 2>&1)"
|
|
202
|
+
case "$unk_out" in
|
|
203
|
+
*"unknown ids requested"*) unk_hit="yes" ;;
|
|
204
|
+
*) unk_hit="no" ;;
|
|
205
|
+
esac
|
|
206
|
+
_lane S6 select-guard "--ids with an unknown id is reported, not silently dropped" "yes" "$unk_hit"
|
|
207
|
+
|
|
208
|
+
# ── dead-pointer: probes_live.yaml naming an id absent from probes.md must fail loudly ─────────
|
|
209
|
+
echo ""
|
|
210
|
+
echo "── dead-pointer ──────────────────────────────────────────────────"
|
|
211
|
+
|
|
212
|
+
BAD_YAML="$T/probes_live_bad.yaml"
|
|
213
|
+
{
|
|
214
|
+
cat "$PROBES_LIVE"
|
|
215
|
+
cat <<'BADEOF'
|
|
216
|
+
- id: G-DOES-NOT-EXIST-99
|
|
217
|
+
polarity: present
|
|
218
|
+
input: "this id has no row in probes.md"
|
|
219
|
+
expect_re: "x"
|
|
220
|
+
control_input: "y"
|
|
221
|
+
BADEOF
|
|
222
|
+
} > "$BAD_YAML"
|
|
223
|
+
|
|
224
|
+
python3 "$LIB" select --probes-md "$PROBES_MD" --probes-live "$BAD_YAML" \
|
|
225
|
+
--json-out "$T/select_bad.json" --spec-dir "$T/spec_bad" >"$T/bad_out.txt" 2>&1
|
|
226
|
+
bad_rc=$?
|
|
227
|
+
_lane D1 dead-pointer "id absent from probes.md -> select exits nonzero" "nonzero" "$([ "$bad_rc" -ne 0 ] && echo nonzero || echo zero)"
|
|
228
|
+
grep -q "DEAD-POINTER: G-DOES-NOT-EXIST-99" "$T/bad_out.txt" && d2=found || d2=missing
|
|
229
|
+
_lane D2 dead-pointer "dead-pointer id named explicitly in the warning" "found" "$d2"
|
|
230
|
+
|
|
231
|
+
# Known-negative for D1/D2: the SAME check must NOT fire on the real, uncorrupted file — otherwise
|
|
232
|
+
# D1/D2 could be passing on a scanner that always says "dead pointer found" regardless of input.
|
|
233
|
+
python3 "$LIB" select --probes-md "$PROBES_MD" --probes-live "$PROBES_LIVE" \
|
|
234
|
+
--json-out "$T/select_clean.json" --spec-dir "$T/spec_clean" >"$T/clean_out.txt" 2>&1
|
|
235
|
+
clean_rc=$?
|
|
236
|
+
grep -q "DEAD-POINTER" "$T/clean_out.txt" && d3=found || d3=missing
|
|
237
|
+
_lane D3 dead-pointer "control: real file has no dead pointer, rc=0" "0 missing" "${clean_rc} ${d3}"
|
|
238
|
+
|
|
239
|
+
# ── dry-run: probe_live_eval.sh --dry-run touches nothing under a fresh OUTDIR and exits 0 ─────
|
|
240
|
+
echo ""
|
|
241
|
+
echo "── dry-run ───────────────────────────────────────────────────────"
|
|
242
|
+
|
|
243
|
+
DRY_OUT="$T/dryrun_out"
|
|
244
|
+
dry_stdout="$(cd "$REPO_ROOT" && bash "$RUNNER" --dry-run --out "$DRY_OUT" 2>&1)"
|
|
245
|
+
dry_rc=$?
|
|
246
|
+
_lane R1 dry-run "probe_live_eval.sh --dry-run exits 0" "0" "$dry_rc"
|
|
247
|
+
_lane R2 dry-run "--dry-run creates no OUTDIR (no live run attempted)" "absent" "$([ -d "$DRY_OUT" ] && echo present || echo absent)"
|
|
248
|
+
case "$dry_stdout" in
|
|
249
|
+
*"SELECTED (11)"*) r3=yes ;;
|
|
250
|
+
*) r3=no ;;
|
|
251
|
+
esac
|
|
252
|
+
_lane R3 dry-run "--dry-run stdout shows the real 11-probe selection" "yes" "$r3"
|
|
253
|
+
|
|
254
|
+
# ── fail-fast: probe_live_eval.sh aborts on the FIRST rc=2 runner call, not after burning the
|
|
255
|
+
# rest of the selected set (2026-09-05, the launchd incident this exists for) ────────────────────
|
|
256
|
+
# WHY: sim_isolated_run.sh's own usage guards exit 2 before ever calling `claude` (bad flags, no
|
|
257
|
+
# `claude` on PATH, or — the actual incident — a launchd PATH with no `timeout(1)` resolvable
|
|
258
|
+
# before that runner grew its own bash-fallback). That condition is identical for every remaining
|
|
259
|
+
# probe in the run, so continuing just burns the rest of the clones on an environment already
|
|
260
|
+
# known broken. These lanes stub OUT sim_isolated_run.sh entirely via FH_SIM_RUNNER_BIN (added to
|
|
261
|
+
# probe_live_eval.sh for exactly this) so the fail-fast branch can be tested without a live
|
|
262
|
+
# `claude` call at all.
|
|
263
|
+
echo ""
|
|
264
|
+
echo "── fail-fast ────────────────────────────────────────────────────"
|
|
265
|
+
|
|
266
|
+
STUBROOT="$T/failfast_stub"; mkdir -p "$STUBROOT"
|
|
267
|
+
|
|
268
|
+
# Broken stand-in: exactly what sim_isolated_run.sh's own preflight guards do — print one line to
|
|
269
|
+
# stderr and exit 2, never touching a network or spawning `claude`. Records its own invocation
|
|
270
|
+
# count so the lane can prove the caller stopped after the FIRST call.
|
|
271
|
+
cat > "$STUBROOT/fake_sim_broken.sh" <<'FAKESIM'
|
|
272
|
+
#!/usr/bin/env bash
|
|
273
|
+
: "${FH_FAKESIM_COUNTER:?FH_FAKESIM_COUNTER must be set by the caller}"
|
|
274
|
+
echo "$$" >> "$FH_FAKESIM_COUNTER"
|
|
275
|
+
echo "FAIL: claude CLI not on PATH" >&2
|
|
276
|
+
exit 2
|
|
277
|
+
FAKESIM
|
|
278
|
+
chmod +x "$STUBROOT/fake_sim_broken.sh"
|
|
279
|
+
|
|
280
|
+
# Healthy stand-in (known-negative control): same argv shape, writes a plausible output file and
|
|
281
|
+
# exits 0 for EVERY call — proves FF1/FF2 discriminate on rc=2 specifically, not on "stopped after
|
|
282
|
+
# one call" regardless of what the runner returns.
|
|
283
|
+
cat > "$STUBROOT/fake_sim_ok.sh" <<'FAKESIMOK'
|
|
284
|
+
#!/usr/bin/env bash
|
|
285
|
+
: "${FH_FAKESIM_COUNTER:?FH_FAKESIM_COUNTER must be set by the caller}"
|
|
286
|
+
echo "$$" >> "$FH_FAKESIM_COUNTER"
|
|
287
|
+
arm=""; out=""
|
|
288
|
+
while [ $# -gt 0 ]; do
|
|
289
|
+
case "$1" in
|
|
290
|
+
--arm) arm="$2"; shift 2 ;;
|
|
291
|
+
--out) out="$2"; shift 2 ;;
|
|
292
|
+
*) shift ;;
|
|
293
|
+
esac
|
|
294
|
+
done
|
|
295
|
+
mkdir -p "$out"
|
|
296
|
+
echo "stub ok output" > "$out/${arm}_r1.txt"
|
|
297
|
+
echo "RESULT: CLEAN"
|
|
298
|
+
exit 0
|
|
299
|
+
FAKESIMOK
|
|
300
|
+
chmod +x "$STUBROOT/fake_sim_ok.sh"
|
|
301
|
+
|
|
302
|
+
# 🟥 FF5 guard — the live nightly record must be untouched by this suite. Measured 2026-09-05 10:18:
|
|
303
|
+
# FF4 completed the REAL script with a stub runner and, with no --report-out, replaced that night's
|
|
304
|
+
# tracks/_meta/live_eval_<date>.md with stub values. A lane that writes into the live artifact path
|
|
305
|
+
# is the fleet class in miniature ([[feedback_sim_with_write_tools_is_a_fleet]]).
|
|
306
|
+
LIVE_REPORT="$REPO_ROOT/tracks/_meta/live_eval_$(date +%Y-%m-%d).md"
|
|
307
|
+
_live_hash() { if [ -f "$LIVE_REPORT" ]; then shasum "$LIVE_REPORT" | cut -c1-40; else echo ABSENT; fi; }
|
|
308
|
+
live_before="$(_live_hash)"
|
|
309
|
+
COUNTER1="$T/failfast_counter_broken.txt"; : > "$COUNTER1"
|
|
310
|
+
ff_out="$(cd "$REPO_ROOT" && FH_SIM_RUNNER_BIN="$STUBROOT/fake_sim_broken.sh" FH_FAKESIM_COUNTER="$COUNTER1" \
|
|
311
|
+
bash "$RUNNER" --subset 2 --model sonnet --out "$T/failfast_run_broken" --report-out "$T/failfast_report_broken.md" 2>&1)"
|
|
312
|
+
ff_rc=$?
|
|
313
|
+
_lane FF1 fail-fast "aborts with rc=2 on the runner's own preflight failure" "2" "$ff_rc"
|
|
314
|
+
ff_calls=$(wc -l < "$COUNTER1" | tr -d ' ')
|
|
315
|
+
_lane FF2 fail-fast "stops after exactly 1 runner call (does not burn the 2nd probe's 3 remaining calls)" "1" "$ff_calls"
|
|
316
|
+
case "$ff_out" in
|
|
317
|
+
*"Aborting the whole run"*) ff_msg=yes ;;
|
|
318
|
+
*) ff_msg=no ;;
|
|
319
|
+
esac
|
|
320
|
+
_lane FF3 fail-fast "abort message names what happened (not a silent stop)" "yes" "$ff_msg"
|
|
321
|
+
|
|
322
|
+
# Known-negative control: the SAME --subset 2 (2 probes x primary+control = 4 calls) against a
|
|
323
|
+
# HEALTHY runner must run to completion, not stop early — otherwise FF1/FF2 could be passing
|
|
324
|
+
# because the loop always stops after one call for any reason at all
|
|
325
|
+
# ([[feedback_control_presence_is_not_discrimination]]).
|
|
326
|
+
COUNTER2="$T/failfast_counter_ok.txt"; : > "$COUNTER2"
|
|
327
|
+
( cd "$REPO_ROOT" && FH_SIM_RUNNER_BIN="$STUBROOT/fake_sim_ok.sh" FH_FAKESIM_COUNTER="$COUNTER2" \
|
|
328
|
+
bash "$RUNNER" --subset 2 --model sonnet --out "$T/failfast_run_ok" --report-out "$T/failfast_report_ok.md" ) >/dev/null 2>&1
|
|
329
|
+
ff2_calls=$(wc -l < "$COUNTER2" | tr -d ' ')
|
|
330
|
+
_lane FF4 fail-fast "control — a healthy runner (rc=0) is called for all 4 (2 probes x 2 arms), not stopped early" "4" "$ff2_calls"
|
|
331
|
+
live_after="$(_live_hash)"
|
|
332
|
+
_lane FF5 fail-fast "live nightly record untouched by the suite (hash before == after, or both ABSENT)" "$live_before" "$live_after"
|
|
333
|
+
[ -s "$T/failfast_report_ok.md" ] && ff_rep=yes || ff_rep=no
|
|
334
|
+
_lane FF6 fail-fast "--report-out receives the report instead of the live path" "yes" "$ff_rep"
|
|
335
|
+
|
|
336
|
+
# 🟥 FF7/FF7b — the seam must bypass the CLI preflight, and ONLY the seam. Measured 2026-09-05 on
|
|
337
|
+
# CI (ubuntu, no `claude` on PATH): probe_live_eval.sh checked `command -v claude` BEFORE the
|
|
338
|
+
# runner seam, so the stub was never called — FF2/FF3/FF4/FF6 red, FF1 green by coincidence (both
|
|
339
|
+
# paths exit 2). A developer machine with `claude` installed cannot see that, so this lane HIDES
|
|
340
|
+
# `claude` (a shadow PATH of symlinks to every other executable) and re-runs the healthy stub.
|
|
341
|
+
# FF7b is the control: on the REAL runner path with no `claude`, the script must still refuse.
|
|
342
|
+
SHADOW_NOCLAUDE="$T/shadow_noclaude"; mkdir -p "$SHADOW_NOCLAUDE"
|
|
343
|
+
IFS=':' read -r -a _ff_dirs <<< "$PATH"
|
|
344
|
+
for _d in "${_ff_dirs[@]}"; do
|
|
345
|
+
[ -d "$_d" ] || continue
|
|
346
|
+
for _f in "$_d"/*; do
|
|
347
|
+
[ -x "$_f" ] && [ ! -d "$_f" ] || continue
|
|
348
|
+
_b="${_f##*/}"; [ "$_b" = claude ] && continue
|
|
349
|
+
[ -e "$SHADOW_NOCLAUDE/$_b" ] || ln -s "$_f" "$SHADOW_NOCLAUDE/$_b"
|
|
350
|
+
done
|
|
351
|
+
done
|
|
352
|
+
if PATH="$SHADOW_NOCLAUDE" command -v claude >/dev/null 2>&1; then
|
|
353
|
+
_lane FF7-FIXTURE fail-fast "shadow PATH hides claude (fixture potency — cannot run FF7 on this machine)" "hidden" "still-visible"
|
|
354
|
+
else
|
|
355
|
+
COUNTER3="$T/failfast_counter_noclaude.txt"; : > "$COUNTER3"
|
|
356
|
+
( cd "$REPO_ROOT" && PATH="$SHADOW_NOCLAUDE" FH_SIM_RUNNER_BIN="$STUBROOT/fake_sim_ok.sh" FH_FAKESIM_COUNTER="$COUNTER3" \
|
|
357
|
+
bash "$RUNNER" --subset 2 --model sonnet --out "$T/failfast_run_noclaude" --report-out "$T/failfast_report_noclaude.md" ) >/dev/null 2>&1
|
|
358
|
+
ff7_calls=$(wc -l < "$COUNTER3" | tr -d ' ')
|
|
359
|
+
_lane FF7 fail-fast "claude absent from PATH + stub runner: stub still called for all 4 (the seam bypasses the CLI preflight)" "4" "$ff7_calls"
|
|
360
|
+
ff7b_out="$(cd "$REPO_ROOT" && PATH="$SHADOW_NOCLAUDE" bash "$RUNNER" --subset 2 --model sonnet --out "$T/failfast_run_noclaude_real" --report-out "$T/failfast_report_noclaude_real.md" 2>&1)"
|
|
361
|
+
ff7b_rc=$?
|
|
362
|
+
case "$ff7b_out" in *"claude CLI not on PATH"*) ff7b_msg=yes ;; *) ff7b_msg=no ;; esac
|
|
363
|
+
_lane FF7b fail-fast "control — the REAL runner path with claude absent still exits 2 and names claude" "2/yes" "$ff7b_rc/$ff7b_msg"
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
echo "── majority over reps (2026-09-06) ───────────────────────────────"
|
|
368
|
+
# WHY THESE LANES EXIST. Until 2026-09-06 the scorer read only `*_r1.txt`, so a probe's verdict was
|
|
369
|
+
# one sample. Re-scoring three real run artifacts then found 5 of 12 probes FLAKY — two runs 15
|
|
370
|
+
# minutes apart with an identical `corpus_head_date` flipped 4 of them, and observed pass_rate was
|
|
371
|
+
# 0.50 / 0.67 / 0.67. The single-rep noise band is wider than the distance to the 0.80 threshold,
|
|
372
|
+
# so a reps=1 pass_rate cannot support a threshold decision. score_run now takes `reps` and votes.
|
|
373
|
+
# These lanes pin the composition rules — especially MV4, which is the one a majority could quietly
|
|
374
|
+
# launder.
|
|
375
|
+
_score_run() { # $1=reps $2..= per-rep spec, one arg per rep: "P" pass | "F" fail | "U" uncal | "X" not-run
|
|
376
|
+
local reps="$1"; shift
|
|
377
|
+
python3 - "$REPO_ROOT" "$reps" "$@" <<'PY'
|
|
378
|
+
import sys, os, json, tempfile
|
|
379
|
+
sys.path.insert(0, os.path.join(sys.argv[1], 'scripts'))
|
|
380
|
+
from probe_live_eval_lib import score_run
|
|
381
|
+
reps = int(sys.argv[2]); specs = sys.argv[3:]
|
|
382
|
+
root = tempfile.mkdtemp(); base = os.path.join(root, 'G-X'); os.makedirs(base)
|
|
383
|
+
# pattern '🐿️'; present polarity. primary hit = pattern in primary; control hit = pattern in control.
|
|
384
|
+
for i, kind in enumerate(specs, start=1):
|
|
385
|
+
if kind == 'X': # neither file written -> that rep did not run
|
|
386
|
+
continue
|
|
387
|
+
prim = '🐿️ hi' if kind in ('P', 'U') else 'nothing here'
|
|
388
|
+
ctrl = '🐿️ also here' if kind == 'U' else 'weather'
|
|
389
|
+
open(os.path.join(base, 'primary_r%d.txt' % i), 'w').write(prim)
|
|
390
|
+
open(os.path.join(base, 'control_r%d.txt' % i), 'w').write(ctrl)
|
|
391
|
+
rows = score_run([{'id': 'G-X', 'polarity': 'present', 'expect_re': '🐿️'}],
|
|
392
|
+
root, ['G-X'], 0.8, 'sonnet', reps=reps)['rows'][0]
|
|
393
|
+
print('%s %s' % (rows['verdict'], rows.get('reps')))
|
|
394
|
+
PY
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
r="$(_score_run 3 P P P)"
|
|
398
|
+
_lane MV1 majority "3 reps all pass -> PASS, spread kept as 3/3" "PASS 3/3" "$r"
|
|
399
|
+
|
|
400
|
+
r="$(_score_run 3 P P F)"
|
|
401
|
+
_lane MV2 majority "2 of 3 pass -> PASS (strict majority), spread 2/3" "PASS 2/3" "$r"
|
|
402
|
+
|
|
403
|
+
r="$(_score_run 3 P F F)"
|
|
404
|
+
_lane MV3 majority "1 of 3 pass -> FAIL, spread 1/3" "FAIL 1/3" "$r"
|
|
405
|
+
|
|
406
|
+
# 🟥 MV4 is the load-bearing one. UNCALIBRATED is deliberately NOT majority-voted: if the pattern
|
|
407
|
+
# fires on a known-negative even once, discrimination is in doubt, and a 2-of-3 majority would
|
|
408
|
+
# launder that doubt into a PASS. Two of the three reps here pass cleanly — a naive majority says
|
|
409
|
+
# PASS. The scorer must not.
|
|
410
|
+
r="$(_score_run 3 P P U)"
|
|
411
|
+
_lane MV4 majority "one rep UNCALIBRATED outranks a passing majority (no laundering)" "UNCALIBRATED 2/3" "$r"
|
|
412
|
+
|
|
413
|
+
# MV5: a rep that never ran is excluded from the denominator, not counted as a failure — "did not
|
|
414
|
+
# run" and "ran and failed" are different facts (the not-found-is-not-zero rule).
|
|
415
|
+
r="$(_score_run 3 X P P)"
|
|
416
|
+
_lane MV5 majority "a non-running rep leaves the denominator, not the numerator" "PASS 2/2" "$r"
|
|
417
|
+
|
|
418
|
+
r="$(_score_run 3 X X X)"
|
|
419
|
+
_lane MV6 majority "no rep ran -> FAILED-TO-RUN, never FAIL" "FAILED-TO-RUN 0/0" "$r"
|
|
420
|
+
|
|
421
|
+
# MV7: control — the default path (reps=1) must be byte-for-byte the old behavior. A change that
|
|
422
|
+
# only works at reps=3 would silently alter every existing caller.
|
|
423
|
+
r="$(_score_run 1 P)"
|
|
424
|
+
_lane MV7 majority "control: reps=1 unchanged (PASS, 1/1)" "PASS 1/1" "$r"
|
|
425
|
+
|
|
426
|
+
r="$(_score_run 1 F)"
|
|
427
|
+
_lane MV8 majority "control: reps=1 failing case unchanged" "FAIL 0/1" "$r"
|
|
428
|
+
|
|
429
|
+
echo ""
|
|
430
|
+
echo "── summary ──────────────────────────────────────────────────────"
|
|
431
|
+
echo "lanes: $N failed: $([ "$FAIL" -eq 0 ] && echo 0 || echo '>=1')"
|
|
432
|
+
if [ "$FAIL" -ne 0 ]; then
|
|
433
|
+
echo "RESULT: REGRESSION"
|
|
434
|
+
exit 1
|
|
435
|
+
fi
|
|
436
|
+
echo "RESULT: CLEAN"
|
|
437
|
+
exit 0
|
|
@@ -15,9 +15,30 @@ exp "Bash sed -i on scripts/*.sh with token" HIT '{"tool_name":"Bash","tool_i
|
|
|
15
15
|
exp "Bash sed -i token ONLY inside quotes (a1)" HIT '{"tool_name":"Bash","tool_input":{"command":"sed -i \"\" \"s/exit 1/exit 2/\" scripts/target.sh"}}'
|
|
16
16
|
exp "Bash sed -i single-quoted token (a1b)" HIT '{"tool_name":"Bash","tool_input":{"command":"sed -i '"'"''"'"' '"'"'s/|| continue/|| { echo x; continue; }/'"'"' scripts/target.sh"}}'
|
|
17
17
|
exp "Bash sed -i no token anywhere (a3)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"sed -i \"\" s/foo/bar/ scripts/target.sh"}}'
|
|
18
|
-
exp "Bash redirect into scripts/*.sh w/ token" HIT '{"tool_name":"Bash","tool_input":{"command":"printf \"
|
|
18
|
+
exp "Bash redirect into scripts/*.sh w/ token" HIT '{"tool_name":"Bash","tool_input":{"command":"printf \"[ -f x ] || exit 1\\n\" >> scripts/x.sh; grep -q y scripts/x.sh"}}'
|
|
19
19
|
exp "Bash redirect into docs (no)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"echo \"exit 1\" >> docs/a.md || exit 1"}}'
|
|
20
20
|
exp "Bash ls only (no target)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"ls scripts/ && [ -d scripts ] || exit 1"}}'
|
|
21
|
+
# ── 2026-09-04 계기 교체 lanes — fixtures are the SHAPES the first independent grading quoted
|
|
22
|
+
# (tracks/_meta/RESULT_2026-09-04_identity5-armC-live-count.md §계기 결함), not easier spellings.
|
|
23
|
+
# Fail-before: the HEAD~ hook (whole-command regex) is HIT on R2/R3/R4-ctrl/D3-*/D4 and CLEAN on D2-* (recorded in
|
|
24
|
+
# proposal_hook_repair_lanes.txt for the patch); the scanner hook inverts exactly those.
|
|
25
|
+
exp "R2 marker heredoc QUOTES sed -i … scripts/target.sh (row 2)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"cat > tracks/_meta/.axes_23_2026-09-03.marker <<'"'"'MK'"'"'\naxes-run: ⓐ ⓑ\na1 `sed -i '"'"''"'"' '"'"'s/exit 1/exit 2/'"'"' scripts/target.sh` → HIT · lanes 16/16 · [ -s x ] || exit 1\nMK"}}'
|
|
26
|
+
exp "R3 gh pr --body QUOTES sed -i … scripts/target.sh (row 3)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"gh pr create --title \"fix(hook): a1\" --body \"Lanes: a1 `sed -i '"'"''"'"' '"'"'s/exit 1/exit 2/'"'"' scripts/target.sh` now HIT; 16/16 · revert 14/16 || exit 1\""}}'
|
|
27
|
+
exp "R4-ctrl : > \"\$T/scripts/…\" fixture root (var path)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"mkdir -p \"$T/scripts\"; : > \"$T/scripts/test_has_lane_lanes.sh\"; [ -f x ] || exit 1"}}'
|
|
28
|
+
exp "D2-P python heredoc open(p,\"w\") verdict edit (8746)" HIT '{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\np=\"scripts/package_coverage_check.sh\"\ns=open(p).read()\nassert s.count(\" exit 2\") == 1\ns=s.replace(\" exit 2\", \" echo \\\"PKG_ORACLE_MISSING: npm pack gave no files[]\\\" >&2; exit 2\")\nopen(p,\"w\").write(s)\nPY"}}'
|
|
29
|
+
exp "D2-P2 python heredoc q=Path(…); q.write_text (verdict)" HIT '{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\nfrom pathlib import Path\nq=Path(\"scripts/selfcheck.sh\")\nq.write_text(q.read_text().replace(\"run_lane x\", \"run_lane x || exit 1\"))\nPY"}}'
|
|
30
|
+
exp "D2-ctrl python heredoc comment-word replace (== in code only)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\np=\"scripts/sim_isolated_run.sh\"\ns=open(p).read()\nassert s.count(\"# 옛말\") == 1\ns=s.replace(\"# 옛말\", \"# 조직\")\nopen(p,\"w\").write(s)\nPY"}}'
|
|
31
|
+
exp "D3 row 6: sed comment word + || exit 1 in another segment" CLEAN '{"tool_name":"Bash","tool_input":{"command":"sed -i '"'"''"'"' '"'"'s/옛말/조직/g'"'"' scripts/sim_isolated_run.sh && bash scripts/test_sim_isolated_run_lanes.sh; rc=$?; [ $rc = 0 ] || exit 1"}}'
|
|
32
|
+
exp "D3 row 1: self-probe sed (no token in expr) + [ -s ] || exit 1" CLEAN '{"tool_name":"Bash","tool_input":{"command":"sed -i '"'"''"'"' '"'"'s/^#NOOP-PROBE-LINE$//'"'"' scripts/proposal_hook.sh && [ -s scripts/proposal_hook.sh ] || exit 1; tail -1 .claude/.proposal_hook_events.tsv"}}'
|
|
33
|
+
exp "D4 old shape: payload w/o token, check in next segment" CLEAN '{"tool_name":"Bash","tool_input":{"command":"printf \"%s\\n\" x >> scripts/x.sh; grep -q y scripts/x.sh || exit 3"}}'
|
|
34
|
+
exp "H1 cat > scripts/new.sh <<EOF body carries token" HIT '{"tool_name":"Bash","tool_input":{"command":"cat > scripts/new_lane.sh <<'"'"'EOF'"'"'\n#!/usr/bin/env bash\n[ -f x ] || exit 1\nEOF"}}'
|
|
35
|
+
exp "H2 cat <<EOF > \"scripts/q.sh\" (quoted target after >)" HIT '{"tool_name":"Bash","tool_input":{"command":"cat <<'"'"'EOF'"'"' > \"scripts/q.sh\"\nexit 1\nEOF"}}'
|
|
36
|
+
exp "H3 printf | tee -a scripts/t.sh" HIT '{"tool_name":"Bash","tool_input":{"command":"printf \"exit 1\\n\" | tee -a scripts/t.sh"}}'
|
|
37
|
+
# R4 proper: the real edit is a python heredoc whose CONTENT quotes `: > "$T/scripts/test_has_lane_lanes.sh"` — the hook
|
|
38
|
+
# must record the file the python writes, not the fixture root inside the string (row 4 recorded `$T/scripts/…`).
|
|
39
|
+
R4='{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\np=\"scripts/test_proposal_hook_lanes.sh\"\ns=open(p).read()\ns=s.replace(\"exp \\\"noqa\", \"mkdir -p \\\"$T/scripts\\\"; : > \\\"$T/scripts/test_has_lane_lanes.sh\\\"; [ -f x ] || exit 1; exp \\\"noqa\")\nopen(p,\"w\").write(s)\nPY"}}'
|
|
40
|
+
printf '%s' "$R4" | bash "$HDIR/proposal_hook.sh" >/dev/null 2>&1; r4fp=$(tail -1 "$T/.claude/.proposal_hook_events.tsv" 2>/dev/null | cut -f3)
|
|
41
|
+
if [ "$r4fp" = "scripts/test_proposal_hook_lanes.sh" ]; then printf ' ✅ %-52s fp=%s\n' "R4 python heredoc → recorded fp is the WRITTEN file" "$r4fp"; pass=$((pass+1)); else printf ' ❌ %-52s fp=%s (expected scripts/test_proposal_hook_lanes.sh)\n' "R4 python heredoc → recorded fp is the WRITTEN file" "${r4fp:-<none>}"; fail=$((fail+1)); fi
|
|
21
42
|
exp "G1 Edit templates/.git-hooks/pre-commit (no .sh)" HIT '{"tool_name":"Edit","tool_input":{"file_path":"/x/templates/.git-hooks/pre-commit","old_string":" [ -f x ] || continue","new_string":" [ -f x ] || { echo missing; PTR_FAIL=1; continue; }"}}'
|
|
22
43
|
exp "G1-ctrl Edit .git-hooks docs-ish no token" CLEAN '{"tool_name":"Edit","tool_input":{"file_path":"/x/templates/.git-hooks/pre-commit","old_string":"# note a","new_string":"# note b"}}'
|
|
23
44
|
exp "noqa exempts" CLEAN '{"tool_name":"Edit","tool_input":{"file_path":"/x/scripts/a.sh","old_string":"a","new_string":"exit 1 # noqa: proposal-hook"}}'
|