@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.
Files changed (134) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +88 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +13 -3
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +43 -6
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +35 -1
  21. package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
  22. package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
  23. package/package.json +48 -2
  24. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  25. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  26. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  27. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  30. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  31. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  32. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  33. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  34. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  35. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  36. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  37. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  38. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  39. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  40. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  41. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  42. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  43. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  44. package/plugins/fh-meta/CHANGELOG.md +67 -0
  45. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  47. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  49. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  50. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  51. package/plugins/fh-qp/README.md +71 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  53. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  54. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  56. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  59. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  60. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  61. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  62. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  63. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  64. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  65. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  66. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  67. package/scripts/backtick_guard.sh +194 -0
  68. package/scripts/chamber_run.sh +14 -5
  69. package/scripts/com.forge-harness.live-eval.plist +84 -0
  70. package/scripts/compaction_probe.sh +9 -35
  71. package/scripts/context_continuity_score.sh +49 -7
  72. package/scripts/directional_diff_gate.sh +14 -2
  73. package/scripts/fh-gate.sh +3 -3
  74. package/scripts/files_manifest_shipping_check.sh +19 -0
  75. package/scripts/frontier_digest_autopilot.sh +4 -1
  76. package/scripts/gate_pathspec_check.sh +1 -1
  77. package/scripts/map_postprocess.py +90 -0
  78. package/scripts/outbound_query_guard.sh +131 -0
  79. package/scripts/outbound_query_hook.sh +373 -0
  80. package/scripts/package_coverage_check.sh +79 -18
  81. package/scripts/pipe_verdict_guard.sh +41 -1
  82. package/scripts/probe_live_eval.sh +240 -0
  83. package/scripts/probe_live_eval_lib.py +579 -0
  84. package/scripts/proposal_hook.sh +192 -0
  85. package/scripts/public_surface_scan_files.sh +11 -2
  86. package/scripts/push_zone_check.sh +78 -0
  87. package/scripts/residency_closure_scan.py +252 -0
  88. package/scripts/revert_probe.sh +250 -0
  89. package/scripts/selfcheck.sh +106 -3
  90. package/scripts/session_close_check.sh +100 -0
  91. package/scripts/sim_isolated_run.sh +195 -9
  92. package/scripts/test_action_yml_lanes.sh +97 -0
  93. package/scripts/test_backtick_guard_lanes.sh +115 -0
  94. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  95. package/scripts/test_fh_qp_lanes.sh +105 -0
  96. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  97. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  98. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  99. package/scripts/test_lane_runner_lanes.sh +59 -33
  100. package/scripts/test_map_postprocess_lanes.sh +143 -0
  101. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  102. package/scripts/test_marker_affected_lanes.sh +93 -0
  103. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  104. package/scripts/test_marker_oracle_lanes.sh +136 -0
  105. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  106. package/scripts/test_node_check_lanes.sh +34 -34
  107. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  108. package/scripts/test_outbound_query_lanes.sh +87 -0
  109. package/scripts/test_package_coverage_lanes.sh +53 -27
  110. package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
  111. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  112. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  113. package/scripts/test_preprep_drift_anchor.sh +16 -7
  114. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  115. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  116. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  117. package/scripts/test_proposal_hook_lanes.sh +57 -0
  118. package/scripts/test_push_zone_lanes.sh +304 -0
  119. package/scripts/test_residency_closure_lanes.sh +70 -0
  120. package/scripts/test_revert_probe_lanes.sh +146 -0
  121. package/scripts/test_session_close_lanes.sh +3 -5
  122. package/scripts/test_sim_isolated_run_lanes.sh +136 -0
  123. package/scripts/test_utterance_intake_lanes.sh +414 -0
  124. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  125. package/scripts/transcript_utterances.py +222 -0
  126. package/scripts/utterance_intake.sh +424 -0
  127. package/scripts/utterance_landing_check.sh +2 -2
  128. package/scripts/validate_yaml.sh +27 -0
  129. package/scripts/worktree_reclaim.sh +95 -0
  130. package/templates/.git-hooks/pre-commit +380 -5
  131. package/templates/.git-hooks/pre-push +91 -0
  132. package/templates/RED_TEAM_REPORT.md +49 -0
  133. package/templates/settings.PreToolUse.snippet.json +101 -1
  134. 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 NOT RUN IN A DISPOSABLE CLONE — so this runner cannot measure anything
73
- # that depends on one. Measured 2026-08-30: an arm copied `.claude/settings.json` into its clone
74
- # to make the PreToolUse PriorArt hook live; not one of the three clones grew
75
- # `.claude/.prior_art_events.tsv`, while the live repo's copy carries entries from the same hour.
76
- # The hook never fired, so the "hook vs no-hook" contrast was HOOK ≡ NOHOOK and either verdict
77
- # would have been false. Before claiming a hook-dependent result, check the hook's own
78
- # evidence file INSIDE the clone; absence of that file invalidates the arm, not the hypothesis.
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
- ( cd "$WORK" && timeout "$TIMEOUT" claude -p "$PROMPT" \
428
- --model "$MODEL" "${TOOLS[@]}" \
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