@chrono-meta/fh-gate 2.15.1 → 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 +88 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +13 -3
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +43 -6
- 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 +77 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -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 +35 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
- package/package.json +48 -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 +67 -0
- 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/backtick_guard.sh +194 -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/context_continuity_score.sh +49 -7
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/fh-gate.sh +3 -3
- package/scripts/files_manifest_shipping_check.sh +19 -0
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/gate_pathspec_check.sh +1 -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 +79 -18
- 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 +192 -0
- package/scripts/public_surface_scan_files.sh +11 -2
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/revert_probe.sh +250 -0
- package/scripts/selfcheck.sh +106 -3
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +195 -9
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_backtick_guard_lanes.sh +115 -0
- package/scripts/test_degrade_scan_shell_probes.sh +7 -7
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_heavy_classifier_lanes.sh +1 -1
- package/scripts/test_lane_runner_lanes.sh +59 -33
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_mapped_tracks_lanes.sh +1 -1
- 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_marker_soul_check_lanes.sh +24 -0
- package/scripts/test_node_check_lanes.sh +34 -34
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_package_coverage_lanes.sh +53 -27
- package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
- package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +16 -7
- package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
- 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 +57 -0
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_revert_probe_lanes.sh +146 -0
- package/scripts/test_session_close_lanes.sh +3 -5
- package/scripts/test_sim_isolated_run_lanes.sh +136 -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/utterance_landing_check.sh +2 -2
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +380 -5
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +101 -1
- package/plugins/fh-commons/README.md +0 -38
|
@@ -46,6 +46,18 @@
|
|
|
46
46
|
# same clone, --tools "", no --restricted → "🐿️" (memory PRESENT)
|
|
47
47
|
# One variable, opposite answers. ([[feedback_instrument_cannot_discriminate_hypotheses]])
|
|
48
48
|
#
|
|
49
|
+
# 🟥 THE SAME FLAG ALSO KILLS `act` MODE'S WRITE PATH — measured 2026-09-03, known-pair.
|
|
50
|
+
# `--restricted` ignores settings for PERMISSIONS too, not just memory, and `-p` has no TTY to
|
|
51
|
+
# ask a human — so `act`+`--no-harness` cannot Write/Edit AT ALL, regardless of what the model
|
|
52
|
+
# decides. Confirmed with one variable (`--restricted` on/off, same prompt, same --tools): Edit
|
|
53
|
+
# succeeds without it; with it every rep says "The edit was blocked — permission to write to
|
|
54
|
+
# file.txt wasn't granted." Deterministic, not a race — this is why a real run reads 5/5.
|
|
55
|
+
# ⇒ A "did not edit" result from `act --no-harness` is NOT evidence the base model chose not to
|
|
56
|
+
# write. Use `--no-harness` as an observe-mode control (memory presence, per the pair above)
|
|
57
|
+
# only — never to ask whether a write would have happened. Workaround not built: `--restricted`
|
|
58
|
+
# still honors an explicit `--settings <file>` (per `claude --help`), so a minimal file carrying
|
|
59
|
+
# only Write/Edit approval could restore act-mode fidelity here — filed, not implemented.
|
|
60
|
+
#
|
|
49
61
|
# 🟢 AND THE DEFECT IS REUSABLE AS AN INSTRUMENT. `--no-harness` answers a question this repo
|
|
50
62
|
# asks constantly and usually by eye: **does this behaviour come from FH, or would the base
|
|
51
63
|
# model have done it anyway?** Run the same prompt with and without the flag; a behaviour that
|
|
@@ -69,17 +81,91 @@
|
|
|
69
81
|
# FH defect (a normal session has Bash and can `ls`) — but scoring an arm without knowing it
|
|
70
82
|
# produces a confident zero from a fixture that was never observable.
|
|
71
83
|
#
|
|
72
|
-
# 🟥 PROJECT HOOKS DO
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
84
|
+
# 🟥 PROJECT HOOKS DO RUN IN A DISPOSABLE CLONE — an earlier version of this paragraph said the
|
|
85
|
+
# opposite, and it was wrong (RETRACTED 2026-09-03). It read: "PROJECT HOOKS DO NOT RUN IN A
|
|
86
|
+
# DISPOSABLE CLONE … measured 2026-08-30: an arm copied `.claude/settings.json` into its clone to
|
|
87
|
+
# make the PreToolUse PriorArt hook live; not one of the three clones grew
|
|
88
|
+
# `.claude/.prior_art_events.tsv`". That absence was read as "hooks never fire here" with NO
|
|
89
|
+
# control arm whose hook was known to fire — and it narrowed two rounds of the identity-⑤
|
|
90
|
+
# design (the hook-layer arm was declared unmeasurable). Known pair, 2026-09-03: a PreToolUse
|
|
91
|
+
# Bash hook that appends to `$CLAUDE_PROJECT_DIR/_hook_evidence.txt`, installed via
|
|
92
|
+
# `--setup 'cp <settings.json> .claude/settings.json'`, FIRED inside this runner's own clone
|
|
93
|
+
# (act mode, deny settings.local.json present) and in a bare `claude -p` clone, trusted or not.
|
|
94
|
+
# Why the 2026-08-30 arm saw nothing — CONFIRMED the same day, one more known pair: with the
|
|
95
|
+
# real project hook set copied into a clone, an arm asked to build a new mechanism script FOUND
|
|
96
|
+
# PRIOR ART and never called Write (identity-④ behaviour), so the Write-matched PriorArt hook had
|
|
97
|
+
# nothing to fire on; an arm forced to Write a new scripts/*.sh produced `.prior_art_events.tsv`
|
|
98
|
+
# with a FIRE row inside the clone. The stimulus never reached the hook — a hook that is never
|
|
99
|
+
# triggered looks identical to a hook that cannot run, which is why a control arm whose hook is
|
|
100
|
+
# KNOWN to fire is mandatory. Signal: tracks/_meta/fh_signal_2026-09-03_runner-hooks-do-fire.md
|
|
101
|
+
# What SURVIVES from the old paragraph: before claiming a hook-dependent result, check the
|
|
102
|
+
# hook's own evidence file INSIDE the clone; absence of that file invalidates the ARM, never the
|
|
103
|
+
# hypothesis. And `--setup` writing an UNTRACKED file (.claude/settings.json is gitignored) is
|
|
104
|
+
# not contamination — the tree baseline is taken after setup, and git status never sees it.
|
|
79
105
|
#
|
|
80
106
|
# 🟥 CONTROL IS NOT OPTIONAL. Always run at least one arm whose correct answer is "the thing
|
|
81
107
|
# being measured should NOT fire". An instrument that fires on everything measures nothing
|
|
82
108
|
# ([[feedback_control_presence_is_not_discrimination]]).
|
|
109
|
+
#
|
|
110
|
+
# ── ⓒ 격리 그라운딩 — 날짜 오염 통제 (six_axis_review_2026-09-04 강화 #3, RECORD ONLY) ──────────
|
|
111
|
+
# WHY: LiveCodeBench (arXiv 2403.07974) and the contamination-taxonomy literature
|
|
112
|
+
# (frontier_verification_map_2026-09-04.md §ⓒ) filter benchmark items by RELEASE DATE precisely
|
|
113
|
+
# because a model can have memorized — not reasoned about — anything published before its
|
|
114
|
+
# training cutoff. This runner has the same exposure: a sim arm's "correct" answer about a
|
|
115
|
+
# doc/rule/gate can be recall of that doc's OWN CONTENT (if the doc predates the model's cutoff
|
|
116
|
+
# and the model happened to see this repo, or a near-identical one, during training) rather than
|
|
117
|
+
# evidence the harness under test actually delivers the behaviour to a cold session.
|
|
118
|
+
# THIS IS A RECORDING, NOT A GATE — per CLAUDE.md §Mechanization Boundary, "what the right value
|
|
119
|
+
# IS" stays judgment; only "was it recorded" is a channel. Below writes two fields per rep to
|
|
120
|
+
# `<out>/<arm>_r<rep>.meta.tsv` and does NOT compare them or fail a run on their basis:
|
|
121
|
+
# corpus_head_date — the clone's HEAD commit date (ISO 8601). PROXY, named honestly: this
|
|
122
|
+
# runner cannot resolve which files a given PROMPT actually cites, so it
|
|
123
|
+
# records the whole corpus's most recent commit as an upper bound on
|
|
124
|
+
# "how fresh could the cited material be", not a per-file citation date.
|
|
125
|
+
# sim_model / sim_model_cutoff — the `--model` value and a best-effort known cutoff via
|
|
126
|
+
# `_model_known_cutoff()` below, `UNKNOWN` when not recorded there. This
|
|
127
|
+
# table WILL go stale as new models ship — it is deliberately not treated
|
|
128
|
+
# as authoritative; a scoring session should verify the cutoff it cites
|
|
129
|
+
# rather than trust this file's guess (§Instrument-Calibration).
|
|
130
|
+
#
|
|
131
|
+
# ── ⓒ 격리 그라운딩 — 일회용 샌드박스 표준 (six_axis_review_2026-09-04 강화 #3) ─────────────────
|
|
132
|
+
# 프런티어 대조표가 "ephemeral wipe-and-reseed 샌드박스(E2B/Modal/Runloop)"를 도입 후보로 든다.
|
|
133
|
+
# 조직 제약상 외부 도구는 보류하고(§도입 후보 3), 이 러너가 "이미 그렇게 동작하는 부분"과
|
|
134
|
+
# "아닌 부분"을 정직하게 갈라 적는다 — 매번 재발견하지 않도록.
|
|
135
|
+
#
|
|
136
|
+
# WIPE (매 rep 마다 새로 만든다 — 재사용 없음):
|
|
137
|
+
# ✅ 이미 참 — 매 rep 이 독립된 `git clone` (§위 "A disposable clone per REP") + 독립된 부모
|
|
138
|
+
# 디렉터리(§L10). 오늘 실행한 실측 (`test_sim_isolated_run_lanes.sh`):
|
|
139
|
+
# ✅ L10 no clone shares a parent with another (2 clones, 0 co-parented)
|
|
140
|
+
# ✅ L10b control — 2 clones found (L10 did not pass on an empty set)
|
|
141
|
+
# (재현: `bash scripts/test_sim_isolated_run_lanes.sh` — 위 두 줄이 그대로 나온다.)
|
|
142
|
+
# RESEED (팔이 못 보면 안 되는 자산을 클론에서 지운다):
|
|
143
|
+
# ✅ 이미 참 — ARM_BLIND_PATHS 제거(§아래) + 그 자체가 known-pair 로 검정됨:
|
|
144
|
+
# ✅ L24 팔 눈가림 자산이 클론에서 제거된다 (히트 >0 → 0)
|
|
145
|
+
# 기계-표면 리셋 (LaunchAgents/crontab/settings — VM 리셋과 달리 "지우고 다시 만들지" 않는다):
|
|
146
|
+
# 🟥 아직 부분 — observe 모드는 도구 자체가 없어 못 건드리고(구조적 예방), act 모드는 실제
|
|
147
|
+
# 변경을 만들고 나서 **탐지**만 한다(§L7). "매 rep 전 기계 상태를 초기화"는 하지 않는다 —
|
|
148
|
+
# E2B/Modal 급 VM wipe-and-reseed와의 실제 델타는 이 한 줄이다. 진짜 OS 샌드박스가 필요한
|
|
149
|
+
# 이유는 헤더 상단 "WHAT THIS GIVES YOU" 절이 이미 말한다 — 여기서 되풀이하지 않는다.
|
|
150
|
+
# 요약: 파일시스템 코퍼스 축(WIPE+RESEED)은 클론 단위로 이미 wipe-and-reseed다. 기계 표면 축은
|
|
151
|
+
# 아니다 — 탐지기이지 리셋기가 아니다. 이 두 문장이 "샌드박스 표준"의 정직한 전부다.
|
|
152
|
+
#
|
|
153
|
+
# ── timeout(1) RESOLUTION — macOS ships none, and launchd's PATH cannot see Homebrew's
|
|
154
|
+
# (2026-09-05, measured) ──────────────────────────────────────────────────────────────────────
|
|
155
|
+
# WHY: stock macOS ships no `timeout(1)`. Homebrew coreutils installs `gtimeout` (plain
|
|
156
|
+
# `timeout` only if the user un-prefixes GNU coreutils onto PATH) under a prefix an interactive
|
|
157
|
+
# login shell sees but a launchd job's PATH does NOT — this repo's own plist templates ship
|
|
158
|
+
# `$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin`, no Homebrew prefix at all. The first launchd
|
|
159
|
+
# live-eval run (2026-09-05 02:30) hit exactly this: every one of 12 probes × 2 arms died at
|
|
160
|
+
# `timeout: command not found` before `claude` ever ran, and the report showed 12/12
|
|
161
|
+
# FAILED-TO-RUN with no clue why — a "did this fire" question this repo cares about a great
|
|
162
|
+
# deal (§Skeleton-Not-Muscle) answered wrong for a reason that had nothing to do with the
|
|
163
|
+
# harness under test. FH ships this script via npm (package.json `files[]`), so a stock-macOS
|
|
164
|
+
# consumer with no Homebrew coreutils hits the identical wall running it by hand — this is not
|
|
165
|
+
# a launchd-only defect. Resolution below: GNU `timeout` → `gtimeout` → a bash-native watchdog
|
|
166
|
+
# fallback, so a run never silently trades "no timeout enforcement" for "works on my machine".
|
|
167
|
+
# The resolved kind is printed in the run header as `timeout_tool=` so a run names its own
|
|
168
|
+
# control rather than leaving it to be inferred from a failure days later.
|
|
83
169
|
|
|
84
170
|
set -uo pipefail
|
|
85
171
|
|
|
@@ -242,6 +328,7 @@ while [ $# -gt 0 ]; do
|
|
|
242
328
|
--base-sha) BASE_SHA="${2:-}"; shift 2 ;;
|
|
243
329
|
--setup) SETUP="${2:-}"; shift 2 ;; # shell run INSIDE each clone before the sim. See below.
|
|
244
330
|
--extra-tools) EXTRA="${2:-}"; shift 2 ;; # append tools to the mode's set. See TOOL VISIBILITY.
|
|
331
|
+
--mcp-config) MCPCFG="${2:-}"; shift 2 ;; # the ONLY MCP servers an arm may see (see MCP ISOLATION). Default: none.
|
|
245
332
|
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
246
333
|
esac
|
|
247
334
|
done
|
|
@@ -251,6 +338,62 @@ case "$MODE" in observe|act) ;; *) echo "FAIL: --mode must be observe|act" >&2;
|
|
|
251
338
|
|
|
252
339
|
command -v claude >/dev/null 2>&1 || { echo "FAIL: claude CLI not on PATH" >&2; exit 2; }
|
|
253
340
|
|
|
341
|
+
# ── timeout(1) resolution — see §timeout(1) RESOLUTION in the header above for WHY. ──────────────
|
|
342
|
+
# GNU `timeout` → Homebrew's `gtimeout` → a bash-native watchdog. Never silently proceeds with NO
|
|
343
|
+
# enforcement at all — that would trade "wrong binary name" for "no timeout, ever", which is worse.
|
|
344
|
+
TIMEOUT_KIND=""; TIMEOUT_BIN=""
|
|
345
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
346
|
+
TIMEOUT_KIND="gnu"; TIMEOUT_BIN="$(command -v timeout)"
|
|
347
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
348
|
+
TIMEOUT_KIND="gtimeout"; TIMEOUT_BIN="$(command -v gtimeout)"
|
|
349
|
+
else
|
|
350
|
+
TIMEOUT_KIND="bash-fallback"; TIMEOUT_BIN=""
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# fh_run_with_timeout SECONDS CMD... — used only when neither `timeout` nor `gtimeout` resolved.
|
|
354
|
+
# Backgrounds CMD, TERMs it if SECONDS elapses, grants 5s to exit cleanly, then KILLs. The verdict
|
|
355
|
+
# "did this actually time out" is a FLAG FILE the watchdog writes when it intervenes — not CMD's
|
|
356
|
+
# own exit code — because a child that catches SIGTERM and exits 0 anyway must still be reported
|
|
357
|
+
# as timed out (matching GNU timeout's rc=124 convention), not read as if it had answered normally.
|
|
358
|
+
fh_run_with_timeout() {
|
|
359
|
+
local _secs="$1"; shift
|
|
360
|
+
local _flag; _flag="$(mktemp "${TMPDIR:-/tmp}/fh_to_XXXXXX")"; rm -f "$_flag"
|
|
361
|
+
"$@" &
|
|
362
|
+
local _cpid=$!
|
|
363
|
+
( sleep "$_secs" 2>/dev/null
|
|
364
|
+
if kill -0 "$_cpid" 2>/dev/null; then
|
|
365
|
+
: > "$_flag"
|
|
366
|
+
kill -TERM "$_cpid" 2>/dev/null
|
|
367
|
+
sleep 5
|
|
368
|
+
kill -0 "$_cpid" 2>/dev/null && kill -KILL "$_cpid" 2>/dev/null
|
|
369
|
+
fi
|
|
370
|
+
) &
|
|
371
|
+
local _watchdog=$!
|
|
372
|
+
local _rc=0
|
|
373
|
+
wait "$_cpid" 2>/dev/null; _rc=$?
|
|
374
|
+
# The watchdog subshell owns a `sleep`; killing the subshell alone leaves that sleep as an
|
|
375
|
+
# orphan for up to SECONDS (test_frontier_digest_retry.sh challenger B-2 measured exactly this
|
|
376
|
+
# class). Kill the subshell's children first, then the subshell.
|
|
377
|
+
pkill -P "$_watchdog" 2>/dev/null; kill "$_watchdog" 2>/dev/null; wait "$_watchdog" 2>/dev/null
|
|
378
|
+
if [ -f "$_flag" ]; then
|
|
379
|
+
rm -f "$_flag"
|
|
380
|
+
return 124
|
|
381
|
+
fi
|
|
382
|
+
rm -f "$_flag" 2>/dev/null
|
|
383
|
+
return "$_rc"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# fh_timeout SECONDS CMD... — drop-in for `timeout SECONDS CMD...` using whichever kind resolved
|
|
387
|
+
# above. The call site never branches on TIMEOUT_KIND itself.
|
|
388
|
+
fh_timeout() {
|
|
389
|
+
local _secs="$1"; shift
|
|
390
|
+
case "$TIMEOUT_KIND" in
|
|
391
|
+
gnu) command timeout "$_secs" "$@" ;;
|
|
392
|
+
gtimeout) command gtimeout "$_secs" "$@" ;;
|
|
393
|
+
*) fh_run_with_timeout "$_secs" "$@" ;;
|
|
394
|
+
esac
|
|
395
|
+
}
|
|
396
|
+
|
|
254
397
|
OUTDIR="${OUTDIR:-$(mktemp -d "${TMPDIR:-/tmp}/fh-sim-XXXXXX")}"
|
|
255
398
|
mkdir -p "$OUTDIR"
|
|
256
399
|
|
|
@@ -274,8 +417,19 @@ snapshot() {
|
|
|
274
417
|
} > "$f"
|
|
275
418
|
}
|
|
276
419
|
|
|
420
|
+
# ⓒ 날짜 오염 통제 — 기록 전용 헬퍼(§헤더 참조). 판정 아님, 있으면 값·없으면 UNKNOWN.
|
|
421
|
+
# 🟥 이 표는 부패한다(stale) — 새 모델이 나올 때마다 손으로 갱신해야 한다. 여기 적힌 값을
|
|
422
|
+
# «검증됨»으로 인용하지 마라: 이 스크립트 자신이 그렇게 적어두라고 요구한다(§Instrument-Calibration).
|
|
423
|
+
_model_known_cutoff() { # $1=--model 값 → ISO 월 또는 UNKNOWN
|
|
424
|
+
case "$1" in
|
|
425
|
+
*sonnet-5*|sonnet) printf '2026-01' ;; # 이 세션의 시스템 프롬프트가 스스로 적은 값
|
|
426
|
+
*) printf 'UNKNOWN' ;;
|
|
427
|
+
esac
|
|
428
|
+
}
|
|
429
|
+
|
|
277
430
|
echo "── sim_isolated_run ──────────────────────────────────────────────"
|
|
278
431
|
echo "arm=$ARM mode=$MODE model=$MODEL reps=$REPS timeout=${TIMEOUT}s"
|
|
432
|
+
echo "timeout_tool=${TIMEOUT_KIND}${TIMEOUT_BIN:+:$TIMEOUT_BIN}"
|
|
279
433
|
echo "out=$OUTDIR"
|
|
280
434
|
|
|
281
435
|
snapshot "$OUTDIR/_machine_before.txt"
|
|
@@ -318,6 +472,17 @@ for r in $(seq 1 "$REPS"); do
|
|
|
318
472
|
continue
|
|
319
473
|
fi
|
|
320
474
|
|
|
475
|
+
# ⓒ 날짜 오염 통제 — 기록만, 판정 아님(§헤더). corpus_head_date 는 "프롬프트가 인용하는
|
|
476
|
+
# 파일들"의 날짜가 아니라 그 상한(코퍼스 전체 HEAD)이다 — 러너는 프롬프트가 무엇을 인용하는지
|
|
477
|
+
# 모르므로 더 좁게 잴 수 없다. 그렇게 좁혀 적는다.
|
|
478
|
+
_corpus_date=$(git -C "$WORK" log -1 --format=%cI 2>/dev/null); [ -n "$_corpus_date" ] || _corpus_date="UNKNOWN"
|
|
479
|
+
_cutoff=$(_model_known_cutoff "$MODEL")
|
|
480
|
+
{
|
|
481
|
+
printf 'corpus_head_date\t%s\n' "$_corpus_date"
|
|
482
|
+
printf 'sim_model\t%s\n' "$MODEL"
|
|
483
|
+
printf 'sim_model_cutoff\t%s\n' "$_cutoff"
|
|
484
|
+
} > "$OUTDIR/${ARM}_r${r}.meta.tsv"
|
|
485
|
+
|
|
321
486
|
# ── 🟥 팔이 읽으면 안 되는 tracked 자산을 «클론 안에서» 제거한다 (2026-09-01) ──────
|
|
322
487
|
# 왜: 얼린 정답지(`scripts/fixtures/knownpair_refusal_48_*`)가 tracked 가 되면서
|
|
323
488
|
# **모든 팔의 클론에 들어갔다.** 실측 — negative 문항의 핵심 명사구가 그 안에 있다
|
|
@@ -345,6 +510,19 @@ for r in $(seq 1 "$REPS"); do
|
|
|
345
510
|
# compaction_probe.sh 7 중 1 · starter.md 22 중 1 — 여유 충분
|
|
346
511
|
ARM_BLIND_PATHS=( "scripts/fixtures" "scripts/round" "scripts/context_continuity_score.sh" )
|
|
347
512
|
for _bp in "${ARM_BLIND_PATHS[@]}"; do
|
|
513
|
+
# 🟥 STATUS 오염 차단 (2026-09-06) — `rm` 만 하면 클론의 `git status` 가 `D` 160줄이 되고,
|
|
514
|
+
# 그 목록이 세션 시작 컨텍스트로 **팔에** 들어간다. 실측: 기록된 응답 76개 중 11개가
|
|
515
|
+
# 삭제된 파일을 «이름으로» 인용했고 **11건 전부 primary · 컨트롤 0** — 오염이 한쪽 팔에만
|
|
516
|
+
# 걸려 «없는 차이»를 만드는 방향이었다([[feedback_instrument_blindspot_correlated_with_arm]]).
|
|
517
|
+
# skip-worktree 로 인덱스가 삭제를 안 보게 한 뒤 지운다. 파일은 여전히 실제로 부재이므로
|
|
518
|
+
# 헤더의 «없는 파일은 어느 도구로도 못 읽는다» 보장은 그대로다(아래 [ -e ] 판정이 그것을 잰다).
|
|
519
|
+
# ⚠️ 남는 잔여: `git ls-files` 는 이름을 계속 나열한다(인덱스 메타데이터). observe 모드엔
|
|
520
|
+
# Bash 가 없어 도달 불가, **act 모드엔 도달 가능** — 닫힌 게 아니라 좁아진 것이다.
|
|
521
|
+
_bp_n=$(git -C "$WORK" ls-files -- "$_bp" 2>/dev/null | wc -l | tr -d ' ')
|
|
522
|
+
if [ "${_bp_n:-0}" -gt 0 ]; then
|
|
523
|
+
git -C "$WORK" ls-files -z -- "$_bp" 2>/dev/null \
|
|
524
|
+
| xargs -0 git -C "$WORK" update-index --skip-worktree -- 2>/dev/null || true
|
|
525
|
+
fi
|
|
348
526
|
rm -rf "$WORK/$_bp"
|
|
349
527
|
# 🟥 rm 의 rc 를 안 믿는다 — 경로 오타면 rm 은 «성공»을 낸다(지울 게 없으니).
|
|
350
528
|
# 판정은 «제거 후에 정말 없는가»로 한다.
|
|
@@ -424,8 +602,16 @@ for r in $(seq 1 "$REPS"); do
|
|
|
424
602
|
# 부르며 정직하게 거부하면서도, 거부문 안에서 정답을 말한다. 채점기는 그걸 토큰으로 센다.
|
|
425
603
|
# 이것이 회차 1~3 과 probe1~4 를 전부 무효로 만든 근인이고, 경로 deny·코퍼스 마스킹은
|
|
426
604
|
# **원리적으로 못 막는다**(도구 읽기가 아니라 프롬프트 조립이다).
|
|
427
|
-
(
|
|
428
|
-
|
|
605
|
+
# 🟥 MCP ISOLATION (measured 2026-09-05, QP chamber floor sim): a headless arm launched by this
|
|
606
|
+
# runner saw the OPERATOR'S user-scope MCP servers (a cloud drive, a wiki, a browser bridge)
|
|
607
|
+
# in its tool list — the clone isolates files and settings, not `~/.claude.json` MCP scope.
|
|
608
|
+
# A blind arm with a write-capable MCP is a residency hole, not a sim. `--strict-mcp-config`
|
|
609
|
+
# = "only the servers given by --mcp-config" (CLI --help), and with no --mcp-config that is
|
|
610
|
+
# NONE. An arm that legitimately needs a server (e.g. Playwright for a web target) gets it
|
|
611
|
+
# explicitly via --mcp-config <file inside the clone>; nothing is inherited.
|
|
612
|
+
MCPARGS=(--strict-mcp-config); [ -n "${MCPCFG:-}" ] && MCPARGS+=(--mcp-config "$MCPCFG") # bash 3.2 + set -u: expanded with the ${a[@]+"${a[@]}"} idiom below
|
|
613
|
+
( cd "$WORK" && fh_timeout "$TIMEOUT" claude -p "$PROMPT" \
|
|
614
|
+
--model "$MODEL" "${TOOLS[@]}" ${MCPARGS[@]+"${MCPARGS[@]}"} \
|
|
429
615
|
< /dev/null 2>"$OUTDIR/${ARM}_r${r}.stderr.txt" ) > "$OUTDIR/${ARM}_r${r}.txt"
|
|
430
616
|
rc=$?
|
|
431
617
|
bytes=$(wc -c < "$OUTDIR/${ARM}_r${r}.txt" | tr -d ' ')
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_action_yml_lanes.sh — behavioural lanes for action.yml's exit-code mapping.
|
|
3
|
+
#
|
|
4
|
+
# WHY: `action.yml` is the only place where the gate's SEVEN typed exit codes get turned into a
|
|
5
|
+
# GitHub step outcome. That translation is exactly where a typed verdict silently becomes a
|
|
6
|
+
# boolean — the failure this repo names in `[[feedback_not_found_is_not_zero_family]]`. Two
|
|
7
|
+
# properties carry the weight and neither is visible by reading the YAML:
|
|
8
|
+
# A. an UNKNOWN exit code (a future gate version adding one) must land on HARNESS_ERROR-class
|
|
9
|
+
# handling, never on PASS. A `case` whose `*)` arm is missing would default to... nothing,
|
|
10
|
+
# and `verdict` would be unset — which under `set -u` is a crash, but under a careless edit
|
|
11
|
+
# could become an empty string that matches no fail-on entry and exits 0. That is the leak.
|
|
12
|
+
# B. `reviewed` must be false for every code where no review ran (10 · 11 · 12 · unknown).
|
|
13
|
+
# «did not run» reported as «passed» is the same defect class as a skipped check scored green.
|
|
14
|
+
#
|
|
15
|
+
# HOW: the mapping is extracted from action.yml and executed as shell — the lanes run the REAL
|
|
16
|
+
# case block, not a copy. A copy would drift and every lane would stay green while the shipped
|
|
17
|
+
# file rotted (that is `[[feedback_built_but_not_wired]]` wearing a test's clothes).
|
|
18
|
+
#
|
|
19
|
+
# USAGE: bash scripts/test_action_yml_lanes.sh → exit 0 all pass · 1 any fail · 10 harness error
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
22
|
+
A="$ROOT/action.yml"
|
|
23
|
+
[ -f "$A" ] || { echo "❌ HARNESS: action.yml absent at $A"; exit 10; }
|
|
24
|
+
PASS=0; FAIL=0
|
|
25
|
+
ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
26
|
+
no(){ FAIL=$((FAIL+1)); printf ' ❌ %s\n' "$1"; }
|
|
27
|
+
chk(){ if [ "$1" = "0" ]; then ok "$2"; else no "$2"; fi; }
|
|
28
|
+
|
|
29
|
+
# ── extract the real case block from the shipped file ────────────────────────────────────────
|
|
30
|
+
MAP="$(awk '/^ *case "\$rc" in/{f=1} f{print} /^ *esac/{if(f){exit}}' "$A" | sed 's/^ *//')"
|
|
31
|
+
[ -n "$MAP" ] || { echo "❌ HARNESS: could not extract the case block from action.yml"; exit 10; }
|
|
32
|
+
printf '%s' "$MAP" | grep -q 'esac' || { echo "❌ HARNESS: extracted block has no esac (truncated)"; exit 10; }
|
|
33
|
+
|
|
34
|
+
verdict_for(){ # $1 = rc → prints "verdict reviewed"
|
|
35
|
+
rc="$1"; verdict=""; reviewed=""
|
|
36
|
+
eval "$MAP"
|
|
37
|
+
printf '%s %s' "${verdict:-<UNSET>}" "${reviewed:-<UNSET>}"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
echo "── L1 documented exit codes map to their documented verdict ──"
|
|
41
|
+
while read -r rc want_v want_r; do
|
|
42
|
+
got="$(verdict_for "$rc")"
|
|
43
|
+
[ "$got" = "$want_v $want_r" ]; chk $? "rc=$rc → $want_v (reviewed=$want_r) [got: $got]"
|
|
44
|
+
done <<'CASES'
|
|
45
|
+
0 PASS true
|
|
46
|
+
1 PENDING true
|
|
47
|
+
2 BLOCKED true
|
|
48
|
+
3 ESCALATE true
|
|
49
|
+
10 HARNESS_ERROR false
|
|
50
|
+
11 ARG_ERROR false
|
|
51
|
+
12 DRY_RUN false
|
|
52
|
+
CASES
|
|
53
|
+
|
|
54
|
+
echo "── L2 KNOWN-NEGATIVE: an undocumented exit code is never PASS and never reviewed=true ──"
|
|
55
|
+
for rc in 4 5 9 13 42 127 255; do
|
|
56
|
+
got="$(verdict_for "$rc")"; v="${got%% *}"; r="${got##* }"
|
|
57
|
+
{ [ "$v" != "PASS" ] && [ "$v" != "PENDING" ] && [ "$v" != "<UNSET>" ] && [ "$r" = "false" ]; }
|
|
58
|
+
chk $? "rc=$rc → $v (reviewed=$r) — not a pass, not unset"
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
echo "── L3 the mapping is TOTAL: no rc leaves verdict unset (the silent-green hole) ──"
|
|
62
|
+
_unset=0
|
|
63
|
+
for rc in $(seq 0 20) 42 100 127 255; do
|
|
64
|
+
got="$(verdict_for "$rc")"; case "$got" in "<UNSET>"*) _unset=$((_unset+1)) ;; esac
|
|
65
|
+
done
|
|
66
|
+
[ "$_unset" -eq 0 ]; chk $? "0 of 28 sampled codes leave verdict unset (found $_unset)"
|
|
67
|
+
|
|
68
|
+
echo "── L4 CONTROL: a mutated mapping without the catch-all IS caught (the lane can fail) ──"
|
|
69
|
+
_MUT="$(printf '%s' "$MAP" | grep -v '^\*)')"
|
|
70
|
+
verdict_mut(){ rc="$1"; verdict=""; reviewed=""; eval "$_MUT"; printf '%s' "${verdict:-<UNSET>}"; }
|
|
71
|
+
[ "$(verdict_mut 42)" = "<UNSET>" ]; chk $? "catch-all removed → rc=42 leaves verdict unset (control is alive)"
|
|
72
|
+
[ "$(verdict_mut 0)" = "PASS" ]; chk $? "…and the mutant still maps documented codes (mutation is surgical)"
|
|
73
|
+
|
|
74
|
+
echo "── L5 action.yml's documented codes match scripts/fh-gate.sh's exit contract ──"
|
|
75
|
+
G="$ROOT/scripts/fh-gate.sh"
|
|
76
|
+
if [ -f "$G" ]; then
|
|
77
|
+
_gate_codes="$(grep -oE '^# [0-9]+ +—' "$G" | grep -oE '[0-9]+' | sort -un | tr '\n' ' ')"
|
|
78
|
+
_act_codes="$(printf '%s' "$MAP" | grep -oE '^[0-9]+\)' | grep -oE '[0-9]+' | sort -un | tr '\n' ' ')"
|
|
79
|
+
[ -n "$_gate_codes" ]; chk $? "CONTROL: the gate's exit contract was actually parsed (got: $_gate_codes)"
|
|
80
|
+
[ "$_gate_codes" = "$_act_codes" ]; chk $? "every documented gate code has an action arm [gate: $_gate_codes | action: $_act_codes]"
|
|
81
|
+
else
|
|
82
|
+
echo " ⬜ L5 SKIPPED (not PASS) — scripts/fh-gate.sh absent, contract un-cross-checked"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo "── L6 fail-on default is fail-closed: every non-reviewed verdict is in it ──"
|
|
86
|
+
_failon="$(grep -A3 "^ fail-on:" "$A" | grep "default:" | sed "s/.*default: *'//; s/'.*//")"
|
|
87
|
+
[ -n "$_failon" ]; chk $? "CONTROL: fail-on default parsed (got: $_failon)"
|
|
88
|
+
for v in BLOCKED ESCALATE HARNESS_ERROR ARG_ERROR DRY_RUN UNKNOWN; do
|
|
89
|
+
case ",$_failon," in *",$v,"*) ok "fail-on default contains $v" ;; *) no "fail-on default is MISSING $v — that verdict would exit 0" ;; esac
|
|
90
|
+
done
|
|
91
|
+
for v in PASS PENDING; do
|
|
92
|
+
case ",$_failon," in *",$v,"*) no "fail-on default contains $v (over-blocks a reviewed pass)" ;; *) ok "fail-on default correctly omits $v" ;; esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
echo ""
|
|
96
|
+
echo "── action.yml lanes: $PASS passed · $FAIL failed ──"
|
|
97
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_backtick_guard_lanes.sh — known pairs for scripts/backtick_guard.sh. Written BEFORE the detector.
|
|
3
|
+
#
|
|
4
|
+
# WHAT IS BEING GUARDED
|
|
5
|
+
# A backtick inside a shell DOUBLE-QUOTING CONTEXT — an unquoted heredoc body (`<<EOF`) or a
|
|
6
|
+
# "double-quoted string" — is command substitution: the text between the backticks is REPLACED by
|
|
7
|
+
# the command's output. With no such command the output is empty, so the text is DELETED; with one,
|
|
8
|
+
# foreign content is INSERTED. The sentence stays grammatical (only its subject is gone), the only
|
|
9
|
+
# signal is one `command not found` line at the top of the output, and every marker/record hook
|
|
10
|
+
# checks a field's presence, not its completeness.
|
|
11
|
+
# Measured 7× (2026-08-10 · 2026-09-01 ×3 · 2026-09-02 ×4 — marker, RESULT doc, fh_completed echo ×2)
|
|
12
|
+
# with a resident memory rule that failed each time because the actor's task had a different NAME
|
|
13
|
+
# (writing a marker · a failure message · a seal). N≥3 → mechanize (weekly_audit_2026-09-02 HIGH #1).
|
|
14
|
+
#
|
|
15
|
+
# BT1 — unquoted heredoc body (`<<TAG`, `<<-TAG`; NOT `<<'TAG'` / `<<"TAG"` / `<<\TAG`) containing `
|
|
16
|
+
# BT2 — double-quoted string containing ` (single-quoted text and `\`` are literal → CLEAN)
|
|
17
|
+
#
|
|
18
|
+
# Surface = the Bash tool call itself (interactively-composed commands), same reasoning as
|
|
19
|
+
# pipe_verdict_guard: every recurrence was in a composed command, none in a shipped file.
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
G="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/backtick_guard.sh"
|
|
23
|
+
pass=0; fail=0
|
|
24
|
+
|
|
25
|
+
# expect <label> <HIT|CLEAN> <command-string> (the command text is passed raw via --stdin-raw)
|
|
26
|
+
expect() {
|
|
27
|
+
local label="$1" want="$2" cmd="$3" out got
|
|
28
|
+
out=$(printf '%s' "$cmd" | bash "$G" --stdin-raw 2>&1)
|
|
29
|
+
# want may be HIT · CLEAN · HIT:BT1@2 (rule and line asserted — a hit on the WRONG line is a miss)
|
|
30
|
+
if printf '%s' "$out" | grep -q 'BACKTICK'; then got=HIT; else got=CLEAN; fi
|
|
31
|
+
case "$want" in HIT:*) printf '%s' "$out" | grep -q "${want#HIT:BT}" 2>/dev/null; :; esac
|
|
32
|
+
if [ "$got" = HIT ] && [ "${want%%:*}" = HIT ] && [ "$want" != HIT ]; then
|
|
33
|
+
local r="${want#HIT:}"; r="${r%@*}"; local l="${want##*@}"
|
|
34
|
+
printf '%s' "$out" | grep -q "$r L$l " || got="HIT-WRONG($(printf '%s' "$out" | grep -oE 'BT[12] L[0-9]+' | head -1))"
|
|
35
|
+
[ "$got" = HIT ] && got="$want"
|
|
36
|
+
fi
|
|
37
|
+
if [ "$got" = "$want" ]; then
|
|
38
|
+
printf ' ✅ %-56s %s (expected %s)\n' "$label" "$got" "$want"; pass=$((pass+1))
|
|
39
|
+
else
|
|
40
|
+
printf ' ❌ %-56s %s (expected %s)\n' "$label" "$got" "$want"; fail=$((fail+1))
|
|
41
|
+
printf ' cmd: %s\n out: %s\n' "$cmd" "$out"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
BT='`' # one backtick, spelled once so the lanes below never carry a live one in double quotes
|
|
45
|
+
NL=$'\n'
|
|
46
|
+
|
|
47
|
+
echo "[backtick-guard] known pairs"
|
|
48
|
+
echo "-- BT1: unquoted heredoc body --"
|
|
49
|
+
# 2026-09-02 ⓐ — the measured shape: a marker written through an unquoted heredoc so a $VAR expands.
|
|
50
|
+
expect "BT1 measured: marker heredoc" HIT:BT1@3 "cat > m.marker <<EOF${NL}date: \$TODAY${NL}axis2-evidence: 러너가 ${BT}--keep-blind-paths${BT} 를 삼켰다${NL}EOF"
|
|
51
|
+
expect "BT1 <<-TAG (tab-stripped) form" HIT "cat <<-EOF${NL} note ${BT}x${BT}${NL} EOF"
|
|
52
|
+
expect "BT1 markdown fence in heredoc" HIT "cat > r.md <<EOF${NL}\`\`\`${NL}out${NL}\`\`\`${NL}EOF"
|
|
53
|
+
expect "BT1 second of two heredocs on a line" HIT:BT1@4 "diff <(cat <<'A') <(cat <<B)${NL}${BT}a${BT}${NL}A${NL}${BT}b${BT}${NL}B"
|
|
54
|
+
expect "BT1 quoted <<'EOF' is CLEAN" CLEAN "cat > m.marker <<'EOF'${NL}evidence: ${BT}--keep-blind-paths${BT} 삼킴${NL}EOF"
|
|
55
|
+
expect "BT1 quoted <<\"EOF\" is CLEAN" CLEAN "cat <<\"EOF\"${NL}${BT}x${BT}${NL}EOF"
|
|
56
|
+
expect "BT1 escaped <<\\EOF is CLEAN" CLEAN "cat <<\\EOF${NL}${BT}x${BT}${NL}EOF"
|
|
57
|
+
expect "BT1 backslash-escaped backtick CLEAN" CLEAN "cat <<EOF${NL}see \\${BT}x\\${BT}${NL}EOF"
|
|
58
|
+
expect "BT1 backtick AFTER the body ends" CLEAN "cat <<EOF${NL}plain${NL}EOF${NL}echo '${BT}later${BT}'"
|
|
59
|
+
expect "BT1 <<< herestring is not a heredoc" CLEAN "grep -c x <<< 'a ${BT}b${BT}'"
|
|
60
|
+
|
|
61
|
+
echo "-- BT2: double-quoted string --"
|
|
62
|
+
# 2026-09-02 ⓑ — the measured shape: a completion-log append through echo "…".
|
|
63
|
+
expect "BT2 measured: echo append" HIT:BT2@1 "echo \"- ✅ 러너 ${BT}sim_isolated_run.sh${BT} 헤더 경고\" >> tracks/_meta/fh_completed.md"
|
|
64
|
+
expect "BT2 failure-message string (09-01)" HIT "printf '%s\\n' \"(${BT}nameleak_check.sh gen${BT} 을 써라)\""
|
|
65
|
+
expect "BT2 single quote inside dq is inert" HIT "echo \"don't ${BT}x${BT}\""
|
|
66
|
+
expect "BT2 single-quoted is CLEAN" CLEAN "printf '%s\\n' '- ✅ 러너 ${BT}sim_isolated_run.sh${BT} 헤더' >> log.md"
|
|
67
|
+
expect "BT2 escaped \\\` is CLEAN" CLEAN "echo \"see \\${BT}x\\${BT}\""
|
|
68
|
+
expect "BT2 dq inside single quotes is CLEAN" CLEAN "echo '\"${BT}x${BT}\"'"
|
|
69
|
+
expect "BT2 sq inside \$( ) inside dq CLEAN" CLEAN "echo \"\$(printf '%s' '${BT}x${BT}')\""
|
|
70
|
+
expect "BT2 no backtick at all" CLEAN "echo \"\$(git log -1) done\" && cat <<EOF${NL}plain \$X${NL}EOF"
|
|
71
|
+
expect "BT2 bare backtick outside quotes" CLEAN "V=${BT}date${BT}; echo ok" # live command substitution on purpose, not a text context
|
|
72
|
+
|
|
73
|
+
echo "-- Axis-2 pass 2026-09-03 (challenger, repros executed by the governor) --"
|
|
74
|
+
# A1: the first build stripped single-quoted spans BEFORE matching heredoc operators, so <<'EOF' was
|
|
75
|
+
# never a heredoc — three symptoms from one cause. Each pinned in its real shape.
|
|
76
|
+
expect "A1a quoted body with dq+backtick CLEAN" CLEAN "cat <<'EOF'${NL}axis2-evidence: 메시지 \"use ${BT}x${BT}\" 가 떴다${NL}EOF"
|
|
77
|
+
expect "A1b quoted A then unquoted B: only B" CLEAN "diff <(cat <<'A') <(cat <<B)${NL}${BT}a${BT}${NL}A${NL}plain${NL}B"
|
|
78
|
+
expect "A1c apostrophe in quoted body, then echo" HIT:BT2@4 "cat <<'EOF'${NL}don't${NL}EOF${NL}echo \"${BT}x${BT}\" >> log"
|
|
79
|
+
# A2: a comment's apostrophe must not open a single-quote context that swallows the next line.
|
|
80
|
+
expect "A2 comment apostrophe then echo" HIT:BT2@2 "# don't re-run this${NL}echo \"${BT}x${BT}\" >> f.md"
|
|
81
|
+
expect "A2 url fragment is not a comment" HIT:BT2@1 "curl \"https://x/a#frag ${BT}x${BT}\""
|
|
82
|
+
# B3: <<TAG inside a double-quoted string (commit message) opens no heredoc.
|
|
83
|
+
expect "B3 <<EOF in commit message is CLEAN" CLEAN "git commit -m \"docs: prefer <<EOF for markers\"${NL}V=${BT}date${BT}; echo ok"
|
|
84
|
+
# B5: escaped backslash + LIVE backtick.
|
|
85
|
+
expect "B5 \\\\ then live backtick HITs" HIT:BT1@2 "cat <<EOF${NL}path\\\\${BT}x${BT}${NL}EOF"
|
|
86
|
+
# B2: ANSI-C $'…' with an escaped apostrophe does not end early.
|
|
87
|
+
expect "B2 \$'don\\'t' then dq backtick" HIT:BT2@1 "echo \$'don\\'t' \"${BT}x${BT}\""
|
|
88
|
+
# B1: backtick inside \$( ) re-entered from dq is live substitution — deliberately NOT flagged.
|
|
89
|
+
expect "B1 backtick inside \$( ) in dq CLEAN" CLEAN "echo \"\$(echo ${BT}x${BT})\""
|
|
90
|
+
|
|
91
|
+
echo "-- hook mode: JSON payload in → JSON out (A4: detection is worthless if delivery is 0) --"
|
|
92
|
+
jexp() { # <label> <expect-substring-in-additionalContext|SILENT> <env> <payload>
|
|
93
|
+
local label="$1" want="$2" env_="$3" payload="$4" out ctx
|
|
94
|
+
out=$(printf '%s' "$payload" | env $env_ bash "$G" 2>/dev/null)
|
|
95
|
+
if [ "$want" = SILENT ]; then
|
|
96
|
+
if [ -z "$out" ]; then printf ' ✅ %-56s SILENT\n' "$label"; pass=$((pass+1)); else printf ' ❌ %-56s expected SILENT, got: %s\n' "$label" "${out:0:80}"; fail=$((fail+1)); fi
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
ctx=$(printf '%s' "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["hookSpecificOutput"]["additionalContext"])' 2>/dev/null)
|
|
100
|
+
if printf '%s' "$ctx" | grep -q "$want"; then printf ' ✅ %-56s JSON additionalContext carries %s\n' "$label" "$want"; pass=$((pass+1))
|
|
101
|
+
else printf ' ❌ %-56s no JSON/context (%s)\n' "$label" "${out:0:80}"; fail=$((fail+1)); fi
|
|
102
|
+
}
|
|
103
|
+
P='{"tool_name":"Bash","tool_input":{"command":"echo \"- done `x.sh` ok\""}}'
|
|
104
|
+
jexp "JSON payload → additionalContext" "BT2 L1" "X=1" "$P"
|
|
105
|
+
jexp "ascii PYTHONIOENCODING still emits" "BT2 L1" "PYTHONIOENCODING=ascii" "$P"
|
|
106
|
+
jexp "non-Bash tool is SILENT" SILENT "X=1" '{"tool_name":"Write","tool_input":{"content":"`x`"}}'
|
|
107
|
+
jexp "unparseable payload is SILENT" SILENT "X=1" 'not json'
|
|
108
|
+
|
|
109
|
+
echo "-- opt-out / payload --"
|
|
110
|
+
expect "noqa exempts" CLEAN "echo \"${BT}x${BT}\" # noqa: backtick"
|
|
111
|
+
expect "empty payload" CLEAN ""
|
|
112
|
+
|
|
113
|
+
echo
|
|
114
|
+
echo "[backtick-guard] $pass passed, $fail failed"
|
|
115
|
+
[ "$fail" -eq 0 ]
|
|
@@ -81,7 +81,7 @@ EOF
|
|
|
81
81
|
cat > "$TMP/non_detections.sh" <<'EOF'
|
|
82
82
|
#!/usr/bin/env bash
|
|
83
83
|
# (a) integer sanitization — the PRESCRIBED remedy for the pipefail-fallback class, not the defect.
|
|
84
|
-
count=$(grep -c pattern file)
|
|
84
|
+
count=$(grep -c pattern file) # portability-noqa: fixture text written by a quoted heredoc, never executed — statically grepped by degrade_direction_scan.sh only
|
|
85
85
|
if [ "${count:-0}" -gt 0 ]; then echo "found"; fi
|
|
86
86
|
# (b) SCOPE guards — "this run does not apply here" is not a claim that a check passed.
|
|
87
87
|
[ -d "$HOME/projects" ] || exit 0
|
|
@@ -165,11 +165,11 @@ N=$(find /nope . -maxdepth 1 2>/dev/null | grep -c . || echo 0)
|
|
|
165
165
|
M=$(git log --oneline 2>/dev/null | wc -l || echo 0)
|
|
166
166
|
# WIDENED 2026-08-04. Every line below was INVISIBLE to the narrowed rule, and each was verified to
|
|
167
167
|
# actually produce "0\n0" before being pinned here (line count measured, not assumed):
|
|
168
|
-
P=$(cat /etc/hosts | grep -c . | tr -d ' ' || echo 0) # transparent filter after the counter
|
|
169
|
-
Q=$(grep -c "^nosuchline$" /etc/hosts 2>/dev/null | tr -d ' ' || echo 0) # the PR #251 shape
|
|
170
|
-
R=$(grep -Ec "^nosuchline$" /etc/hosts || echo 0) # combined flag cluster -Ec
|
|
171
|
-
S=$(grep --count "^nosuchline$" /etc/hosts || echo 0) # long option
|
|
172
|
-
T=$(grep -Fcx "nosuchline" /etc/hosts || echo 0) # -Fcx
|
|
168
|
+
P=$(cat /etc/hosts | grep -c . | tr -d ' ' || echo 0) # transparent filter after the counter # portability-noqa: fixture text, never executed
|
|
169
|
+
Q=$(grep -c "^nosuchline$" /etc/hosts 2>/dev/null | tr -d ' ' || echo 0) # the PR #251 shape # portability-noqa: fixture text, never executed
|
|
170
|
+
R=$(grep -Ec "^nosuchline$" /etc/hosts || echo 0) # combined flag cluster -Ec # portability-noqa: fixture text, never executed
|
|
171
|
+
S=$(grep --count "^nosuchline$" /etc/hosts || echo 0) # long option # portability-noqa: fixture text, never executed
|
|
172
|
+
T=$(grep -Fcx "nosuchline" /etc/hosts || echo 0) # -Fcx # portability-noqa: fixture text, never executed
|
|
173
173
|
U=$(false | grep -c . | cat || echo 0) # trailing stage that always emits
|
|
174
174
|
V=$(false | grep -c . | grep -v nosuch || echo 0) # trailing grep whose pattern misses the "0"
|
|
175
175
|
EOF
|
|
@@ -688,7 +688,7 @@ fi
|
|
|
688
688
|
# flips to a hit — proves the exclusion is load-bearing for this specific fixture, not just
|
|
689
689
|
# present somewhere in the file.
|
|
690
690
|
_scan_reverted="$TMP/scan_reverted.sh"
|
|
691
|
-
_excl_line=$(grep -n "grep -vE 'in \[A-Z_\]" "$SCAN" | head -1 | cut -d: -f1)
|
|
691
|
+
_excl_line=$(grep -n "grep -vE 'in \[A-Z_\]" "$SCAN" | head -1 | cut -d: -f1) # portability-noqa: this file has no `set -e` (top line 22 is `set -uo pipefail` only) and the next line already guards empty via -z
|
|
692
692
|
if [ -z "$_excl_line" ]; then
|
|
693
693
|
bad "C2-REVERT could not locate the exclusion line by its known text — fixture cannot run"
|
|
694
694
|
else
|