@chrono-meta/fh-gate 2.0.0 → 2.0.1

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 (39) hide show
  1. package/.claude/capabilities/adapters/gstack-content-safety.cap +110 -0
  2. package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
  3. package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
  4. package/.claude/capabilities/degrade-direction-scan.cap +24 -0
  5. package/.claude/capabilities/public-surface-scan.cap +24 -0
  6. package/.claude-plugin/marketplace.json +3 -3
  7. package/AGENTS.md +9 -3
  8. package/CHEATSHEET.md +17 -1
  9. package/CLAUDE.md +2 -2
  10. package/README.ja.md +55 -20
  11. package/README.ko.md +86 -20
  12. package/README.md +57 -20
  13. package/README.zh.md +49 -19
  14. package/knowledge/shared/harness-core/fh_three_layer_canon.md +96 -5
  15. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +1 -1
  16. package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +214 -0
  17. package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
  18. package/knowledge/shared/harness-core/ship_readiness_gate.md +29 -10
  19. package/knowledge/shared/learnings/subagent_invocations_log.yaml +62 -0
  20. package/package.json +10 -2
  21. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  22. package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
  23. package/plugins/fh-meta/CHANGELOG.md +17 -0
  24. package/scripts/adapters/gstack_content_safety.sh +186 -0
  25. package/scripts/adapters/mate_agent_boundary.sh +120 -0
  26. package/scripts/adapters/peer_resolve.sh +99 -0
  27. package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
  28. package/scripts/branch_claim.sh +14 -0
  29. package/scripts/capability_effect_probe.sh +340 -20
  30. package/scripts/capability_registry_check.sh +198 -10
  31. package/scripts/cluster_capability_scan.sh +629 -0
  32. package/scripts/fh_session_load.sh +70 -0
  33. package/scripts/portability_lint.sh +257 -0
  34. package/scripts/relay_channel.sh +70 -7
  35. package/scripts/selfcheck.sh +138 -1
  36. package/scripts/test_adapter_lanes.sh +296 -0
  37. package/scripts/test_capability_entrypoint_shipping.sh +8 -1
  38. package/scripts/test_relay_channel_lanes.sh +76 -0
  39. package/templates/.git-hooks/pre-commit +14 -2
@@ -276,6 +276,82 @@ else
276
276
  fail=1
277
277
  fi
278
278
 
279
+ # gate-anchor harness — the SAME defect the block above documents, still open six days later.
280
+ # MEASURED 2026-08-16 (weekly_audit_2026-08-16.md 🟥-2): scripts/gate_anchor_check.sh has shipped in
281
+ # package.json files[] since 2026-08-10 (b18a2bf, PR #323) with ZERO callers — not selfcheck, not
282
+ # prepublishOnly, not .git-hooks/, not .github/, not npm scripts. Its only other mention is a comment
283
+ # line in a .cap adapter, which is not a call. So the one instrument this repo owns for asking
284
+ # "is the gate green for the RIGHT REASON?" was itself never green, never red, never run.
285
+ #
286
+ # 🟥 The detector did not see it: lane_runner_check.sh reports 46/46 wired · self-test 13/13, because
287
+ # this file is neither a lane suite nor a --self-test subject — it is OUTSIDE the detector's field of
288
+ # view, exactly like directional_diff_gate on 2026-08-15, and it survived that repair.
289
+ #
290
+ # ⚠️ Do NOT use "documented in a .md" as the wiring test: publish_freshness_check.sh has no reference
291
+ # in any TRACKED doc (knowledge/, docs/) and IS wired (prepublishOnly), and it blocked a publish on
292
+ # 2026-08-16. (An earlier draft of this line said "zero .md references" flat; that is false — three
293
+ # gitignored tracks/*.md files mention it. The thesis survives, the number did not, and Axis 3 caught
294
+ # it.) Call sites are the evidence; documentation is not. The first scan for this finding also missed
295
+ # templates/.git-hooks/* because those files carry no extension — scope corrected twice.
296
+ #
297
+ # Exit contract (from the script header): 0 = every known-pair held · 1 = a pair COLLAPSED (real
298
+ # defect) · 10 = harness error OR hooks-not-installed.
299
+ #
300
+ # 🟥 10 IS TWO DIFFERENT PROPOSITIONS AND THIS BLOCK MUST SPLIT THEM. gate_anchor_check.sh reaches
301
+ # `exit 10` from FOUR places, and only ONE of them is a legitimate consumer state:
302
+ # :43 cwd/script repo mismatch → harness defect
303
+ # :99 mktemp failed → harness defect
304
+ # :133 _fixture_fail (git init/seed/remote/origin-main) → harness defect
305
+ # :339 NOT_INSTALLED > 0 → legitimate: a package-mode consumer has no hooks
306
+ # Folding all four into a quiet SKIP is fail-open: a fixture collapse in THIS repo (a git default
307
+ # -branch policy change, a restricted TMPDIR, forced commit.gpgsign) would render byte-identically to
308
+ # a consumer who simply has no hooks. That is the "unmeasured → clean" move this file legislates
309
+ # against — and worse, the --self-test loop 200 lines below already routes rc=10 to `fail=1` saying
310
+ # "it could not measure, so its verdicts prove nothing". Same file, same code, opposite verdict.
311
+ # (Adversarial review 2026-08-16 found this; the contradiction was reproduced by reading :524.)
312
+ #
313
+ # The discriminator is mechanical, not a guess: the three harness-defect paths use plain `echo
314
+ # "❌ harness-error: …"`, while the NOT-INSTALLED path goes through `say()` which `--quiet` suppresses.
315
+ # Verified 2026-08-16: running the script from an unrelated git repo with --quiet returns rc=10 AND
316
+ # prints `harness-error` to the captured stream. So harness-error-in-output → FAIL; rc=10 without it
317
+ # → genuine NOT-INSTALLED → SKIP, labelled UNMEASURED and loud.
318
+ #
319
+ # ⚠️ NAMED RESIDUAL, not closed here: in CI this lane is structurally UNMEASURED. No workflow sets
320
+ # core.hooksPath (grep: 0 hits in .github/workflows/), so validate.yml's selfcheck step always lands
321
+ # on the NOT-INSTALLED arm — and `validate` is this repo's only required status check. A permanently
322
+ # SKIPPED lane in the one required check is worse than a permanently red one, because SKIP does not
323
+ # catch the eye. Fixing it is one line in validate.yml (set core.hooksPath before the selfcheck step)
324
+ # and is deliberately NOT bundled here — see the handoff. Until then the only real measurement arm is
325
+ # the author's machine, which is the exact shape portability_lint.sh was written to condemn.
326
+ if [ ! -f scripts/gate_anchor_check.sh ]; then
327
+ _absent_subject_verdict "gate-anchor harness" "scripts/gate_anchor_check.sh" || fail=1
328
+ else
329
+ # Capture the status BEFORE any display filter — $? after a pipe is the filter's, and a failed
330
+ # check would read as 0 (the repo's own pipe-verdict guard fired on this exact shape twice while
331
+ # this block was being written).
332
+ _ga_out="$(bash scripts/gate_anchor_check.sh --quiet 2>&1)"; _ga_rc=$?
333
+ case "$_ga_rc" in
334
+ 0) echo "PASS gate-anchor harness (every known-pair held — gate is green for the right reason)" ;;
335
+ 1) echo "FAIL gate-anchor harness: a known-pair COLLAPSED — a gate is green for the wrong reason"
336
+ [ -n "$_ga_out" ] && printf '%s\n' "$_ga_out" | sed 's/^/ /'
337
+ fail=1 ;;
338
+ 10) if printf '%s' "$_ga_out" | grep -q 'harness-error'; then
339
+ echo "FAIL gate-anchor harness: the anchor could not MEASURE (harness-error, rc=10) — a"
340
+ echo " collapsed fixture is a defect in the instrument, not a consumer's missing hook."
341
+ printf '%s\n' "$_ga_out" | sed 's/^/ /'
342
+ fail=1
343
+ else
344
+ echo "SKIP gate-anchor harness: UNMEASURED (hooks not installed, rc=10)"
345
+ echo " Absence is not a pass — this run did NOT establish that the gates are sound."
346
+ [ -n "$_ga_out" ] && printf '%s\n' "$_ga_out" | sed 's/^/ /'
347
+ fi ;;
348
+ *) echo "FAIL gate-anchor harness: undeclared exit code $_ga_rc — the contract is 0/1/10, so an"
349
+ echo " unknown status is a harness defect, not a pass."
350
+ [ -n "$_ga_out" ] && printf '%s\n' "$_ga_out" | sed 's/^/ /'
351
+ fail=1 ;;
352
+ esac
353
+ fi
354
+
279
355
  # count_check's README format override is a mandatory-pass gate whose pattern is caller-supplied.
280
356
  # Same subject-present/anchor-gone shape as the block above: if the guard ships without its known
281
357
  # pair, a template that defeats the gate (a rendered newline turns the pattern into an OR search)
@@ -320,6 +396,22 @@ fi
320
396
  # how a fresh CI checkout gets approximated, so the check was absent from exactly the tree used
321
397
  # to reason about CI. scripts/test_package_coverage_lanes.sh pins the predicate across all four
322
398
  # tree shapes plus a known pair.
399
+ # ── FH 내장 어댑터 레인 ────────────────────────────────────────────────────
400
+ # 어댑터(`.claude/capabilities/adapters/*.cap` + `scripts/adapters/*.sh`)는 남의 하네스 능력을
401
+ # FH 안에서 호출하는 **기본 경로**다(운영자 결정 2026-08-16 — 남의 레포에 선언을 심지 않는다).
402
+ # 🟥 배선이 없으면 이 레인은 «만들고 배선 안 함» 이 된다 — 같은 날 `cluster_capability_scan.sh`
403
+ # 가 비재귀 글롭이라 어댑터를 통째로 못 보던 것을 이 세션이 실측으로 잡았고, 그 회귀를 막는
404
+ # 레인(L16/L16b)이 그 파일 self-test 안에 있다. 어댑터 자체의 레인은 여기서 돈다.
405
+ if [ ! -f scripts/adapters/peer_resolve.sh ]; then
406
+ _absent_subject_verdict "test_adapter_lanes.sh" "scripts/adapters/peer_resolve.sh" || fail=1
407
+ elif [ -f scripts/test_adapter_lanes.sh ]; then
408
+ if ! bash scripts/test_adapter_lanes.sh; then
409
+ fail=1
410
+ fi
411
+ else
412
+ _absent_subject_verdict "test_adapter_lanes.sh" "scripts/test_adapter_lanes.sh" || fail=1
413
+ fi
414
+
323
415
  if [ ! -f scripts/package_coverage_check.sh ]; then
324
416
  _absent_subject_verdict "test_package_coverage_lanes.sh" "scripts/package_coverage_check.sh" || fail=1
325
417
  elif [ -f scripts/test_package_coverage_lanes.sh ]; then
@@ -489,7 +581,16 @@ done
489
581
  # and capability_registry_check does not: its terminal line is a genuine VERDICT
490
582
  # ("publish_freshness 캘리브레이션 통과/실패"), not a Korean test-case title, so a future rename of
491
583
  # any single lane cannot flip a real PASS into a false "dispatcher missing?" here.
492
- for _subj in compaction_probe judgment_circuit_lint novelty_claim_check chamber_witness digest_landing_check publish_freshness_check; do
584
+ # portability_lint joined 2026-08-16 (weekly_audit 🟧-3). Same qualification as publish_freshness_check:
585
+ # its terminal line is a genuine verdict ("portability-lint 캘리브레이션 통과/실패"), emitted once in
586
+ # the summary position, not a Korean test-case title — so renaming any single lane cannot flip a real
587
+ # PASS into a false "dispatcher missing?" here. It earns the slot for a second reason: its own
588
+ # self-test caught THREE defects in it before it ever shipped (a `|` field separator colliding with
589
+ # regex alternation, a line-window too narrow to see the next-line guard, and a rule that flagged its
590
+ # own prescription), and a hand-check then removed two false-positive classes that had inflated its
591
+ # first repo-wide count by 40% (50 → 30). An instrument that has never been able to fail is not
592
+ # calibrated; this one has failed, been fixed, and still discriminates (10 pass / 0 fail).
593
+ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check chamber_witness digest_landing_check publish_freshness_check portability_lint; do
493
594
  if [ ! -f "scripts/$_subj.sh" ]; then
494
595
  _absent_subject_verdict "$_subj --self-test" "scripts/$_subj.sh" || fail=1
495
596
  else
@@ -528,6 +629,24 @@ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check chamber_
528
629
  echo " so its verdicts prove nothing about $_subj. Not a lane failure, not a pass either."
529
630
  fail=1; continue ;;
530
631
  esac
632
+ # 🟥 portability_lint 만 **전체 줄 + 비영 카운트**를 요구한다 (적대검증 2026-08-16).
633
+ # 아래 substring 게이트는 «`캘리브레이션` 이라는 글자가 출력 어딘가에 있나» 만 본다. 그래서
634
+ # 그 스크립트의 `RULES` 를 통째로 비우고 `_ck` 호출을 전부 지워도
635
+ # `portability-lint 캘리브레이션 통과: 0 pass / 0 fail` 이 나오고 rc=0 → **PASS** 다.
636
+ # 이건 이 파일이 directional_diff_gate 에 대해 이미 명시적으로 방어한 결함
637
+ # (*"a suite with nothing left in it certifies itself"*)이고, 새 subject 만 약한 게이트에
638
+ # 들어갔다. 저자의 되돌림 프로브(«디스패처 제거 → FAIL»)는 **디스패처 존재**만 입증하고
639
+ # **레인 생존**은 입증하지 않는다 — 더 강한 뮤턴트(레인 삭제)는 안 돌았고, 돌았으면 초록이었다.
640
+ if [ "$_subj" = portability_lint ]; then
641
+ case "$_st_out" in
642
+ *"portability-lint 캘리브레이션 통과: "[1-9]*" pass / 0 fail"*) : ;;
643
+ *) echo "FAIL $_subj: --self-test 가 «비영 pass + 0 fail» 종단 verdict 를 내지 않았다"
644
+ echo " (레인이 전멸해도 substring 게이트는 통과한다 — 그래서 여기만 전체 줄을 본다)"
645
+ printf '%s\n' "$_st_out" | tail -3 | sed 's/^/ /'
646
+ fail=1; _st_rc=0 ;;
647
+ esac
648
+ continue
649
+ fi
531
650
  case "$_st_out" in
532
651
  *캘리브레이션*) : ;;
533
652
  *) echo "FAIL $_subj: --self-test produced no calibration verdict (dispatcher missing?)"
@@ -557,6 +676,24 @@ else
557
676
  fail=1
558
677
  fi
559
678
 
679
+ # cluster_capability_scan — wired 2026-08-16 (정체성 ①-(a) cluster-wizard).
680
+ # 종단 판정줄이 `… 캘리브레이션 통과/실패: N PASS / M FAIL` 이라 위 `_subj` 루프의
681
+ # 캘리브레이션 게이트를 **진짜 판정줄로** 만족한다 — 테스트 케이스 제목이 아니다.
682
+ # 그래서 이 subject 는 그 루프에 넣어도 안전하지만, 루프는 `scripts/<name>.sh` 존재를
683
+ # 전제로 SKIP/FAIL 을 가르므로 여기 직접 디스패치로 둔다(다른 두 capability 검사기와 같은 형태).
684
+ # 🟥 exit code 만 보지 않는다 — 레인이 전멸해도 0 이 나온다.
685
+ if [ ! -f scripts/cluster_capability_scan.sh ]; then
686
+ echo "FAIL cluster_capability_scan.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
687
+ fail=1
688
+ elif _out=$(bash scripts/cluster_capability_scan.sh --self-test < /dev/null 2>&1) \
689
+ && printf '%s\n' "$_out" | grep -qE '캘리브레이션 통과: [1-9][0-9]* PASS / 0 FAIL'; then
690
+ echo "PASS cluster_capability_scan.sh --self-test ($(printf '%s\n' "$_out" | grep -oE '[0-9]+ PASS / [0-9]+ FAIL' | tail -1))"
691
+ else
692
+ echo "FAIL cluster_capability_scan.sh: --self-test failed or produced no terminal verdict line"
693
+ _show_failure "$_out"
694
+ fail=1
695
+ fi
696
+
560
697
  # capability_effect_probe — wired 2026-08-16. `lane_runner_check.sh` had been flagging it as an
561
698
  # embedded --self-test subject with ZERO dispatchers, and that warning was load-bearing: the same
562
699
  # day, two SECURITY regression anchors were added to this suite (L8 args injection · L9 the git
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env bash
2
+ # test_adapter_lanes.sh — `scripts/adapters/*` (크로스하네스 **어댑터**)의 회귀 레인.
3
+ #
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # 무엇을 고정하나
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # 등록 바(`capability_registry_check.sh`)는 **선언된 캘리브레이션 arm 두 개만** 돌리고,
8
+ # 자기 출력에 «나머지 enum 값은 관측하지 않았다» 고 적는다. 어댑터에서 그 «나머지» 는
9
+ # 하필 가장 중요한 값이다 — **`PEER_ABSENT`**. 「그 하네스를 이 머신에 안 깔았다」가
10
+ # `CLEAN` 으로 접히면 미측정이 통과로 렌더되고, `HARNESS_ERROR` 로 접히면 멀쩡한 이웃이
11
+ # 고장으로 렌더된다. 이 레인 묶음이 그 값과 peer 해석 규칙을 고정한다.
12
+ #
13
+ # 레인 클래스 (되돌림 프로브가 이 태그로 «어느 레인이 빨개져야 하는가» 를 사전등록한다)
14
+ # resolve-absent peer 가 안 보일 때 **전용 값**이 나오는가
15
+ # resolve-alias 별칭 규칙(`<n>-dev` · `_`→`-`)이 실제로 도는가
16
+ # resolve-guard 모호/부분일치를 **고르지 않는가**
17
+ # control peer 가 보이면 **실제 판정**이 나오는가
18
+ # (「늘 PEER_ABSENT 를 찍는 계기」와 구분하는 자리)
19
+ # enum M4 가 관측하지 않는 나머지 enum 값들
20
+ #
21
+ # 사용법
22
+ # bash scripts/test_adapter_lanes.sh # 레인 전부
23
+ # bash scripts/test_adapter_lanes.sh --revert-probe # 레인 + 되돌림 프로브 2종
24
+ # exit: 0 = 전부 기대대로 · 1 = 회귀 · 10 = 하네스 오류(전제 파손 — 판정 아님)
25
+
26
+ set -uo pipefail
27
+
28
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
29
+ # 되돌림 프로브가 **중화된 사본**을 가리키게 하려고 파라미터화한다. 기본값은 실물이다.
30
+ ADAPTER_DIR="${FH_ADAPTER_DIR:-$REPO_ROOT/scripts/adapters}"
31
+
32
+ T="$(mktemp -d)" || { echo "❌ HARNESS-ERROR — mktemp 실패"; exit 10; }
33
+ trap 'rm -rf "$T"' EXIT
34
+
35
+ FAIL=0; N=0
36
+ RED_LANES="" # 이번 실행에서 빨개진 레인 ID (되돌림 프로브가 읽는다)
37
+
38
+ # ── peer 가 이 머신에 **있어야만** 의미가 있는 레인 ──────────────────────────
39
+ # 🟥 실측(2026-08-16): 로컬 23/23 초록인데 **CI 는 9레인 적색**이었다(G2 G3 G6 G9 M2 M3
40
+ # Q2 Q3 Q6). 이 레인들이 루트 오버라이드 없이 어댑터를 불러 `$HOME/projects` 를 보는데,
41
+ # CI 체크아웃엔 peer 레포가 없어 전부 `PEER_ABSENT(20)` 이 나왔다. 즉 레인의 앵커가
42
+ # **운영자 머신의 레이아웃**에 걸려 있었다 — 같은 날 `L13c` 가 gitignored 데이터에 앵커를
43
+ # 걸어 CI 만 빨갛던 것과 **같은 클래스**다([[feedback_ci_measures_state_not_transition]]).
44
+ #
45
+ # 🟥 그리고 이 스킵은 **조용하면 안 된다.** peer 가 없는 환경에서 컨트롤이 전부 빠지면
46
+ # 남는 것은 PEER_ABSENT 레인뿐이고, 그건 「늘 PEER_ABSENT 를 내는 계기」와 **구분되지 않는다**
47
+ # — 컨트롤이 막으려던 바로 그 상태다. 그래서 스킵 수를 세어 **종결 줄에 반드시 찍는다.**
48
+ SKIPPED=0
49
+ _lane_peer() { # $1=peer_root $2=id $3=class $4=설명 $5=expected $6=actual
50
+ if [ -n "$1" ]; then
51
+ _lane "$2" "$3" "$4" "$5" "$6"
52
+ else
53
+ SKIPPED=$((SKIPPED + 1))
54
+ printf ' ⏭ %-6s [%-14s] %s — peer 부재로 SKIP (**통과 아님**)\n' "$2" "$3" "$4"
55
+ fi
56
+ }
57
+
58
+ _lane() { # $1=id $2=class $3=설명 $4=expected_rc $5=actual_rc
59
+ N=$((N + 1))
60
+ if [ "$4" = "$5" ]; then
61
+ printf ' ✅ %-6s [%-14s] %s (rc=%s)\n' "$1" "$2" "$3" "$5"
62
+ else
63
+ printf ' ❌ %-6s [%-14s] %s — expect rc=%s, got rc=%s\n' "$1" "$2" "$3" "$4" "$5"
64
+ FAIL=1; RED_LANES="$RED_LANES $1"
65
+ fi
66
+ }
67
+
68
+ # 계기 전제 — 어댑터 3종이 **실제로 거기 있는지** 먼저 본다. 없으면 이 레인 묶음은
69
+ # «전부 초록» 이 아니라 하네스 오류다(빈 대상에 대고 초록을 내는 것이 여기서 막으려는 형태).
70
+ for a in gstack_content_safety mate_agent_boundary qasp_new_code_anchor peer_resolve; do
71
+ [ -r "$ADAPTER_DIR/$a.sh" ] || { echo "❌ HARNESS-ERROR — $ADAPTER_DIR/$a.sh 부재. 빈 대상에 초록을 내지 않는다"; exit 10; }
72
+ done
73
+
74
+ G="$ADAPTER_DIR/gstack_content_safety.sh"
75
+ M="$ADAPTER_DIR/mate_agent_boundary.sh"
76
+ Q="$ADAPTER_DIR/qasp_new_code_anchor.sh"
77
+
78
+ # 「peer 가 안 보이는 세계」 = 잘 형성된 루트 목록인데 그 안에 이 peer 가 없는 것.
79
+ # (빈 **문자열**이 아니다 — 그건 「어디를 볼지 자체가 없다」는 다른 명제이고 전제 파손이다.)
80
+ EMPTY_WORLD="$T/empty_world"; mkdir -p "$EMPTY_WORLD"
81
+
82
+ _rc() { "$@" >/dev/null 2>&1; printf '%s' "$?"; } # 판정은 파이프를 통과시키지 않는다
83
+
84
+ # 실물 peer 루트 — 있으면 alias/control 레인을 돌리고, 없으면 그 레인은 SKIP 으로 **말한다**.
85
+ PEER_HOME="${FH_PROJECTS_HOME:-$HOME/projects}"
86
+ _peer_root() { # $1=이름 → 실물 루트 or 빈 문자열 (별칭 순서는 어댑터와 같다)
87
+ local n="$1" c
88
+ for c in "$n" "$n-dev" "$(printf '%s' "$n" | tr '_' '-')"; do
89
+ [ -d "$PEER_HOME/$c" ] && { printf '%s' "$PEER_HOME/$c"; return; }
90
+ done
91
+ printf ''
92
+ }
93
+ GS_ROOT="$(_peer_root gstack)"; MT_ROOT="$(_peer_root mate)"; QS_ROOT="$(_peer_root qasp)"
94
+
95
+ echo "== 어댑터 레인 (대상: $ADAPTER_DIR) =="
96
+ echo
97
+
98
+ # ═══════════════════════════════════════════════════════════════════════════
99
+ # ① gstack-content-safety
100
+ # ═══════════════════════════════════════════════════════════════════════════
101
+ echo "── gstack-content-safety (enum 0=CLEAN 1=BLOCK_HIGH 2=REVIEW_MEDIUM 3=NO_TARGET 10=HARNESS_ERROR 20=PEER_ABSENT)"
102
+ _lane G1 resolve-absent "peer 불가시 → 전용값 PEER_ABSENT(CLEAN 도 HARNESS_ERROR 도 아니다)" 20 \
103
+ "$(FH_CLUSTER_ROOTS="$EMPTY_WORLD" _rc bash "$G" --known-positive)"
104
+ _lane_peer "$GS_ROOT" G2 control "peer 가시 → 실제 판정 BLOCK_HIGH («늘 PEER_ABSENT» 와 구분)" 1 \
105
+ "$(_rc bash "$G" --known-positive)"
106
+ _lane_peer "$GS_ROOT" G3 control "peer 가시 → 음성 arm 은 CLEAN (계기가 «가른다» 는 증거)" 0 \
107
+ "$(_rc bash "$G" --known-negative)"
108
+ if [ -n "$GS_ROOT" ]; then
109
+ _lane G4 resolve-alias "명시 루트 목록(FH_CLUSTER_ROOTS)으로 해석된다" 0 \
110
+ "$(FH_CLUSTER_ROOTS="$GS_ROOT" _rc bash "$G" --known-negative)"
111
+ else
112
+ echo " ⏭ G4 [resolve-alias ] gstack 실물 루트 없음 — SKIP (통과 아님)"
113
+ fi
114
+ # 부분일치로 남의 레포를 집지 않는다. basename 이 **정확히** 별칭 후보여야 한다.
115
+ mkdir -p "$T/world_prefix/gstackx"
116
+ _lane G5 resolve-guard "basename 부분일치는 peer 가 아니다(gstackx ≠ gstack)" 20 \
117
+ "$(FH_CLUSTER_ROOTS="$T/world_prefix/gstackx" _rc bash "$G" --known-positive)"
118
+ mkdir -p "$T/empty_target"
119
+ _lane_peer "$GS_ROOT" G6 enum "빈 디렉토리 → NO_TARGET(3). CLEAN 이 아니다 — 아무것도 안 쟀다" 3 \
120
+ "$(_rc bash "$G" --target "$T/empty_target")"
121
+ _lane_peer "$GS_ROOT" G7 enum "없는 대상 → HARNESS_ERROR(10). 부재 대상은 빈 대상이 아니다" 10 \
122
+ "$(_rc bash "$G" --target "$T/does_not_exist")"
123
+ _lane_peer "$GS_ROOT" G8 enum "인자 없음 → HARNESS_ERROR(10). 안 돈 실행을 판정으로 렌더하지 않는다" 10 \
124
+ "$(_rc bash "$G")"
125
+ # MEDIUM 만 — 자동 차단이 아니라 사람 판단. 리터럴은 이 소스가 자기 스캐너에 물리지 않게 쪼갠다.
126
+ printf 'payroll note\ncontact ssn %s-%s-%s for the record\n' '123' '45' '6789' > "$T/medium_only.txt"
127
+ _lane_peer "$GS_ROOT" G9 enum "MEDIUM 만 → REVIEW_MEDIUM(2). CLEAN 과도 BLOCK 과도 다른 값이다" 2 \
128
+ "$(_rc bash "$G" --target "$T/medium_only.txt")"
129
+ echo
130
+
131
+ # ═══════════════════════════════════════════════════════════════════════════
132
+ # ② mate-agent-boundary
133
+ # ═══════════════════════════════════════════════════════════════════════════
134
+ echo "── mate-agent-boundary (enum 0=PASS 1=FAIL 10=HARNESS_ERROR 20=PEER_ABSENT)"
135
+ _lane M1 resolve-absent "peer 불가시 → 전용값 PEER_ABSENT(PASS 로도 HARNESS_ERROR 로도 안 접힌다)" 20 \
136
+ "$(FH_CLUSTER_ROOTS="$EMPTY_WORLD" _rc bash "$M" --known-positive)"
137
+ _lane_peer "$MT_ROOT" M2 control "peer 가시 → 실제 판정 PASS" 0 "$(_rc bash "$M" --known-positive)"
138
+ _lane_peer "$MT_ROOT" M3 control "peer 가시 → 음성 arm 은 FAIL (판별력의 증거)" 1 "$(_rc bash "$M" --known-negative)"
139
+ if [ -n "$MT_ROOT" ]; then
140
+ # 🟥 별칭 레인의 핵심: 이름은 `mate` 인데 실물 디렉토리는 `mate-dev` 다.
141
+ # 별칭 후보에서 `<n>-dev` 가 빠지면 이 레인이 즉시 PEER_ABSENT 로 빨개진다.
142
+ _lane M4 resolve-alias "이름 mate → 실물 $(basename "$MT_ROOT") 로 별칭 해석된다" 0 \
143
+ "$(FH_CLUSTER_ROOTS="$MT_ROOT" _rc bash "$M" --known-positive)"
144
+ else
145
+ echo " ⏭ M4 [resolve-alias ] mate 실물 루트 없음 — SKIP (통과 아님)"
146
+ fi
147
+ _lane_peer "$MT_ROOT" M5 enum "없는 대상 → HARNESS_ERROR(10). peer 헤더가 «위반 0 이 아니다» 라고 못 박은 값" 10 \
148
+ "$(_rc bash "$M" --target "$T/does_not_exist.md")"
149
+ _lane_peer "$MT_ROOT" M6 enum "알 수 없는 모드 → HARNESS_ERROR(10)" 10 "$(_rc bash "$M" --bogus-mode)"
150
+ echo
151
+
152
+ # ═══════════════════════════════════════════════════════════════════════════
153
+ # ③ qasp-new-code-anchor
154
+ # ═══════════════════════════════════════════════════════════════════════════
155
+ echo "── qasp-new-code-anchor (enum 0=PASS 1=VIOLATION 2=ARGS 3=HARNESS_ERROR 4=OUT_OF_SCOPE 20=PEER_ABSENT)"
156
+ _lane Q1 resolve-absent "peer 불가시 → 전용값 PEER_ABSENT(PASS 로도 HARNESS_ERROR 로도 안 접힌다)" 20 \
157
+ "$(FH_CLUSTER_ROOTS="$EMPTY_WORLD" _rc bash "$Q" --known-positive)"
158
+ _lane_peer "$QS_ROOT" Q2 control "peer 가시 → 실제 판정 VIOLATION" 1 "$(_rc bash "$Q" --known-positive)"
159
+ _lane_peer "$QS_ROOT" Q3 control "peer 가시 → 음성 arm 은 PASS (판별력의 증거)" 0 "$(_rc bash "$Q" --known-negative)"
160
+ if [ -n "$QS_ROOT" ]; then
161
+ _lane Q4 resolve-alias "이름 qasp → 실물 $(basename "$QS_ROOT") 로 별칭 해석된다" 0 \
162
+ "$(FH_CLUSTER_ROOTS="$QS_ROOT" _rc bash "$Q" --known-negative)"
163
+ # 🟥 모호 가드 — 한 이름에 레포 둘. **고르지 않는다.** 조용히 하나를 고르면 «어느
164
+ # 하네스를 쟀는지» 아무도 모르게 된다. 판정 불가는 PASS 가 아니므로 HARNESS_ERROR.
165
+ mkdir -p "$T/ambig/qasp" "$T/ambig/qasp-dev"
166
+ _lane Q5 resolve-guard "한 이름에 레포 둘 → 고르지 않고 HARNESS_ERROR(3)" 3 \
167
+ "$(FH_CLUSTER_ROOTS="$T/ambig/qasp:$T/ambig/qasp-dev" _rc bash "$Q" --known-positive)"
168
+ else
169
+ echo " ⏭ Q4/Q5 [resolve-* ] qasp 실물 루트 없음 — SKIP (통과 아님)"
170
+ fi
171
+ _lane_peer "$QS_ROOT" Q6 enum "--range 에 base 없음 → ARGS(2). 판정이 아니다" 2 "$(_rc bash "$Q" --range)"
172
+
173
+ # ── 합성 peer — peer 히스토리를 만지지 않고 나머지 enum 을 덮는다 ────────────
174
+ # 🟥 왜 합성 peer 인가: peer 는 `residency: company` 다. 실물 히스토리를 레인으로 쓰면
175
+ # 레인 출력에 조직 경로가 실린다. 그리고 실물 히스토리는 남의 커밋에 따라 조용히
176
+ # 다른 것을 재게 된다(감사 대상은 얼려야 한다). 합성 peer 는 **peer 의 실제 진입점
177
+ # 스크립트를 복사해** 쓰므로, 재는 것은 여전히 «어댑터가 peer 의 exit 을 옳게
178
+ # 재매핑하는가» 다 — 어댑터의 매핑이지 peer 의 히스토리가 아니다.
179
+ if [ -n "$QS_ROOT" ] && [ -r "$QS_ROOT/scripts/new_code_anchor_scan.sh" ]; then
180
+ SYN="$T/synworld/qasp-dev"; mkdir -p "$SYN/scripts" "$SYN/src" "$SYN/tests"
181
+ cp "$QS_ROOT/scripts/new_code_anchor_scan.sh" "$SYN/scripts/" || { echo "❌ HARNESS-ERROR — peer 진입점 복사 실패"; exit 10; }
182
+ GIT=(git -c user.email=lane@localhost -c user.name=lane -c commit.gpgsign=false)
183
+ ( cd "$SYN" && "${GIT[@]}" init -q . && printf 'base\n' > README.md \
184
+ && "${GIT[@]}" add -A && "${GIT[@]}" commit -qm base ) >/dev/null 2>&1 \
185
+ || { echo "❌ HARNESS-ERROR — 합성 peer 레포 생성 실패"; exit 10; }
186
+ SYN_BASE="$(cd "$SYN" && git rev-parse HEAD)"
187
+ # 범위 밖 언어의 신규 실행 코드만 넣는다 → 「검사되지 않았다」 ≠ 「깨끗하다」
188
+ ( cd "$SYN" && printf 'export const f = (a) => a + 1;\n' > src/newthing.ts \
189
+ && "${GIT[@]}" add -A && "${GIT[@]}" commit -qm oos ) >/dev/null 2>&1
190
+ _lane Q7 enum "범위 밖 언어의 신규 실행 코드 → OUT_OF_SCOPE(4). PASS 로 접히지 않는다" 4 \
191
+ "$(FH_CLUSTER_ROOTS="$SYN" _rc bash "$Q" --range "$SYN_BASE" HEAD)"
192
+ _lane Q8 enum "해석 불가 ref → HARNESS_ERROR(3). 판정이 아니다" 3 \
193
+ "$(FH_CLUSTER_ROOTS="$SYN" _rc bash "$Q" --range no-such-ref-xyz HEAD)"
194
+ else
195
+ echo " ⏭ Q7/Q8 [enum ] qasp 진입점 도달 불가 — SKIP (통과 아님)"
196
+ fi
197
+ echo
198
+
199
+ # ═══════════════════════════════════════════════════════════════════════════
200
+ # 되돌림 프로브 — 앵커가 장식이 아님을 «되돌려 보고» 잰다 (적용확인 → 실행 → 복원)
201
+ # ═══════════════════════════════════════════════════════════════════════════
202
+ # 레인이 초록인 것과 레인에 **판별력이 있는 것**은 다른 명제다. 여기서는 peer 해석부를
203
+ # 두 방향으로 중화하고, **사전등록한 레인 집합만** 빨개지는지 본다.
204
+ # R1 «부재 분기» 중화 → 부재를 부재로 못 낸다 ⇒ resolve-absent + resolve-guard(부분일치) 빨강
205
+ # R2 «별칭 목록» 중화 → `-dev` 를 못 찾는다 ⇒ mate/qasp 의 alias·control·enum 레인만 빨강
206
+ #
207
+ # 🟥 **초판 사전등록이 두 군데 틀렸고, 프로브가 그걸 잡았다**(2026-08-16). 적어 두는 이유는
208
+ # 이게 프로브에 판별력이 있다는 증거이기 때문이다 — 사전등록을 관측에 맞춰 조용히 고치면
209
+ # 그 증거가 사라진다. 두 차이는 **레인 결함이 아니라 사전등록 오류**였고, 기계적으로 설명된다:
210
+ # · R1 에 **G5** 가 추가로 빨강: G5 는 `gstackx` 루트를 준다. 중화판은 부재 분기 대신
211
+ # `$PROJECTS_HOME/gstack` 를 내는데 **그 경로는 실재한다** → 어댑터가 실제로 돌아
212
+ # BLOCK_HIGH(1) 을 낸다. G5 도 부재 분기에 결박된 레인이므로 빨강이 맞다.
213
+ # · R2 에서 **Q5 는 초록**: Q5 는 `qasp` 와 `qasp-dev` 둘을 주는 모호 레인이다. 별칭
214
+ # 목록이 `$1` 하나로 줄면 `qasp` 만 맞아 **모호가 아니게 되고**, 어댑터는 진입점
215
+ # 부재로 HARNESS_ERROR(3) 를 낸다 — 기대값과 같은 값이다. 즉 R2 는 Q5 를 원리적으로
216
+ # 구분하지 못한다. 그건 Q5 의 결함이 아니라 **이 프로브 축의 사각지대**이고,
217
+ # Q5 는 R1 이 아니라 별도 축(모호 판정 자체를 지우는 중화)으로만 흔들 수 있다.
218
+ # ★중화는 **실물 파일이 아니라 사본**에 건다(FH_ADAPTER_DIR). 실물 트리를 건드리지 않고,
219
+ # 사본이 실물과 «중화한 줄 말고는 동일» 함을 적용확인 단계에서 대조한다.
220
+ _revert_probe() {
221
+ local name="$1" sedexpr="$2" applied_marker="$3" expect_red="$4" desc="$5"
222
+ local C="$T/neutralized_$name"
223
+ rm -rf "$C"; mkdir -p "$C"
224
+ cp -R "$REPO_ROOT/scripts/adapters/." "$C/" || { echo "❌ HARNESS-ERROR — 사본 생성 실패"; return 10; }
225
+ # ── 1단 적용확인: 중화가 **실제로 들어갔는가**. 안 들어간 채 «초록» 을 보고
226
+ # «레인이 튼튼하다» 고 읽는 것이 이 프로브의 최대 실패모드다.
227
+ sed -i.bak "$sedexpr" "$C/peer_resolve.sh" && rm -f "$C/peer_resolve.sh.bak"
228
+ if ! grep -q "$applied_marker" "$C/peer_resolve.sh"; then
229
+ echo " ❌ $name — 중화가 적용되지 않았다(마커 «$applied_marker» 부재). 프로브를 신뢰할 수 없다"
230
+ return 1
231
+ fi
232
+ # 중화한 파일 말고는 사본이 실물과 동일해야 한다(다른 변수를 안 건드렸다는 확인).
233
+ local other_diff
234
+ other_diff="$(diff -rq "$REPO_ROOT/scripts/adapters" "$C" 2>/dev/null | grep -v 'peer_resolve.sh' | wc -l | tr -d ' ')"
235
+ if [ "$other_diff" != "0" ]; then
236
+ echo " ❌ $name — 사본이 peer_resolve.sh 외에도 다르다($other_diff건). 프로브 격리 실패"
237
+ return 1
238
+ fi
239
+ # ── 2단 실행: 사본을 대상으로 레인을 통째로 다시 돌린다.
240
+ local out red
241
+ out="$(FH_ADAPTER_DIR="$C" bash "${BASH_SOURCE[0]}" --lanes-only 2>&1)"
242
+ red="$(printf '%s\n' "$out" | sed -n 's/^ ❌ \([A-Z][0-9]*\) .*/\1/p' | sort | tr '\n' ' ' | sed 's/ $//')"
243
+ # ── 3단 복원: 사본을 지운다. 실물은 애초에 안 건드렸다.
244
+ rm -rf "$C"
245
+ printf ' %s — %s\n' "$name" "$desc"
246
+ printf ' 사전등록 빨강: [%s]\n' "$expect_red"
247
+ printf ' 실측 빨강: [%s]\n' "$red"
248
+ if [ "$red" = "$expect_red" ]; then
249
+ printf ' ✅ 정확히 그 레인만 빨개졌다 — 이 레인들은 peer 해석부에 실제로 결박돼 있다\n'
250
+ return 0
251
+ fi
252
+ printf ' ❌ 사전등록과 다르다 — 레인이 장식이거나 사전등록이 틀렸다. 둘 다 결함이다\n'
253
+ return 1
254
+ }
255
+
256
+ case "${1:-}" in
257
+ --lanes-only)
258
+ # 되돌림 프로브가 자기를 재귀 호출할 때 쓰는 모드. 프로브를 다시 돌리지 않는다.
259
+ ;;
260
+ --revert-probe)
261
+ echo "== 되돌림 프로브 (적용확인 → 실행 → 복원) =="
262
+ RP=0
263
+ # R1: 「없다」를 못 내게 한다 — 부재 분기를 순진한 추측 경로로 바꾼다.
264
+ _revert_probe R1 \
265
+ 's|^ printf .peer-resolve: peer| printf "%s" "${FH_PROJECTS_HOME:-$HOME/projects}/$n"; return 0 # NEUTRALIZED_R1\n printf '"'"'peer-resolve: peer|' \
266
+ 'NEUTRALIZED_R1' \
267
+ 'G1 G5 M1 Q1' \
268
+ '«부재» 분기 중화 → 부재 분기에 결박된 레인만 빨개져야 한다' || RP=1
269
+ echo
270
+ # R2: 별칭 후보를 이름 자신 하나로 줄인다 — `-dev` 를 못 찾게 된다.
271
+ _revert_probe R2 \
272
+ 's|^ printf .%s\\n. "\$1" "\$1-dev".*$| printf "%s\\n" "$1" # NEUTRALIZED_R2|' \
273
+ 'NEUTRALIZED_R2' \
274
+ 'M2 M3 M4 M5 M6 Q2 Q3 Q4 Q6 Q7 Q8' \
275
+ '별칭 목록 중화 → mate/qasp 쪽 레인만 빨개져야 한다(gstack 은 별칭이 필요 없다)' || RP=1
276
+ echo
277
+ [ "$RP" -eq 0 ] || FAIL=1
278
+ ;;
279
+ '') ;;
280
+ *) echo "usage: $(basename "$0") [--revert-probe]" >&2; exit 10 ;;
281
+ esac
282
+
283
+ echo
284
+ if [ "$FAIL" -eq 0 ]; then
285
+ # ★`${N}` 로 감싼다 — 한글이 바로 뒤에 오면 bash 가 `$N개` 를 **변수명 `N개`** 로 읽어
286
+ # `unbound variable` 로 죽는다(실측 2026-08-16: 레인 23개 전부 초록인데 마지막 요약
287
+ # 줄에서 rc=1). 계기가 자기 보고 줄에서 죽으면 초록이 빨강으로 렌더된다.
288
+ echo "✅ 레인 ${N}개 전부 기대대로"
289
+ if [ "$SKIPPED" -gt 0 ]; then
290
+ echo "⏭ 단, peer 부재로 **${SKIPPED}개 레인이 안 돌았다** — 그중 컨트롤이 빠지면"
291
+ echo " 남은 초록은 「늘 PEER_ABSENT 를 내는 계기」와 구분되지 않는다. 초록을 그렇게 읽지 마라."
292
+ fi
293
+ else
294
+ echo "❌ 회귀 — 빨강:$RED_LANES (레인 ${N}개 실행 · peer 부재 SKIP ${SKIPPED}개)"
295
+ fi
296
+ exit "$FAIL"
@@ -41,7 +41,14 @@ command -v node >/dev/null 2>&1 || {
41
41
  # Discover entry points by convention. A DISCOVERY of zero is an instrument failure, not a clean
42
42
  # result: this check exists precisely because the surface it guards is invisible to reference
43
43
  # walking, so "found nothing to check" must never render as "everything ships".
44
- ENTRIES=$(find scripts -maxdepth 1 -type f -name '*_capability.sh' 2>/dev/null | sort)
44
+ # 🟥 2026-08-16: 발견 규약이 **한 벌뿐**이었다(`scripts/*_capability.sh`, maxdepth 1). 같은 날
45
+ # 도입된 **어댑터 진입점**(`scripts/adapters/*.sh` — 남의 하네스 능력을 FH 안에서 부르는 기본
46
+ # 경로)은 이름도 위치도 그 규약 밖이라 **이 검사에 구조적으로 안 보였다.** 즉 어댑터 스크립트가
47
+ # `files[]` 에서 빠져도 이 검사는 초록이었다 — 검사기가 지키려던 바로 그 결함이 검사기 자신의
48
+ # 사각에서 재현될 수 있었다. 두 규약을 **합집합**으로 본다(빼지 말고 더한다).
49
+ ENTRIES=$( { find scripts -maxdepth 1 -type f -name '*_capability.sh' 2>/dev/null
50
+ find scripts/adapters -maxdepth 1 -type f -name '*.sh' 2>/dev/null
51
+ } | LC_ALL=C sort -u )
45
52
  n=$(printf '%s\n' "$ENTRIES" | grep -c . || true)
46
53
  n=$(( ${n:-0} + 0 ))
47
54
  if [ "$n" -eq 0 ]; then
@@ -578,6 +578,82 @@ A_N10=$(cap a_n10.cap "id: demo:a" "entry: bash $T/n_key_pass.sh" "verdict_chann
578
578
  rc=0; bash "$RELAY" run --cap "$A_N10" --cap "$B" --task t > "$T/on10" 2>&1 || rc=$?
579
579
  _t "N10 both 채널의 **일치** 정상 경로가 실제로 돈다(과차단 아님)" 0 "$rc"
580
580
 
581
+ # ── N11–N15 노드별 호출 인자 채널 (`--cap-args`) ─────────────────────────────
582
+ # 🟥 이 채널이 없던 동안 **호출 시점 인자를 요구하는 능력은 relay 를 통과할 수 없었다**
583
+ # (실측 2026-08-16, pmh-dev:merge-noop-check → 항상 ARGS→HARNESS_ERROR→BLOCKED).
584
+ # fail-closed 라 방향은 옳았지만 신호가 0 이었고, 그건 «호출 가능» 을 구조적으로 못 만든다.
585
+ # 레인은 네 가지를 따로 단언한다 — 도달 · 결박 대상 · fail-closed · **비주입 컨트롤**.
586
+ mk n_echo_args.sh '#!/usr/bin/env bash
587
+ # 받은 위치 인자를 그대로 찍고, 첫 인자가 "GO" 일 때만 통과한다.
588
+ echo "ARGS=[$*]"
589
+ [ "${1:-}" = "GO" ] && exit 0
590
+ exit 1'
591
+ ARGSCAP() { cap "$1" "id: demo:$2" "entry: bash $T/n_echo_args.sh" "verdict_channel: exit" \
592
+ "verdict_enum: 0=PASS 1=FAIL" "approval: auto" "reversibility: reversible" "residency: public" \
593
+ "degrade: fail-closed" "tier_floor: none" "writes: read-only" "judge: mechanical" \
594
+ "verdict_binding: FAIL"; }
595
+
596
+ # N11 인자가 실제로 진입점에 **도달한다** — 없으면 FAIL 이어야 할 노드가 통과한다
597
+ A_N11=$(ARGSCAP a_n11.cap a)
598
+ rc=0; bash "$RELAY" run --cap "$A_N11" --cap-args "GO" --cap "$B" --task t \
599
+ --record "$T/rec11" > "$T/on11" 2>&1 || rc=$?
600
+ _t "N11 --cap-args 가 진입점에 실제로 도달한다" 0 "$rc"
601
+ # ★ 단언 대상은 relay 의 stdout 이 아니라 **노드 자신의 출력**이다. relay 는 노드 stdout 을
602
+ # 기록 디렉터리로 보내므로, relay 출력에 대고 grep 하면 늘 «없음» 이 나와 거짓 적색이 된다
603
+ # (초판이 그렇게 틀렸다 — 계기가 아니라 단언 위치가 틀린 경우).
604
+ _tout "N11b 받은 인자를 진입점이 그대로 본다" "ARGS=\[GO\]" "$T/rec11/node1.out"
605
+
606
+ # N12 🟥 **비주입 컨트롤** — 같은 capfile 에 인자만 빼면 FAIL 이어야 한다.
607
+ # 이게 없으면 N11 은 «인자와 무관하게 늘 통과하는 계기» 와 구분되지 않는다.
608
+ rc=0; bash "$RELAY" run --cap "$A_N11" --cap "$B" --task t \
609
+ --record "$T/rec12" > "$T/on12" 2>&1 || rc=$?
610
+ _t "N12 인자를 안 주면 같은 노드가 막힌다(컨트롤 — 채널이 실재한다는 증거)" 2 "$rc"
611
+ _tout "N12b 인자 없이 부르면 진입점이 빈 인자를 본다" "ARGS=\[\]" "$T/rec12/node1.out"
612
+
613
+ # N13 결박 대상은 **가장 최근 --cap** 이다 — 위치 인덱스로 짝지으면 조용히 다른 노드에 붙는다
614
+ A_N13=$(ARGSCAP a_n13.cap a)
615
+ rc=0; bash "$RELAY" run --cap "$B" --cap "$A_N13" --cap-args "GO" --task t > "$T/on13" 2>&1 || rc=$?
616
+ _t "N13 인자가 «가장 최근 --cap» 에 결박된다(첫 노드로 새지 않는다)" 0 "$rc"
617
+
618
+ # N14 `--cap` 없이 먼저 오면 fail-closed — 어디 붙일지 모르는 인자를 조용히 버리지 않는다
619
+ rc=0; bash "$RELAY" run --cap-args "GO" --cap "$A_N11" --cap "$B" --task t > "$T/on14" 2>&1 || rc=$?
620
+ _t "N14 --cap 앞의 --cap-args 는 fail-closed(무음 폐기 아님)" 10 "$rc"
621
+
622
+ # N15 셸 메타문자 거부 — 진입점이 단어분해로 실행되므로 프로브와 같은 규칙을 쓴다
623
+ rc=0; bash "$RELAY" run --cap "$A_N11" --cap-args 'GO; rm -rf /tmp/x' --cap "$B" --task t > "$T/on15" 2>&1 || rc=$?
624
+ _t "N15 --cap-args 의 셸 메타문자는 거부된다" 10 "$rc"
625
+
626
+ # ── N16–N18 cross-family 3건(2026-08-16) — 전부 손 재현 후 앵커 ────────────────
627
+ # 셋 다 「조용히 잘못되는」 형태다: 값이 사라지거나, 결박이 죽거나, 인자 경계가 바뀐다.
628
+ A_N16=$(ARGSCAP a_n16.cap a)
629
+ rc=0; bash "$RELAY" run --cap "$A_N16" --cap-args "FIRST" --cap-args "GO" --cap "$B" --task t > "$T/on16" 2>&1 || rc=$?
630
+ _t "N16 같은 --cap 에 --cap-args 두 번은 fail-closed(앞 값 무음 폐기 금지)" 10 "$rc"
631
+
632
+ # N17 결박 플래그와의 충돌 — `--cap-args --` 로 `--upstream-verdict` 를 위치인자로 밀어낼 수 있었다
633
+ A_N17=$(cap a_n17.cap "id: demo:a" "entry: bash $T/n_echo_args.sh" "upstream_argv: yes" \
634
+ "verdict_channel: exit" "verdict_enum: 0=PASS 1=FAIL" \
635
+ "approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
636
+ "tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
637
+ rc=0; bash "$RELAY" run --cap "$A_N17" --cap-args "GO" --cap "$B" --task t > "$T/on17" 2>&1 || rc=$?
638
+ _t "N17 upstream_argv 노드에 --cap-args 를 붙이면 fail-closed(결박이 조용히 죽지 않는다)" 10 "$rc"
639
+ # N17b 컨트롤 — upstream_argv 를 **선언하지 않은** 노드는 종전대로 통과해야 한다(과차단 방지)
640
+ rc=0; bash "$RELAY" run --cap "$A_N16" --cap-args "GO" --cap "$B" --task t > "$T/on17b" 2>&1 || rc=$?
641
+ _t "N17b 컨트롤 — upstream_argv 미선언 노드는 --cap-args 를 정상 수용" 0 "$rc"
642
+
643
+ # N18 개행 — 초판 가드를 통과해 **한 인자가 두 인자로 쪼개졌다**($'GO\nSECOND' → ARGC=2)
644
+ rc=0; bash "$RELAY" run --cap "$A_N16" --cap-args "$(printf 'GO\nSECOND')" --cap "$B" --task t > "$T/on18" 2>&1 || rc=$?
645
+ _t "N18 --cap-args 의 개행은 거부된다(인자 경계 무음 변형 차단)" 10 "$rc"
646
+ # N18b 컨트롤 — **정상 다중 인자**(공백 구분)는 계속 통과해야 한다. 안 그러면 N18 은
647
+ # 「인자를 여러 개 못 주는 계기」와 구분되지 않는다.
648
+ A_N18=$(cap a_n18.cap "id: demo:a" "entry: bash $T/n_echo_args.sh" "verdict_channel: exit" \
649
+ "verdict_enum: 0=PASS 1=FAIL" "approval: auto" "reversibility: reversible" "residency: public" \
650
+ "degrade: fail-closed" "tier_floor: none" "writes: read-only" "judge: mechanical" \
651
+ "verdict_binding: FAIL")
652
+ rc=0; bash "$RELAY" run --cap "$A_N18" --cap-args "GO SECOND THIRD" --cap "$B" --task t \
653
+ --record "$T/rec18" > "$T/on18b" 2>&1 || rc=$?
654
+ _t "N18b 컨트롤 — 공백 구분 다중 인자는 정상 통과" 0 "$rc"
655
+ _tout "N18b2 세 인자가 그대로 도달한다" "ARGS=\[GO SECOND THIRD\]" "$T/rec18/node1.out"
656
+
581
657
  printf '\n── relay_channel lanes: %d PASS / %d FAIL ──\n' "$pass" "$fail"
582
658
  [ "$fail" -eq 0 ] || exit 1
583
659
  exit 0
@@ -284,8 +284,20 @@ else
284
284
  psa_load "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" \
285
285
  "${PSA_PATTERNS:-$REPO_ROOT/.claude/rules/.public-surface-patterns}"
286
286
  if [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
287
- echo " ⚠️ operator pattern override absent only committed defaults active (home paths)."
288
- echo " Populate .claude/rules/.public-surface-patterns (gitignored) for company-specific tokens."
287
+ # 2026-08-16 weekly-audit residualoperator decision: LOUDER WARNING, not fail-closed.
288
+ # A hard block here would fail every fresh clone's / CI's / worktree's first commit (the
289
+ # override is gitignored by construction), training PUBLIC_SURFACE_OK into a reflex — the same
290
+ # override-habituation this repo has measured and warned against repeatedly. So the degrade
291
+ # direction stays PASS; what changed is that the old two-line notice was easy to scroll past in
292
+ # a green hook run. This is now impossible to miss, still non-blocking.
293
+ echo ""
294
+ echo " 🟧🟧🟧 CONFIDENTIALITY COVERAGE REDUCED — READ BEFORE TRUSTING THIS PASS 🟧🟧🟧"
295
+ echo " ⚠️ operator pattern override ABSENT — only committed defaults are active."
296
+ echo " Company/employer-name and real-username token classes are UNSCANNED this run."
297
+ echo " A clean PASS below reflects the defaults-only pattern set, not the full one."
298
+ echo " Fix: populate .claude/rules/.public-surface-patterns (gitignored, one severity<TAB>regex per line)."
299
+ echo " 🟧🟧🟧 ────────────────────────────────────────────────────────────── 🟧🟧🟧"
300
+ echo ""
289
301
  fi
290
302
  if [ "$PSA_DEFAULTS_OK" -eq 0 ] || [ "$PSA_BAD_ROWS" -gt 0 ] \
291
303
  || [ -z "$(printf '%s' "$PSA_STREAM" | grep -vE '^[[:space:]]*(#|$)' || true)" ]; then