@chrono-meta/fh-gate 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/regression/probes_live.yaml +137 -0
- package/.claude/rules/.residency-patterns.defaults +7 -0
- package/.claude/rules/fh_4axis_gate.md +50 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +12 -2
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +37 -5
- package/README.zh.md +45 -8
- package/docs/STANDARDS_ALIGNMENT.md +61 -0
- package/docs/USER_GUIDE.md +3 -0
- package/docs/USE_CASES.md +50 -0
- package/docs/model_tier_expectations.md +60 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
- package/package.json +40 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/README.md +4 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
- package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
- package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
- package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
- package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
- package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
- package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
- package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +43 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-qp/README.md +71 -0
- package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
- package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
- package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
- package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
- package/plugins/fh-qp/qp_profile.example.yaml +29 -0
- package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
- package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
- package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
- package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
- package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
- package/scripts/chamber_run.sh +14 -5
- package/scripts/com.forge-harness.live-eval.plist +84 -0
- package/scripts/compaction_probe.sh +9 -35
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/map_postprocess.py +90 -0
- package/scripts/outbound_query_guard.sh +131 -0
- package/scripts/outbound_query_hook.sh +373 -0
- package/scripts/package_coverage_check.sh +55 -16
- package/scripts/pipe_verdict_guard.sh +41 -1
- package/scripts/probe_live_eval.sh +240 -0
- package/scripts/probe_live_eval_lib.py +579 -0
- package/scripts/proposal_hook.sh +120 -17
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/selfcheck.sh +41 -1
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +98 -2
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_marker_affected_lanes.sh +93 -0
- package/scripts/test_marker_crossfamily_lanes.sh +90 -6
- package/scripts/test_marker_oracle_lanes.sh +136 -0
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +3 -3
- package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
- package/scripts/test_probe_live_eval_lanes.sh +437 -0
- package/scripts/test_proposal_hook_lanes.sh +22 -1
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_sim_isolated_run_lanes.sh +119 -0
- package/scripts/test_utterance_intake_lanes.sh +414 -0
- package/scripts/test_worktree_reclaim_lanes.sh +70 -0
- package/scripts/transcript_utterances.py +222 -0
- package/scripts/utterance_intake.sh +424 -0
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +353 -1
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +65 -1
|
@@ -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
|
-
(
|
|
518
|
-
|
|
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"
|