@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.
Files changed (111) 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 +50 -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 +12 -2
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +37 -5
  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 +30 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -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 +20 -0
  21. package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
  22. package/package.json +40 -2
  23. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  24. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  25. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  26. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  27. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  30. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  31. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  32. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  33. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  34. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  35. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  36. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  37. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  38. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  39. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  40. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  41. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  42. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  43. package/plugins/fh-meta/CHANGELOG.md +43 -1
  44. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  45. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  47. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  50. package/plugins/fh-qp/README.md +71 -0
  51. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  53. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  54. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  56. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  59. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  60. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  61. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  62. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  63. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  64. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  65. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  66. package/scripts/chamber_run.sh +14 -5
  67. package/scripts/com.forge-harness.live-eval.plist +84 -0
  68. package/scripts/compaction_probe.sh +9 -35
  69. package/scripts/directional_diff_gate.sh +14 -2
  70. package/scripts/frontier_digest_autopilot.sh +4 -1
  71. package/scripts/map_postprocess.py +90 -0
  72. package/scripts/outbound_query_guard.sh +131 -0
  73. package/scripts/outbound_query_hook.sh +373 -0
  74. package/scripts/package_coverage_check.sh +55 -16
  75. package/scripts/pipe_verdict_guard.sh +41 -1
  76. package/scripts/probe_live_eval.sh +240 -0
  77. package/scripts/probe_live_eval_lib.py +579 -0
  78. package/scripts/proposal_hook.sh +120 -17
  79. package/scripts/push_zone_check.sh +78 -0
  80. package/scripts/residency_closure_scan.py +252 -0
  81. package/scripts/selfcheck.sh +41 -1
  82. package/scripts/session_close_check.sh +100 -0
  83. package/scripts/sim_isolated_run.sh +98 -2
  84. package/scripts/test_action_yml_lanes.sh +97 -0
  85. package/scripts/test_fh_qp_lanes.sh +105 -0
  86. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  87. package/scripts/test_map_postprocess_lanes.sh +143 -0
  88. package/scripts/test_marker_affected_lanes.sh +93 -0
  89. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  90. package/scripts/test_marker_oracle_lanes.sh +136 -0
  91. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  92. package/scripts/test_outbound_query_lanes.sh +87 -0
  93. package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
  94. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  95. package/scripts/test_preprep_drift_anchor.sh +3 -3
  96. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  97. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  98. package/scripts/test_proposal_hook_lanes.sh +22 -1
  99. package/scripts/test_push_zone_lanes.sh +304 -0
  100. package/scripts/test_residency_closure_lanes.sh +70 -0
  101. package/scripts/test_sim_isolated_run_lanes.sh +119 -0
  102. package/scripts/test_utterance_intake_lanes.sh +414 -0
  103. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  104. package/scripts/transcript_utterances.py +222 -0
  105. package/scripts/utterance_intake.sh +424 -0
  106. package/scripts/validate_yaml.sh +27 -0
  107. package/scripts/worktree_reclaim.sh +95 -0
  108. package/templates/.git-hooks/pre-commit +353 -1
  109. package/templates/.git-hooks/pre-push +91 -0
  110. package/templates/RED_TEAM_REPORT.md +49 -0
  111. package/templates/settings.PreToolUse.snippet.json +65 -1
@@ -269,6 +269,73 @@ fi
269
269
 
270
270
  fi
271
271
 
272
+ # ── ①-f UTTERANCE LANDING (advisory) ─────────────────────────────────────────
273
+ # WHY. 마감 체크는 **형식**만 본다 — 카드가 로그보다 새로운가, 필수 아티팩트가 있는가.
274
+ # **운영자 발화가 기록에 착지했는지는 아무도 안 봤다.** `utterance_landing_check.sh` 가 그걸
275
+ # 재려고 2026-08-08 에 지어졌는데 probes.tsv 를 **손으로** 짜야 해서 호출부가 0 개였다
276
+ # (2026-09-05 실측: 이 파일에서 그 스크립트를 부르는 줄이 하나도 없었다). 그 사이의 빠진 칸이
277
+ # `utterance_intake.sh` 다 — 전사본에서 발화를 뽑아 프로브를 자동 생성한다.
278
+ # 근거: 같은 날 운영자의 교리·설계급 발화 6 건이 착지한 경로가 전부 **거버너의 손**이었다.
279
+ #
280
+ # 🟥 라벨이 `①-e` 가 아닌 이유: 그건 아래 stray-path 블록이 **이미 쓰고 있다.** 한 라벨을 두
281
+ # 검사가 나눠 쓰면 「어느 ①-e 가 울렸나」를 아무도 못 가른다 — 이 배선을 준비한 진단은
282
+ # «①-e 는 비어 있다» 고 적었고, 그건 거짓이었다(레인 L13 이 «배선 전인데 초록» 으로 잡았다).
283
+ #
284
+ # 🟥 **advisory 다 — 막지 않는다.** 프로브는 키 낱말이라 기록이 같은 뜻을 다른 낱말로 적으면
285
+ # 과차단이 난다(안전한 방향이지만 소음이다). 노출 사다리(shadow) 로 몇 세션 관찰한 뒤
286
+ # close push 차단으로 승격할지는 **운영자 결정**이고, 그때까지 FAIL 을 세우지 않는다.
287
+ _UI_SUT="$FH/scripts/utterance_intake.sh"
288
+ if [ -f "$_UI_SUT" ]; then
289
+ # 세션 id 의 정본은 `branch_claim.sh` 다 — **새 판정기를 짓지 않는다**(재발명 금지).
290
+ # `show` 의 `세션ID` 줄이 그 값이고, 값이 `pid-` 로 시작하면 런타임이 id 를 안 준 것이라
291
+ # 전사본을 특정할 수 없다. 그때는 «다른 세션 것을 대신 읽는» 대신 UNMEASURED 로 남긴다 —
292
+ # 남의 전사본으로 낸 착지 판정은 틀린 답이지 근사값이 아니다.
293
+ _UI_SID=""
294
+ if [ -f "$FH/scripts/branch_claim.sh" ]; then
295
+ _UI_SID=$(LC_ALL=C bash "$FH/scripts/branch_claim.sh" show 2>/dev/null \
296
+ | LC_ALL=C sed -n 's/^세션ID[[:space:]][[:space:]]*//p' | head -1)
297
+ fi
298
+ case "$_UI_SID" in pid-*|'') _UI_SID="" ;; esac
299
+ # 전사본 slug = 프로젝트 절대경로에서 `/` `.` `_` 를 `-` 로 (실측 2026-09-05, ~/.claude/projects 대조)
300
+ _UI_SLUG=$(printf '%s' "$FH" | tr '/._' '---')
301
+ _UI_TRDIR="${FH_TRANSCRIPT_DIR:-$HOME/.claude/projects/$_UI_SLUG}"
302
+ _UI_TR="$_UI_TRDIR/$_UI_SID.jsonl"
303
+ if [ -z "$_UI_SID" ]; then
304
+ echo "ℹ️ ①-f UNMEASURED — 세션 id 미상(런타임이 CLAUDE_CODE_SESSION_ID 를 안 줬다)"
305
+ echo " 전사본을 특정할 수 없다. «발화가 다 적혔다» 가 아니라 «못 쟀다» 다."
306
+ elif [ ! -f "$_UI_TR" ]; then
307
+ echo "ℹ️ ①-f UNMEASURED — 전사본 부재: $_UI_TR"
308
+ echo " 부재를 0 으로 접지 마라 — 발화가 없었다는 뜻이 아니다."
309
+ else
310
+ # 기록 대상: 오늘자 산출 + 상시 카드/UAP. 배열로 모은다(zsh word-split 함정 회피).
311
+ _UI_RECS=()
312
+ for _p in "$FH/tracks/_meta/fh_completed_$TODAY.md" \
313
+ "$FH/tracks/_meta/user_adaptation_profile.md" \
314
+ "$FH/tracks/_meta/reference_next_session_starter.md"; do
315
+ [ -f "$_p" ] && _UI_RECS+=("$_p")
316
+ done
317
+ for _p in "$FH"/tracks/_meta/fh_signal_"$TODAY"_*.md "$FH"/tracks/*/*_"$TODAY".md; do
318
+ [ -f "$_p" ] && _UI_RECS+=("$_p")
319
+ done
320
+ if [ "${#_UI_RECS[@]}" -eq 0 ]; then
321
+ echo "⚠️ ①-f 오늘자 기록 파일이 하나도 없다 — 발화 착지를 잴 대상이 없다(UNMEASURED)"
322
+ echo " ④/⑤ 가 아직 안 돌았다는 뜻일 수 있다. 마감 순서를 확인해라."
323
+ else
324
+ _UI_OUT=$(bash "$_UI_SUT" "$_UI_TR" "${_UI_RECS[@]}" 2>&1); _UI_RC=$?
325
+ case "$_UI_RC" in
326
+ 0) echo "✅ ①-f 발화 착지 — 프로브 전건 착지 (${#_UI_RECS[@]} 파일 대조)"
327
+ echo " ⚠️ 발화별 최장 토큰 ⌈n/2⌉ 일치 기준(키 2 개 이하는 OR) — «제대로 적혔다» 의 증명이 아니다." ;;
328
+ 1) echo "⚠️ ①-f 미착지 발화가 있다 — advisory, 막지 않는다. 아래를 **확인**해라(기록 강제 아님):"
329
+ printf '%s\n' "$_UI_OUT" | sed 's/^/ /' ;;
330
+ *) echo "⚠️ ①-f 발화 착지 검사 UNMEASURED/계기이상 (rc=$_UI_RC) — 0 으로 읽지 마라"
331
+ printf '%s\n' "$_UI_OUT" | sed 's/^/ /' ;;
332
+ esac
333
+ fi
334
+ fi
335
+ else
336
+ echo "⚠️ ①-f utterance_intake.sh 없음 — skipped, not passed (UNMEASURED)"
337
+ fi
338
+
272
339
  # ② FH assets changed today → harvest-loop owed
273
340
  # NOTE: no `grep -q` here — under `set -o pipefail`, -q's early exit SIGPIPEs git log (141),
274
341
  # masking a real match as pipeline failure so the warning NEVER fired on true positives
@@ -704,5 +771,38 @@ else
704
771
  echo "⚠️ ①-e stray_path_scan.sh 없음 — skipped, not passed"
705
772
  fi
706
773
 
774
+ # ── ①-f PUSH-ZONE advisory (surface-not-block; mirrors the pre-push hook's own zone check) ────
775
+ # WHY: templates/.git-hooks/pre-push blocks a single push whose remote sits outside the owner-
776
+ # account list, but it only ever sees the ONE push in front of it. The 2026-07-26 miss this whole
777
+ # axis exists for was a multi-repo ROUND — CLAUDE.local.md §REST API push 계정규칙 was applied
778
+ # correctly to one org repo and forgotten on another in the SAME round. `scripts/push_zone_check.sh`
779
+ # is the hand-run ENUMERATE tool for that round; wiring it here means it runs at least once per
780
+ # close without the operator separately remembering to ask for it — same shape as the ①-b open-PR
781
+ # sweep above. Advisory only: it lists, it never blocks (CLAUDE.md §Surface-Class Degrade Invariant,
782
+ # reversible half — a session-close check is not the irreversible boundary; that is the pre-push
783
+ # hook's job). Applicability is mechanical: both the script and an owner-account list must exist,
784
+ # or this reports UNCALIBRATED rather than guessing.
785
+ _PZ_SCRIPT="$FH/scripts/push_zone_check.sh"
786
+ _PZ_OWNERS="${PUSH_ZONE_OWNERS:-$FH/.claude/rules/.push-zone-owners}"
787
+ if [ -x "$_PZ_SCRIPT" ] && [ -f "$_PZ_OWNERS" ]; then
788
+ if _pz_out=$(bash "$_PZ_SCRIPT" 2>&1); then
789
+ # Counts only — the enumerator prints owner-account names and per-repo owners, and this close
790
+ # output lands in transcripts and session cards. The pre-push block never prints the list; this
791
+ # advisory must not either (codex #9, 2026-09-05). The names are one command away for the operator.
792
+ _pz_n=$(printf '%s\n' "$_pz_out" | grep -cE '^(✅|🔶) ' 2>/dev/null || true)
793
+ _pz_rest=$(printf '%s\n' "$_pz_out" | grep -cE '^🔶 ' 2>/dev/null || true)
794
+ echo "⚠️ ①-f push-zone: ${_pz_n:-0} repo(s) enumerated · ${_pz_rest:-0} outside the owner accounts (REST channel) — names withheld here; run: bash scripts/push_zone_check.sh"
795
+ else
796
+ # Verified 2026-09-05: push_zone_check.sh does not exit non-zero merely because `gh` is
797
+ # missing (its own `active="(unknown)"` fallback absorbs that) — the only documented non-zero
798
+ # exit is UNCALIBRATED for a missing owner list, already excluded by the `-f "$_PZ_OWNERS"`
799
+ # guard above. A non-zero exit reaching here is therefore an unanticipated failure; report it
800
+ # as UNMEASURED rather than silently treating empty/partial output as "nothing to enumerate".
801
+ echo "⚠️ ①-f push-zone: UNMEASURED — push_zone_check.sh exited non-zero, could not enumerate"
802
+ fi
803
+ else
804
+ echo "⏭️ ①-f push-zone UNCALIBRATED — script or owner-account list absent"
805
+ fi
806
+
707
807
  echo "── close check: $([ "$FAIL" -eq 0 ] && echo CONSISTENT || echo VIOLATIONS) ──"
708
808
  exit "$FAIL"
@@ -149,6 +149,23 @@
149
149
  # 이유는 헤더 상단 "WHAT THIS GIVES YOU" 절이 이미 말한다 — 여기서 되풀이하지 않는다.
150
150
  # 요약: 파일시스템 코퍼스 축(WIPE+RESEED)은 클론 단위로 이미 wipe-and-reseed다. 기계 표면 축은
151
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.
152
169
 
153
170
  set -uo pipefail
154
171
 
@@ -311,6 +328,7 @@ while [ $# -gt 0 ]; do
311
328
  --base-sha) BASE_SHA="${2:-}"; shift 2 ;;
312
329
  --setup) SETUP="${2:-}"; shift 2 ;; # shell run INSIDE each clone before the sim. See below.
313
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.
314
332
  *) echo "unknown flag: $1" >&2; exit 2 ;;
315
333
  esac
316
334
  done
@@ -320,6 +338,62 @@ case "$MODE" in observe|act) ;; *) echo "FAIL: --mode must be observe|act" >&2;
320
338
 
321
339
  command -v claude >/dev/null 2>&1 || { echo "FAIL: claude CLI not on PATH" >&2; exit 2; }
322
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
+
323
397
  OUTDIR="${OUTDIR:-$(mktemp -d "${TMPDIR:-/tmp}/fh-sim-XXXXXX")}"
324
398
  mkdir -p "$OUTDIR"
325
399
 
@@ -355,6 +429,7 @@ _model_known_cutoff() { # $1=--model 값 → ISO 월 또는 UNKNOWN
355
429
 
356
430
  echo "── sim_isolated_run ──────────────────────────────────────────────"
357
431
  echo "arm=$ARM mode=$MODE model=$MODEL reps=$REPS timeout=${TIMEOUT}s"
432
+ echo "timeout_tool=${TIMEOUT_KIND}${TIMEOUT_BIN:+:$TIMEOUT_BIN}"
358
433
  echo "out=$OUTDIR"
359
434
 
360
435
  snapshot "$OUTDIR/_machine_before.txt"
@@ -435,6 +510,19 @@ for r in $(seq 1 "$REPS"); do
435
510
  # compaction_probe.sh 7 중 1 · starter.md 22 중 1 — 여유 충분
436
511
  ARM_BLIND_PATHS=( "scripts/fixtures" "scripts/round" "scripts/context_continuity_score.sh" )
437
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
438
526
  rm -rf "$WORK/$_bp"
439
527
  # 🟥 rm 의 rc 를 안 믿는다 — 경로 오타면 rm 은 «성공»을 낸다(지울 게 없으니).
440
528
  # 판정은 «제거 후에 정말 없는가»로 한다.
@@ -514,8 +602,16 @@ for r in $(seq 1 "$REPS"); do
514
602
  # 부르며 정직하게 거부하면서도, 거부문 안에서 정답을 말한다. 채점기는 그걸 토큰으로 센다.
515
603
  # 이것이 회차 1~3 과 probe1~4 를 전부 무효로 만든 근인이고, 경로 deny·코퍼스 마스킹은
516
604
  # **원리적으로 못 막는다**(도구 읽기가 아니라 프롬프트 조립이다).
517
- ( cd "$WORK" && timeout "$TIMEOUT" claude -p "$PROMPT" \
518
- --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[@]}"} \
519
615
  < /dev/null 2>"$OUTDIR/${ARM}_r${r}.stderr.txt" ) > "$OUTDIR/${ARM}_r${r}.txt"
520
616
  rc=$?
521
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,105 @@
1
+ #!/usr/bin/env bash
2
+ # test_fh_qp_lanes.sh — known-pair lanes for plugins/fh-qp (QP · Quality Platform).
3
+ #
4
+ # What it proves: the MECHANICAL half (plugins/fh-qp/scripts/qp_tools.sh) discriminates on fixtures
5
+ # shipped with the plugin — three negatives from the chamber oracle (profile-required target → refuse ·
6
+ # adapter absent → HARNESS_ERROR · dirty evidence → masked, residue 0), surface_reach four states,
7
+ # the verdict-record contract (mtm-check), the state-changing-verb floor, and a residency scan of the
8
+ # whole scaffold (fail-closed when the scanner cannot run — exit 10 is never read as clean).
9
+ # What it does NOT prove: that a session FOLLOWS the SKILL prose (that is the floor-tier sim's job),
10
+ # or that any verdict label is TRUE (judgment, deliberately unmechanised).
11
+ # Exit: 0 all lanes pass · 1 any lane fails · 10 harness error (plugin/tool missing).
12
+ set -uo pipefail
13
+ FH="$(cd "$(dirname "$0")/.." && pwd)"
14
+ T="$FH/plugins/fh-qp/scripts/qp_tools.sh"
15
+ X="$FH/plugins/fh-qp/fixtures"
16
+ [ -f "$T" ] || { echo "HARNESS-ERROR: $T missing"; exit 10; }
17
+ [ -d "$X" ] || { echo "HARNESS-ERROR: fixtures dir missing"; exit 10; }
18
+ TMP="$(mktemp -d "${TMPDIR:-/tmp}/fhqp.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
19
+ PASS=0; FAIL=0
20
+ lane() { # $1 name · $2 expected rc · $3 expected stdout regex · rest = command
21
+ local name="$1" want_rc="$2" want_re="$3"; shift 3
22
+ local out rc; out="$("$@" 2>&1)"; rc=$?
23
+ if [ "$rc" = "$want_rc" ] && printf '%s' "$out" | /usr/bin/grep -qE "$want_re"; then
24
+ PASS=$((PASS+1)); printf ' ✅ %-34s rc=%s %s\n' "$name" "$rc" "$(printf '%s' "$out" | head -1 | cut -c1-70)"
25
+ else
26
+ FAIL=$((FAIL+1)); printf ' ❌ %-34s rc=%s (want %s /%s/)\n %s\n' "$name" "$rc" "$want_rc" "$want_re" "$out"
27
+ fi
28
+ }
29
+ echo "── fh-qp lanes ──"
30
+ # ── negative ① profile-required → refuse (and the positive pair: public passes, profile unlocks) ──
31
+ lane N1-private-host-refused 4 '^PROFILE_REQUIRED kind=web' bash "$T" target-class https://private.example.internal/x
32
+ lane N1-rfc1918-refused 4 '^PROFILE_REQUIRED' bash "$T" target-class http://10.1.2.3/login
33
+ lane N1-desktop-needs-profile 4 '^PROFILE_REQUIRED kind=desktop' bash "$T" target-class app:SomeApp
34
+ lane N1-public-passes 0 '^PUBLIC kind=web host=example\.com' bash "$T" target-class https://example.com/
35
+ printf 'targets:\n web:\n - base_url: https://private.example.internal\n' > "$TMP/profile.yaml"
36
+ lane N1-profile-unlocks 0 '^PROFILE_OK' bash "$T" target-class https://private.example.internal/x --profile "$TMP/profile.yaml"
37
+ lane N1-bad-form-unknown 10 '^UNKNOWN' bash "$T" target-class ftp://x
38
+ # ── negative ② adapter absent → HARNESS_ERROR (pair: present → ADAPTER=) ──
39
+ lane N2-no-tools-harness-error 10 '^HARNESS_ERROR reason=tool-list-not-supplied' bash "$T" adapter-probe --need web
40
+ lane N2-wrong-kind-harness-error 10 '^HARNESS_ERROR reason=no-desktop-adapter' bash "$T" adapter-probe --need desktop --tools "mcp__playwright__browser_navigate,Read"
41
+ lane N2-web-adapter-found 0 '^ADAPTER=playwright-mcp evidence=dom' bash "$T" adapter-probe --need web --tools "Read,mcp__playwright__browser_navigate"
42
+ lane N2-desktop-adapter-found 0 '^ADAPTER=computer-use-mcp evidence=pixel' bash "$T" adapter-probe --need desktop --tools "mcp__computer-use__screenshot"
43
+ # ── negative ③ dirty evidence → masked, residue 0 (pair: clean stays clean; raw never survives) ──
44
+ lane N3-dirty-masked 0 '^MASKED residue=0' bash "$T" mask "$X/evidence_known_dirty.txt" "$TMP/masked.txt"
45
+ if /usr/bin/grep -qE 'qa\.tester@|hunter2|eyJhbGci|abcdefghijklmnop' "$TMP/masked.txt"; then FAIL=$((FAIL+1)); echo " ❌ N3-raw-survived-in-output"; else PASS=$((PASS+1)); echo " ✅ N3-raw-absent-in-output"; fi
46
+ lane N3-clean-stays-clean 0 '^MASKED residue=0' bash "$T" mask "$X/evidence_known_clean.txt" "$TMP/masked2.txt"
47
+ lane N3-missing-input-harness 10 '^HARNESS_ERROR' bash "$T" mask "$X/does_not_exist.txt" "$TMP/m3.txt"
48
+ # ── surface_reach four states ──
49
+ lane SR-reached 0 '^REACHED tcs_beyond_entry=2 tcs_total=2' bash "$T" surface-reach "$X/reach_known_reached.tsv"
50
+ lane SR-partial 5 '^PARTIAL tcs_beyond_entry=1 tcs_total=2' bash "$T" surface-reach "$X/reach_known_partial.tsv"
51
+ lane SR-wall 5 '^NOT_REACHED tcs_beyond_entry=0 tcs_total=3' bash "$T" surface-reach "$X/reach_known_wall.tsv"
52
+ : > "$TMP/reach_empty.tsv" # generated here — an empty file cannot travel in a diff (PATCH.diff dropped it; measured 2026-09-05)
53
+ lane SR-empty-unmeasured 10 '^UNMEASURED reason=evidence-empty' bash "$T" surface-reach "$TMP/reach_empty.tsv"
54
+ lane SR-missing-unmeasured 10 '^UNMEASURED reason=evidence-missing' bash "$T" surface-reach "$X/no_such_file.tsv"
55
+ # ── verdict record contract ──
56
+ lane MTM-good 0 '^OK mtm=UNAVAILABLE rows=3 machine_closed=2 judgment_left=1' bash "$T" mtm-check "$X/verdicts_known_good.tsv"
57
+ lane MTM-branch-needs-active 10 'DIFFERS_FROM_PLAN-needs-mtm-ACTIVE' bash "$T" mtm-check "$X/verdicts_known_bad_branch.tsv"
58
+ lane MTM-vacuous-machine 10 'MACHINE-closure-without-assertion' bash "$T" mtm-check "$X/verdicts_known_bad_vacuous_machine.tsv"
59
+ printf '#mtm: ACTIVE\nTC-1\tFAIL\tDIFFERS_FROM_PLAN\tMACHINE\tverify\ttext-visible:x\tINVENTORY\n' > "$TMP/v_src.tsv"
60
+ lane MTM-branch-source-binding 10 'DIFFERS_FROM_PLAN-needs-expected_source=PLAN_DOC' bash "$T" mtm-check "$TMP/v_src.tsv"
61
+ printf 'TC-1\tPASS\tAS_PLANNED\tMACHINE\tclick\tx\tINVENTORY\n' > "$TMP/v_nomtm.tsv"
62
+ lane MTM-state-line-required 10 'mtm-state-missing' bash "$T" mtm-check "$TMP/v_nomtm.tsv"
63
+ lane VERBS-good 0 '^OK state_changing_verbs=2' bash "$T" run-verbs "$X/verdicts_known_good.tsv"
64
+ lane VERBS-verify-only 5 '^VERIFY_ONLY' bash "$T" run-verbs "$X/verdicts_verify_only.tsv"
65
+ # ── screen-id: same content with different ref tokens → same id; different content → different id ──
66
+ printf -- '- heading "Home" [level=1] [ref=e2]\n- link "About" [ref=e7]\n' > "$TMP/s1.md"
67
+ printf -- '- heading "Home" [level=1] [ref=f3e2]\n- link "About" [ref=f3e7]\n' > "$TMP/s2.md"
68
+ printf -- '- heading "About" [level=1] [ref=e2]\n' > "$TMP/s3.md"
69
+ id1=$(bash "$T" screen-id "$TMP/s1.md"); id2=$(bash "$T" screen-id "$TMP/s2.md"); id3=$(bash "$T" screen-id "$TMP/s3.md")
70
+ if [ "$id1" = "$id2" ] && [ "$id1" != "$id3" ] && [ ${#id1} -eq 12 ]; then PASS=$((PASS+1)); echo " ✅ SID-content-hash-stable-across-refs ($id1)"; else FAIL=$((FAIL+1)); echo " ❌ SID-content-hash ($id1 / $id2 / $id3)"; fi
71
+ # ── residency: the whole scaffold, fail-closed ──
72
+ FILES="$(cd "$FH" && find plugins/fh-qp scripts/test_fh_qp_lanes.sh -type f | sort)"
73
+ # Worktrees do not carry the gitignored operator pattern file — fall back to the main checkout's copy
74
+ # (git-common-dir), else honour RESIDENCY_PATTERNS, else the scanner exits 10 and this lane FAILS (fail-closed).
75
+ if [ -z "${RESIDENCY_PATTERNS:-}" ] && [ ! -f "$FH/.claude/rules/.residency-patterns" ]; then
76
+ _common="$(cd "$FH" && git rev-parse --git-common-dir 2>/dev/null)"
77
+ [ -n "$_common" ] && [ -f "$_common/../.claude/rules/.residency-patterns" ] && export RESIDENCY_PATTERNS="$(cd "$_common/.." && pwd)/.claude/rules/.residency-patterns"
78
+ fi
79
+ if [ -f "$FH/scripts/residency_closure_scan.py" ]; then
80
+ # shellcheck disable=SC2086
81
+ out="$(cd "$FH" && python3 scripts/residency_closure_scan.py --files $FILES 2>&1)"; rc=$?
82
+ case "$rc" in
83
+ 0) PASS=$((PASS+1)); echo " ✅ RES-closure-scan CLEAN ($(printf '%s' "$FILES" | wc -l | tr -d ' ') files)" ;;
84
+ 10) if printf '%s' "$out" | grep -q '운영자 패턴 파일이 없다'; then
85
+ # CI and consumer clones structurally lack the operator override (gitignored). That is
86
+ # UNMEASURED, not a failure of the shipped files — the generic RES-zero-constant-grep lane
87
+ # below is the guarantee that travels; this lane measures only where the override exists.
88
+ echo " ⚪ RES-closure-scan SKIPPED — operator patterns file absent (UNMEASURED, not clean; the generic grep lane still runs)"
89
+ else
90
+ FAIL=$((FAIL+1)); echo " ❌ RES-closure-scan HARNESS (rc=10 — defaults missing or pattern compile error)"; echo "$out" | tail -3
91
+ fi ;;
92
+ *) FAIL=$((FAIL+1)); echo " ❌ RES-closure-scan TAINTED (rc=$rc)"; echo "$out" | tail -5 ;;
93
+ esac
94
+ else
95
+ FAIL=$((FAIL+1)); echo " ❌ RES-closure-scan scanner missing (fail-closed)"
96
+ fi
97
+ # generic zero-constant grep (no scanner needed): private hosts / secrets shapes must not appear in shipped files
98
+ # except inside the profile EXAMPLE and fixtures, which exist to show the shapes.
99
+ # The lane itself is the instrument (its known-negative literals are the point) — scanned by residency above, excluded here.
100
+ SHIPPED="$(printf '%s' "$FILES" | /usr/bin/grep -vE 'fixtures/|qp_profile\.example\.yaml|test_fh_qp_lanes\.sh')"
101
+ # shellcheck disable=SC2086
102
+ hits="$(cd "$FH" && /usr/bin/grep -nE '\b(10|192\.168)\.[0-9]+\.[0-9]+\.[0-9]+\b|eyJ[A-Za-z0-9_-]{8,}\.|AKIA[A-Z0-9]{8,}' $SHIPPED 2>/dev/null | /usr/bin/grep -vE 'qp_tools\.sh:.*(_MASK_|case|10\.\*)' || true)"
103
+ if [ -z "$hits" ]; then PASS=$((PASS+1)); echo " ✅ RES-zero-constant-grep (0 hits in shipped files)"; else FAIL=$((FAIL+1)); echo " ❌ RES-zero-constant-grep"; echo "$hits"; fi
104
+ echo "── fh-qp lanes: $PASS passed, $FAIL failed ──"
105
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # test_gate_two_verdicts_lanes.sh — pre-commit 의 `_acceptance_evidence_line` 계약을 고정한다.
3
+ #
4
+ # WHY (2026-09-06, frontier-digest 답습 · arXiv:2609.04167 «SWE-Gate»): 그 논문의 실측은
5
+ # 기능 테스트를 통과한 수리 644 건 중 **221 건(34%)** 이 실제 PR 리뷰 코멘트에서 뽑은 제약을
6
+ # 어겼다는 것이고, 처방은 «기능 정확성과 리뷰 제약 충족을 따로 채점하라» 다. 이 훅도 같은
7
+ # 형태였다 — 축이 다 통과하면 «ALL AXES PASSED» 라는 **합성 판정 하나**만 찍혔고, 마커의
8
+ # `crossfamily: DEGRADED_PANEL_UNUSED` 같은 값은 출력 어디에도 안 나왔다.
9
+ #
10
+ # SCOPE — 채널이지 판정이 아니다(§Mechanization Boundary): 이 함수는 «어떤 값이 적혔나」만
11
+ # 찍는다. 값의 진위를 판정하지 않고, 어떤 경우에도 종료코드를 바꾸지 않는다. 그래서 레인도
12
+ # «막나」가 아니라 «가려지나」를 잰다 — 특히 DEGRADED/UNKNOWN 이 조용히 사라지지 않는지.
13
+ #
14
+ # 종료코드: 0 = 계약대로 · 1 = 회귀
15
+ set -uo pipefail
16
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
18
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
19
+ [ -f "$HOOK" ] || { echo "ⓘ $HOOK absent — subject missing when we looked (NOT a pass)"; exit 2; }
20
+
21
+ sed -n '/^_acceptance_evidence_line()/,/^}/p' "$HOOK" > "$T/fn.sh"
22
+ # 계기 보정: 추출이 비면 아래 픽스처가 «아무것도 아닌 것」을 재고 전부 통과한다.
23
+ if ! grep -q '_acceptance_evidence_line' "$T/fn.sh"; then
24
+ echo "❌ HARNESS-ERROR — _acceptance_evidence_line 이 $HOOK 에서 추출되지 않았다."
25
+ echo " 픽스처가 빈 함수를 잴 참이었다. 초록을 보고하지 않고 중단한다."
26
+ exit 1
27
+ fi
28
+
29
+ FAIL=0
30
+ run() { bash -c 'set -uo pipefail; . "$1"; _acceptance_evidence_line "$2"' _ "$T/fn.sh" "${1:-}" 2>&1; }
31
+ lane() { # $1=id $2=marker body ('' = 마커 없음) $3..=출력에 반드시 있어야 하는 조각들
32
+ local id="$1" body="$2"; shift 2
33
+ local path="" out n rc
34
+ if [ -n "$body" ]; then path="$T/m.marker"; printf '%s\n' "$body" > "$path"; fi
35
+ out=$(run "$path"); rc=$?
36
+ if [ $rc -ne 0 ]; then
37
+ printf ' ❌ %-34s 함수가 rc=%s — 보고 전용인데 실패했다\n %s\n' "$id" "$rc" "$out"; FAIL=1; return
38
+ fi
39
+ for n in "$@"; do
40
+ if ! printf '%s' "$out" | grep -qF -- "$n"; then
41
+ printf ' ❌ %-34s «%s» 가 출력에 없다\n %s\n' "$id" "$n" "$out"; FAIL=1; return
42
+ fi
43
+ done
44
+ printf ' ✅ %-34s %s\n' "$id" "$(printf '%s' "$out" | head -1)"
45
+ }
46
+
47
+ echo "== 수용-근거 줄: 값이 그대로 드러나는가 =="
48
+
49
+ # L1 known-positive — 네 필드가 다 있으면 네 값이 다 나온다
50
+ lane L1-all-four "crossfamily: panel(codex,gemini) — 2라운드
51
+ standpoint: tier2(qasp) — 명령과 출력을 댔다
52
+ thirdparty: checked — 상류 소스 독해
53
+ oracle: known-pair — 양성/음성 픽스처" \
54
+ "crossfamily=panel(codex,gemini)" "standpoint=tier2(qasp)" "thirdparty=checked" "oracle=known-pair"
55
+
56
+ # L2 🟥 하중 레인 — DEGRADED 가 «통과» 뒤에 숨지 않는다 (이 레인 스위트의 존재 이유)
57
+ lane L2-degraded-visible "crossfamily: DEGRADED_PANEL_UNUSED — 패널 미사용
58
+ standpoint: not-applicable — Q0 ⓒ
59
+ oracle: none — 잴 값이 없다" \
60
+ "crossfamily=DEGRADED_PANEL_UNUSED" "standpoint=not-applicable"
61
+
62
+ # L3 UNKNOWN 도 마찬가지 — «안 봤다»가 «봤는데 괜찮다»로 안 접힌다
63
+ lane L3-unknown-visible "crossfamily: UNKNOWN — 안 봤다
64
+ standpoint: UNKNOWN — 안 봤다" \
65
+ "crossfamily=UNKNOWN" "standpoint=UNKNOWN"
66
+
67
+ # L4 부재는 «(없음)» 으로 — 빈 문자열이면 필드가 있었는지조차 안 보인다 (미측정≠0)
68
+ lane L4-absent-is-typed "axis2-model: opus" \
69
+ "crossfamily=(없음)" "standpoint=(없음)" "thirdparty=(없음)" "oracle=(없음)"
70
+
71
+ # L5 마커 자체가 없는 경로(경량 게이트) — 근거가 «있다»고 주장하지 않는다
72
+ lane L5-no-marker "" "n/a" "경량 게이트"
73
+
74
+ # L6 들여쓴 필드도 읽는다(마커는 손으로 쓰는 파일이다)
75
+ lane L6-indented " crossfamily: declined — 근거 있음" "crossfamily=declined"
76
+
77
+ # L7 첫 줄만 — 같은 키가 둘이면 앞의 것을 찍고 조용히 합치지 않는다
78
+ lane L7-first-wins "crossfamily: UNKNOWN — 안 봤다
79
+ crossfamily: panel(codex) — 나중 줄" "crossfamily=UNKNOWN"
80
+
81
+ # L8 🟥 첫 실사용이 잡은 결함 — 괄호 «안»에 공백이 있으면 값이 잘려 문법도 깨진 문자열이 찍혔다
82
+ # (`thirdparty: checked(확인한 것 = …)` → `checked(확인한`). 판별자는 «괄호가 닫혔는가» 하나.
83
+ lane L8-open-paren-trimmed "thirdparty: checked(확인한 것 = 초록 원문 대조)" "thirdparty=checked"
84
+ # L9 반대편 — 닫힌 괄호는 값의 일부라 그대로 살아야 한다(L8 의 수리가 이걸 깨면 안 된다)
85
+ lane L9-closed-paren-kept "crossfamily: panel(codex,gemini) — 2라운드" "crossfamily=panel(codex,gemini)"
86
+ lane L9b-tier-paren-kept "standpoint: tier2(qasp) — 명령과 출력" "standpoint=tier2(qasp)"
87
+
88
+ # L8b 잘린 값이 «그대로» 새어나오지 않는지 직접 확인 (needle 부재를 단언)
89
+ printf 'thirdparty: checked(확인한 것 = x)\n' > "$T/p.marker"
90
+ if run "$T/p.marker" | grep -qF 'checked(확인한'; then
91
+ echo " ❌ L8b 잘린 괄호 문자열이 아직 출력에 있다"; FAIL=1
92
+ else
93
+ echo " ✅ L8b 잘린 괄호 문자열은 출력에 없다"
94
+ fi
95
+
96
+ # ── 컨트롤 A (판별력): 값이 다르면 출력도 달라야 한다 ─────────────────────
97
+ printf 'crossfamily: panel(codex) — x\n' > "$T/a.marker"
98
+ printf 'crossfamily: DEGRADED_SINGLE_FAMILY — y\n' > "$T/b.marker"
99
+ OA=$(run "$T/a.marker"); OB=$(run "$T/b.marker")
100
+ if [ "$OA" != "$OB" ] && printf '%s' "$OB" | grep -qF 'DEGRADED_SINGLE_FAMILY'; then
101
+ echo " ✅ CTRL-A 판별력: 서로 다른 값이 서로 다른 줄을 낸다"
102
+ else
103
+ echo " ❌ CTRL-A 판별력 없음 — 두 마커가 같은 줄을 냈다(계기가 값을 안 읽는다)"; FAIL=1
104
+ fi
105
+
106
+ # ── 컨트롤 B (되돌림): 값 추출을 죽인 뮤턴트에서 L2 가 빨개지는가 ──────────
107
+ # 이게 없으면 «필드를 안 읽는 함수»도 이 스위트를 통과한다.
108
+ sed 's/^ for f in crossfamily.*/ for f in ; do :; done; out="";/' "$T/fn.sh" > "$T/mut.sh"
109
+ if ! grep -q 'out=""' "$T/mut.sh"; then
110
+ echo " ❌ CTRL-B 뮤턴트가 적용되지 않았다 — 되돌림 프로브가 장식이다"; FAIL=1
111
+ else
112
+ MOUT=$(bash -c 'set -uo pipefail; . "$1"; _acceptance_evidence_line "$2"' _ "$T/mut.sh" "$T/b.marker" 2>&1 || true)
113
+ if printf '%s' "$MOUT" | grep -qF 'DEGRADED_SINGLE_FAMILY'; then
114
+ echo " ❌ CTRL-B 뮤턴트인데도 값이 나온다 — 레인이 실물을 안 재고 있다"; FAIL=1
115
+ else
116
+ echo " ✅ CTRL-B 되돌림: 추출을 죽이면 값이 사라진다(레인이 실패할 수 있음을 증명)"
117
+ fi
118
+ fi
119
+
120
+ # ── 컨트롤 C (배선): 훅의 PASS 배너가 이 함수를 실제로 부르는가 ───────────
121
+ # 함수만 있고 호출부가 없으면 산문이다([[feedback_built_but_not_wired]]).
122
+ if grep -q '_acceptance_evidence_line "\${MARKER:-}"' "$HOOK" \
123
+ && grep -q 'ALL AXES PASSED' "$HOOK"; then
124
+ echo " ✅ CTRL-C 배선: PASS 배너가 이 함수를 부른다"
125
+ else
126
+ echo " ❌ CTRL-C 배선 없음 — 정의만 있고 PASS 배너가 안 부른다"; FAIL=1
127
+ fi
128
+
129
+ # ── 컨트롤 D (무해): 이 함수는 종료코드를 바꾸지 않는다 (채널이지 게이트가 아니다) ──
130
+ run "" >/dev/null 2>&1; RC_NONE=$?
131
+ run "$T/b.marker" >/dev/null 2>&1; RC_DEG=$?
132
+ if [ "$RC_NONE" -eq 0 ] && [ "$RC_DEG" -eq 0 ]; then
133
+ echo " ✅ CTRL-D 채널: 마커 부재도 DEGRADED 도 rc=0 (막지 않는다)"
134
+ else
135
+ echo " ❌ CTRL-D 이 함수가 종료코드를 바꾼다 (none=$RC_NONE degraded=$RC_DEG) — 채널이 아니라 게이트가 됐다"; FAIL=1
136
+ fi
137
+
138
+ echo "── gate-two-verdicts lanes: $([ $FAIL -eq 0 ] && echo 'all green' || echo 'FAILED')"
139
+ exit "$FAIL"