@chrono-meta/fh-gate 2.6.0 → 2.8.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 (55) hide show
  1. package/.claude/rules/fh_4axis_gate.md +26 -3
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/AGENTS.md +28 -3
  4. package/CLAUDE.md +146 -168
  5. package/README.ja.md +144 -24
  6. package/README.ko.md +135 -22
  7. package/README.md +204 -27
  8. package/README.zh.md +126 -21
  9. package/docs/ETHOS.md +10 -3
  10. package/knowledge/shared/dialogue/ai_dialogue_playbook.md +131 -0
  11. package/knowledge/shared/harness-core/claude_md_gate_details.md +90 -0
  12. package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +75 -0
  13. package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -3
  14. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +92 -5
  15. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
  16. package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
  17. package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
  18. package/knowledge/shared/learnings/subagent_invocations_log.yaml +196 -0
  19. package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
  20. package/knowledge/shared/rules/multi_session_close_protocol.md +7 -3
  21. package/package.json +15 -2
  22. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  23. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  24. package/plugins/fh-meta/CHANGELOG.md +157 -0
  25. package/plugins/fh-meta/agents/persona-innovator.md +170 -0
  26. package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +37 -0
  27. package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
  28. package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
  29. package/scripts/adapters/peer_resolve.sh +58 -6
  30. package/scripts/branch_claim.sh +3 -1
  31. package/scripts/cluster_capability_scan.sh +42 -13
  32. package/scripts/digest_landing_check.sh +142 -2
  33. package/scripts/fh_hub_identity.sh +83 -0
  34. package/scripts/fh_session_load.sh +53 -5
  35. package/scripts/fh_track_resolve.sh +114 -0
  36. package/scripts/field_canon_preload.sh +50 -5
  37. package/scripts/listing_watch.sh +187 -0
  38. package/scripts/package_coverage_check.sh +8 -0
  39. package/scripts/prior_art_prompt.sh +168 -0
  40. package/scripts/psa_scan_lib.sh +201 -15
  41. package/scripts/residency_admission_check.sh +204 -0
  42. package/scripts/selfcheck.sh +103 -0
  43. package/scripts/test_adapter_lanes.sh +67 -2
  44. package/scripts/test_heavy_classifier_lanes.sh +144 -0
  45. package/scripts/test_listing_watch_lanes.sh +162 -0
  46. package/scripts/test_marker_defense_lanes.sh +152 -0
  47. package/scripts/test_marker_soul_check_lanes.sh +211 -0
  48. package/scripts/test_prior_art_prompt_lanes.sh +128 -0
  49. package/scripts/test_psa_singlefile_lanes.sh +351 -1
  50. package/scripts/test_residency_admission_lanes.sh +60 -0
  51. package/scripts/test_track_resolve_lanes.sh +158 -0
  52. package/scripts/test_wizard_snippet_merge_lanes.sh +40 -0
  53. package/templates/.git-hooks/pre-commit +400 -4
  54. package/templates/.git-hooks/pre-push +17 -2
  55. package/templates/settings.PriorArt.snippet.json +44 -0
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env bash
2
+ # test_marker_soul_check_lanes.sh — regression fixtures for pre-commit's
3
+ # validate_soul_check_leg
4
+ # (ⓒ 격리 그라운딩 — 사전 선언으로 확장, 2026-08-21).
5
+ #
6
+ # 🟥 NOT a seventh axis. `fh_three_layer_canon.md §1-a-2` splits axes by «무엇을 받았는가», and
7
+ # ⓒ already receives «저자가 쓴 문장 + 지금의 트리» — identical to this leg's input. Tense
8
+ # (사전 선언 vs 사후 주장) is a posture, not an axis.
9
+ #
10
+ # WHY: `①영혼` has been required on every marker since 2026-08-09 and is written 4/4 on the real
11
+ # corpus, with ZERO consuming code (measured with a control: `crossfamily` = 21 hits in the hook,
12
+ # `①영혼`/`soul` = 0 lines that read it; `fh_4axis_gate.md` does not name the field). Slot with no
13
+ # consumer → presence does the judging → always "done".
14
+ #
15
+ # SCOPE — channel, not judgment. These lanes assert the RECORD's properties: single · closed enum ·
16
+ # non-vacuous · not wearing another axis's tokens · consistent with the ①영혼 line in the SAME file.
17
+ # They never assert the read-back reached the right conclusion.
18
+ #
19
+ # 🟥 NAMED RESIDUAL — a fabricated `reflected(<fluent reason>)` on a marker that DOES carry an
20
+ # ①영혼 line PASSES. Provenance is not checkable from a file (the hook says so at the ①영혼
21
+ # comment, and that part is correct). The hole is §4-b's — cross-family reads the marker.
22
+ #
23
+ # Fixtures assert BOTH directions (known-pair): every intended shape is admitted, and every hole
24
+ # the lane closes still blocks.
25
+ #
26
+ # Usage: bash scripts/test_marker_soul_check_lanes.sh Exit: 0 = all behave; 1 = regression.
27
+
28
+ set -uo pipefail
29
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
30
+ HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
31
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
32
+
33
+ sed -n '/^validate_soul_check_leg()/,/^}/p' "$HOOK" > "$T/fn.sh"
34
+
35
+ # Instrument calibration — an empty extraction would let every fixture "pass" against nothing.
36
+ if ! grep -q 'DEGRADED_NO_SOUL' "$T/fn.sh"; then
37
+ echo "❌ HARNESS-ERROR — validate_soul_check_leg did not extract from $HOOK."
38
+ echo " Fixtures below would measure an empty function. Aborting rather than reporting green."
39
+ exit 1
40
+ fi
41
+
42
+ SOUL='①영혼: 성공 정의 = «소비자 경로에서 완주해 rc=0 을 내고 컨트롤로 가른다». 절대 안 함 = «FAIL 을 SKIP 으로 바꾸는 것»'
43
+
44
+ FAIL=0
45
+ lane() { # $1 = id $2 = expect(BLOCK|PASS) $3 = marker body
46
+ local id="$1" expect="$2" body="$3" rc out
47
+ printf '%s\n' "$body" > "$T/m.marker"
48
+ out=$( bash -c 'set -uo pipefail; . "$1"; validate_soul_check_leg "$2"' _ "$T/fn.sh" "$T/m.marker" 2>&1 ); rc=$?
49
+ local got=PASS; [ $rc -ne 0 ] && got=BLOCK
50
+ if [ "$got" = "$expect" ]; then
51
+ printf ' ✅ %-40s %s\n' "$id" "$got"
52
+ else
53
+ printf ' ❌ %-40s expected %s, got %s\n %s\n' "$id" "$expect" "$got" "$(printf '%s' "$out" | head -3)"
54
+ FAIL=1
55
+ fi
56
+ }
57
+
58
+ echo "== soul-check lanes — BLOCK side (holes that must stay closed) =="
59
+ lane G1-not-in-enum BLOCK "$SOUL
60
+ soul-check: done"
61
+ lane G2-bare-reflected BLOCK "$SOUL
62
+ soul-check: reflected"
63
+ lane G3-vacuous-grounds BLOCK "$SOUL
64
+ soul-check: reflected(ok)"
65
+ lane G4-duplicate-line BLOCK "$SOUL
66
+ soul-check: UNKNOWN
67
+ soul-check: reflected(cross-family agy 가 델타를 성공정의에 비춰 2건 지적)"
68
+ lane G5-crossaxis-tier2 BLOCK "$SOUL
69
+ soul-check: tier2(qasp)"
70
+ lane G6-crossaxis-checked BLOCK "$SOUL
71
+ soul-check: checked(prior art 없음)"
72
+ lane G7-crossaxis-panel BLOCK "$SOUL
73
+ soul-check: panel(codex,agy)"
74
+ echo " -- the consistency check (two fields, one record) --"
75
+ lane G8-reflected-no-soul BLOCK "axis2-model: opus
76
+ soul-check: reflected(델타를 성공정의에 비춰 봤고 어긋남 없음)"
77
+ lane G9-reflected-empty-soul BLOCK "①영혼: 없음
78
+ soul-check: reflected(델타를 성공정의에 비춰 봤고 어긋남 없음)"
79
+ lane G10-violated-no-soul BLOCK "axis2-model: opus
80
+ soul-check: violated(절대안함 을 어겼다 — SKIP 으로 바꿨다)"
81
+ lane G11-nosoul-but-soul-is BLOCK "$SOUL
82
+ soul-check: DEGRADED_NO_SOUL(영혼 줄이 없어서 되비출 대상 자체가 없다)"
83
+ lane G11b-nosoul-but-soul-key BLOCK "soul: 성공 정의 = 소비자 경로에서 완주한다. 절대 안 함 = FAIL 을 SKIP 으로 바꾸지 않는다
84
+ soul-check: DEGRADED_NO_SOUL(영혼 줄이 없어서 되비출 대상 자체가 없다)"
85
+
86
+ echo "== soul-check lanes — PASS side (intended shapes must be admitted) =="
87
+ lane G12-absent-is-grace PASS "$SOUL
88
+ axis2-model: opus"
89
+ lane G13-reflected-ok PASS "$SOUL
90
+ soul-check: reflected(agy/gemini 가 델타를 성공정의에 비춰 재검 — 절대안함 위반 0, 성공정의 미달 0)"
91
+ lane G13b-reflected-soul-key PASS "soul: 성공 정의 = 소비자 경로에서 완주한다. 절대 안 함 = FAIL 을 SKIP 으로 바꾸지 않는다
92
+ soul-check: reflected(codex 가 soul 필드를 성공정의에 비춰 재검 — 위반 0)"
93
+ lane G14-violated-is-legal PASS "$SOUL
94
+ soul-check: violated(절대안함 «FAIL 을 SKIP 으로» 를 어겼다 — 되돌리고 주체를 이름으로 찍게 고쳤다)"
95
+ lane G15-unknown-bare-ok PASS "$SOUL
96
+ soul-check: UNKNOWN"
97
+ lane G16-degraded-not-run PASS "$SOUL
98
+ soul-check: DEGRADED_NOT_RUN(되비출 사이드카가 없어 이번 커밋에선 안 돌렸다)"
99
+ lane G17-nosoul-when-truly PASS "axis2-model: opus
100
+ soul-check: DEGRADED_NO_SOUL(경량 예외 커밋이라 영혼 줄을 안 썼다)"
101
+ lane G18-not-applicable PASS "$SOUL
102
+ soul-check: not-applicable(문서 오타 수정이라 성공정의에 걸리는 델타가 없다)"
103
+ lane G19-em-dash-reason PASS "$SOUL
104
+ soul-check: reflected(codex 가 성공정의 대조 — 어긋남 없음) — 2026-08-21"
105
+
106
+
107
+ # ── ①영혼 presence lane (validate_soul_present_leg) ───────────────────────────
108
+ sed -n '/^validate_soul_present_leg()/,/^}/p' "$HOOK" > "$T/fnp.sh"
109
+ if ! grep -q 'SOUL_PRESENT_GRACE' "$T/fnp.sh" && ! grep -q '①영혼 line' "$T/fnp.sh"; then
110
+ echo "❌ HARNESS-ERROR — validate_soul_present_leg did not extract. Aborting."
111
+ exit 1
112
+ fi
113
+ GRACE=$(grep -m1 '^SOUL_PRESENT_GRACE_DATE=' "$HOOK" | sed -E 's/.*"(.*)"/\1/')
114
+
115
+ planep() { # $1=id $2=expect $3=filename-date $4=body
116
+ local id="$1" expect="$2" d="$3" body="$4" rc out f
117
+ f="$T/.axes_23_passed_fix_x_${d}.marker"
118
+ printf '%s\n' "$body" > "$f"
119
+ out=$( bash -c 'set -uo pipefail; SOUL_PRESENT_GRACE_DATE="$3"; . "$1"; validate_soul_present_leg "$2"' \
120
+ _ "$T/fnp.sh" "$f" "$GRACE" 2>&1 ); rc=$?
121
+ local got=PASS; [ $rc -ne 0 ] && got=BLOCK
122
+ if [ "$got" = "$expect" ]; then printf ' ✅ %-40s %s\n' "$id" "$got"
123
+ else printf ' ❌ %-40s expected %s, got %s\n %s\n' "$id" "$expect" "$got" "$(printf '%s' "$out"|head -2)"; FAIL=1; fi
124
+ }
125
+
126
+ echo
127
+ echo "== ①영혼 presence lanes (grace = $GRACE) =="
128
+ planep P1-missing-post-grace BLOCK "$GRACE" 'axes-run: ⓐ=codex ⓑ=→standpoint
129
+ crossfamily: panel(codex,agy) — 겹친 지적 0'
130
+ # 🟥 P2 flipped BLOCK→PASS on 2026-08-21: a bare `없음` is the value CLAUDE.md §자기 대조
131
+ # explicitly permits, and blocking it punished the honest form while a padded «I didn't write
132
+ # one» paragraph passed. Found by a peer session's first real use, not by this suite.
133
+ planep P2-declared-absent-ok PASS "$GRACE" '①영혼: 없음
134
+ axes-run: ⓐ=codex'
135
+ planep P2b-empty-field-blocks BLOCK "$GRACE" 'soul:
136
+ axes-run: ⓐ=codex'
137
+ planep P2c-absent-heading-form PASS "$GRACE" '## ①영혼 — 없음
138
+ axes-run: ⓐ=codex'
139
+ # fixture updated 2026-08-21: `없음` became a legal declared-absence value, so a truly vacuous
140
+ # field is now the discriminating input (the point of codex's lane was masking-by-next-field,
141
+ # not the token `없음`).
142
+ planep P2b-vacuous-before-fields BLOCK "$GRACE" '①영혼: ok
143
+ axis2-engine: steel-quench
144
+ axis2-model: opus
145
+ floor-status: ok
146
+ axes-run: ⓐ=codex ⓑ=→standpoint ⓒ=x ⓓ=x ⓔ=x ⓕ=x
147
+ controls: alive — known-positive x 1 hit · known-negative 0'
148
+ planep P3-present-korean PASS "$GRACE" "$SOUL
149
+ axes-run: ⓐ=codex"
150
+ planep P4-present-soul-key PASS "$GRACE" 'soul: 성공 정의 = 소비자 경로에서 완주한다. 절대 안 함 = FAIL 을 SKIP 으로 바꾸지 않는다
151
+ axes-run: ⓐ=codex'
152
+ planep P5-grace-old-exempt PASS "2026-08-01" 'axes-run: ⓐ=codex
153
+ crossfamily: panel(codex,agy) — 겹친 지적 0'
154
+ planep P6-real-corpus-negative BLOCK "$GRACE" "$(cat "$REPO_ROOT/tracks/_meta/.axes_23_passed_feat_target-freeze-gate_2026-08-18.marker" 2>/dev/null || echo 'axes-run: x')"
155
+ planep P7-real-corpus-positive PASS "$GRACE" "$(cat "$REPO_ROOT/tracks/_meta/.axes_23_passed_fix_witness-two-commit-discipline_2026-08-17.marker" 2>/dev/null || echo '①영혼: 성공 정의 = 하나 둘 셋 넷 다섯 여섯')"
156
+
157
+ echo
158
+ echo "== cross-family findings — regression lanes (codex/gpt-5.5, diff 축, 2026-08-21) =="
159
+ # F1 self-referential FP: codex's own alignment fix flipped G8 to PASS because the
160
+ # `soul-check:` line contains 「성공정의」 and matched as its own evidence.
161
+ lane F1-selfref-soulcheck-only BLOCK 'axis2-model: opus
162
+ soul-check: reflected(델타를 성공정의에 비춰 봤고 절대 안 함 도 안 어겼다)'
163
+ # F4 grounds outside the parens
164
+ lane F4-grounds-outside-parens BLOCK "$SOUL
165
+ soul-check: reflected() — 아주 길고 그럴듯한 꼬리말이 여기 붙어 있어서 길이를 채운다"
166
+ # F3 near-miss key reads as absent
167
+ lane F3-nearmiss-key BLOCK "$SOUL
168
+ soul_check: reflected(델타를 성공정의에 비춰 재검했다)"
169
+ lane F3b-nearmiss-space BLOCK "$SOUL
170
+ soul-check : reflected(델타를 성공정의에 비춰 재검했다)"
171
+ planep F1b-presence-selfref BLOCK "$GRACE" 'axes-run: ⓐ=codex
172
+ soul-check: reflected(성공 정의 대조 · 절대 안 함 확인)'
173
+ planep F1c-comment-not-soul BLOCK "$GRACE" '# 성공 정의 를 나중에 쓸 예정이다 절대 안 함 도
174
+ axes-run: ⓐ=codex'
175
+ # F2: unparseable date must NOT take the grace exit (was fail-open)
176
+ printf 'axes-run: a=codex\n' > "$T/.axes_23_passed_nodate.marker"
177
+ _o=$( bash -c 'set -uo pipefail; SOUL_PRESENT_GRACE_DATE="$3"; . "$1"; validate_soul_present_leg "$2"' \
178
+ _ "$T/fnp.sh" "$T/.axes_23_passed_nodate.marker" "$GRACE" 2>&1 ); _r=$?
179
+ if [ $_r -ne 0 ]; then printf ' ✅ %-40s %s\n' "F2-unparseable-date-not-exempt" "BLOCK"
180
+ else printf ' ❌ %-40s expected BLOCK, got PASS\n' "F2-unparseable-date-not-exempt"; FAIL=1; fi
181
+
182
+ echo
183
+ echo "== cross-family findings — regression lanes (agy/gemini-3.1-pro, 주장·기록 축) =="
184
+ # A6 [S]: grounds counted with ${#} passed on a single long Korean token. Word count now.
185
+ lane A6-korean-onetoken-grounds BLOCK "$SOUL
186
+ soul-check: reflected(어긋남없음확인완료했음)"
187
+ # A6b [S]: the two legs disagreed on the SAME soul block (6 vs 4 words). Aligned to 6.
188
+ # 5 words under `wc -w` — passed the old 4-word floor in the check leg while the present leg
189
+ # demanded 6 on the SAME block. Empirically calibrated, not guessed: `wc -w` does not equal a
190
+ # human word count on Korean («①영혼: 완주» reports 3), so the fixture was measured, not reasoned.
191
+ lane A6b-threshold-aligned BLOCK '①영혼: 성공 정의
192
+ soul-check: reflected(델타를 성공정의에 비춰 재검했고 어긋남 없음)'
193
+ # A6-control: a genuinely grounded, multi-word reason still passes (this lane must NOT block).
194
+ lane A6c-control-passes PASS "$SOUL
195
+ soul-check: reflected(codex 가 성공정의 대조 — 어긋남 없음)"
196
+
197
+ echo
198
+ echo "== 첫 실사용 findings — peer session (forge-harness-59, 2026-08-21) =="
199
+ # A marker that declares NO success definition cannot claim to have reflected against one.
200
+ lane N1-absent-soul-vs-reflected BLOCK 'soul: 없음
201
+ soul-check: reflected(델타를 성공정의에 비춰 재검했고 어긋남 없음)'
202
+ lane N2-absent-soul-degraded-ok PASS 'soul: 없음
203
+ soul-check: DEGRADED_NO_SOUL(설계 전에 성공 정의를 쓰지 않았다 — 되비출 대상이 없다)'
204
+ # The padded confession that passed in the field: it IS a real declaration of absence, and once
205
+ # `없음` is a first-class value the padded form is no longer the only way through.
206
+ lane N3-padded-confession PASS 'soul: 없음 — 설계 전에 안 썼다. 사후 재구성이 아니라 그 사실을 적는다. 붙잡고 있던 것은 범위 결정 하나였다
207
+ axes-run: ⓐ=codex'
208
+
209
+ echo
210
+ if [ $FAIL -eq 0 ]; then echo "SOUL LANES: PASS"; else echo "SOUL LANES: FAIL"; fi
211
+ exit $FAIL
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # test_prior_art_prompt_lanes.sh — T2 훅의 known-pair 앵커.
3
+ # 🟥 이 훅의 사활은 «막나» 가 아니라 **«소음이 아닌가»** 다. 냉독자가 지목한 자기무력화 경로:
4
+ # 트리거가 부정확하면 하루 수십 번 뜨고, 3회 거절 억제가 오전 중에 켜져서 기능이 조용히 죽는다.
5
+ # 그래서 음성 레인이 양성보다 많다.
6
+ set -uo pipefail
7
+ FH="$(cd "$(dirname "$0")/.." && pwd)"
8
+ H="$FH/scripts/prior_art_prompt.sh"
9
+ PASS=0; FAIL=0
10
+ T="$(mktemp -d -t pap.XXXXXX)" || exit 3
11
+ trap 'rm -rf "$T"' EXIT INT TERM
12
+ chk(){ if [ "$2" = "$3" ]; then echo " ✅ $1"; PASS=$((PASS+1)); else echo " ❌ $1 (got=$2 want=$3)"; FAIL=$((FAIL+1)); fi; }
13
+ # 각 호출은 세션-스탬프를 격리해야 «한 번만» 규칙에 서로 안 걸린다
14
+ fires(){ local d; d="$(mktemp -d -t papd.XXXXXX)"; mkdir -p "$d/.claude"
15
+ local o; o=$(printf '{"tool_input":{"file_path":"%s"}}' "$1" | CLAUDE_PROJECT_DIR="$d" bash "$H" 2>/dev/null)
16
+ rm -rf "$d"; [ -n "$o" ] && echo yes || echo no; }
17
+
18
+ echo "── 양성: 새 «메커니즘» 을 쓰기 직전 ──"
19
+ chk "P1 새 .sh" "$(fires "$T/new_gate.sh")" yes
20
+ chk "P2 새 SKILL.md" "$(fires "$T/plugins/x/skills/y/SKILL.md")" yes
21
+ chk "P3 새 agents/*.md" "$(fires "$T/plugins/x/agents/z.md")" yes
22
+ chk "P4 새 .py" "$(fires "$T/tool.py")" yes
23
+
24
+ echo "── 음성: 소음이 되면 이 능력이 죽는다 ──"
25
+ printf 'x\n' > "$T/exists.sh"
26
+ chk "N1 🟥 «이미 있는 파일 편집» 은 새로 짓기가 아니다" "$(fires "$T/exists.sh")" no
27
+ chk "N2 기록/산출물(.md 일반)" "$(fires "$T/notes.md")" no
28
+ chk "N3 tracks/ 아래 기록" "$(fires "$T/tracks/_meta/x.sh")" no
29
+ chk "N4 로그" "$(fires "$T/logs/a.sh")" no
30
+ chk "N5 json/lock" "$(fires "$T/a.json")" no
31
+
32
+ echo "── 🟥 스코프가 «세션» 이지 «날짜» 가 아니다 (초판 결함의 앵커) ──"
33
+ # 초판은 스탬프를 날짜로 잡아, 하루에 세션을 여럿 도는 환경에서 **첫 세션만 뜨고 나머지는 전부
34
+ # 침묵**했다. 주석은 «세션당» 이었고 구현은 «하루당» 이었다 — 그 둘이 갈리는 것이 이 레인이다.
35
+ S="$(mktemp -d -t papsid.XXXXXX)"; mkdir -p "$S/.claude"
36
+ sid_fire(){ local o; o=$(printf '{"session_id":"%s","tool_input":{"file_path":"%s/%s.sh"}}' "$1" "$T" "$2" \
37
+ | CLAUDE_PROJECT_DIR="$S" bash "$H" 2>/dev/null); [ -n "$o" ] && echo yes || echo no; }
38
+ chk "S1 세션 A 1회차 → 뜬다" "$(sid_fire sess-A f1)" yes
39
+ chk "S2 세션 A 2회차 → 무음" "$(sid_fire sess-A f2)" no
40
+ chk "S3 🟥 세션 B 1회차 → 뜬다 (같은 날 다른 세션)" "$(sid_fire sess-B f3)" yes
41
+ chk "S4 CONTROL — session_id 없으면 날짜로 degrade(무음 아님)" \
42
+ "$(printf '{"tool_input":{"file_path":"%s/f4.sh"}}' "$T" | CLAUDE_PROJECT_DIR="$(mktemp -d -t papnos.XXXXXX)" bash "$H" 2>/dev/null | grep -c additionalContext)" 1
43
+ rm -rf "$S"
44
+
45
+ echo "── 세션당 한 번 (3회 거절 억제로 조용히 죽는 경로 차단) ──"
46
+ D="$(mktemp -d -t paponce.XXXXXX)"; mkdir -p "$D/.claude"
47
+ o1=$(printf '{"tool_input":{"file_path":"%s/a.sh"}}' "$T" | CLAUDE_PROJECT_DIR="$D" bash "$H" 2>/dev/null)
48
+ o2=$(printf '{"tool_input":{"file_path":"%s/b.sh"}}' "$T" | CLAUDE_PROJECT_DIR="$D" bash "$H" 2>/dev/null)
49
+ chk "O1 첫 번째는 뜬다" "$([ -n "$o1" ] && echo yes || echo no)" yes
50
+ chk "O2 두 번째는 안 뜬다" "$([ -n "$o2" ] && echo yes || echo no)" no
51
+ rm -rf "$D"
52
+
53
+ echo "── 🟥 절대 안 막는다 (가역 표면) ──"
54
+ D2="$(mktemp -d -t papnb.XXXXXX)"; mkdir -p "$D2/.claude"
55
+ out=$(printf '{"tool_input":{"file_path":"%s/c.sh"}}' "$T" | CLAUDE_PROJECT_DIR="$D2" bash "$H" 2>/dev/null); rc=$?
56
+ chk "B1 exit 0" "$rc" 0
57
+ chk "B2 permissionDecision 을 안 낸다 (자동승인 역전 금지)" \
58
+ "$(printf '%s' "$out" | grep -c permissionDecision)" 0
59
+ chk "B3 모델 채널(additionalContext)로 간다" \
60
+ "$(printf '%s' "$out" | grep -c additionalContext)" 1
61
+ rm -rf "$D2"
62
+
63
+ echo "── D: Bash 경로 — 🟥 첫 실사용이 «비발화» 로 났던 자리 (2026-08-21) ──"
64
+ # matcher 가 `Write` 뿐이었는데 auto-mode 는 파일을 **Bash 리다이렉트/heredoc** 으로 만든다.
65
+ # 즉 이 훅이 겨냥한 순간이 구조적으로 Write 를 안 탔다. 아래가 그 재현이다.
66
+ D3=$(mktemp -d); export CLAUDE_PROJECT_DIR="$D3"; mkdir -p "$D3/.claude"
67
+ chk "D1 Bash heredoc 로 새 .sh → 발화" \
68
+ "$(printf '%s' '{"session_id":"d1","tool_input":{"command":"cat > scripts/new_thing.sh <<EOF\necho hi\nEOF"}}' | bash "$H" 2>/dev/null | grep -c additionalContext)" 1
69
+ rm -f "$D3/.claude/.prior_art_prompted_"*
70
+ chk "D2 printf 리다이렉트로 새 SKILL.md → 발화" \
71
+ "$(printf '%s' '{"session_id":"d2","tool_input":{"command":"printf x > plugins/z/skills/q/SKILL.md"}}' | bash "$H" 2>/dev/null | grep -c additionalContext)" 1
72
+ rm -f "$D3/.claude/.prior_art_prompted_"*
73
+ # ★컨트롤 — D1/D2 가 «Bash 면 무조건 발화» 가 아님을 보인다. 소음이 이 훅을 죽인다.
74
+ chk "D3 ★컨트롤 로그 리다이렉트 + 2>/dev/null → 침묵" \
75
+ "$(printf '%s' '{"session_id":"d3","tool_input":{"command":"echo x > /tmp/a.log 2>/dev/null"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
76
+ chk "D4 ★컨트롤 tracks/ 산출물 리다이렉트 → 침묵" \
77
+ "$(printf '%s' '{"session_id":"d4","tool_input":{"command":"cat > tracks/_meta/note.sh <<EOF\nx\nEOF"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
78
+ chk "D5 ★컨트롤 리다이렉트 없는 명령 → 침묵" \
79
+ "$(printf '%s' '{"session_id":"d5","tool_input":{"command":"git status --short"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
80
+ chk "D6 ★컨트롤 같은 세션 두 번째 Bash → 침묵 (세션당 1회가 Bash 에서도 산다)" \
81
+ "$( { printf '%s' '{"session_id":"d6","tool_input":{"command":"cat > scripts/a.sh <<EOF\nx\nEOF"}}' | bash "$H" >/dev/null 2>&1
82
+ printf '%s' '{"session_id":"d6","tool_input":{"command":"cat > scripts/b.sh <<EOF\nx\nEOF"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' '; } )" 0
83
+ # 🟥 E: cross-family 4 HIGH — **전부 오발화**다 (2026-08-21). 이 훅에서 오발화는 미발화보다
84
+ # 훨씬 비싸다: 소음이 되면 운영자가 무시하고 능력이 죽는다(원 저자가 «자기무력화 경로» 라 명명).
85
+ rm -f "$D3/.claude/.prior_art_prompted_"*
86
+ chk "E1 ★컨트롤 tee_probe.py 언급 (tee 에 뒤 경계 없던 자리) → 침묵" \
87
+ "$(printf '%s' '{"session_id":"e1","tool_input":{"command":"rg tee_probe.py scripts"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
88
+ chk "E2 ★컨트롤 stderr 리다이렉트 2> …/errors.sh → 침묵 (>>? 가 2> 안의 > 를 잡던 자리)" \
89
+ "$(printf '%s' '{"session_id":"e2","tool_input":{"command":"pytest 2> /tmp/pytest_errors.sh"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
90
+ chk "E2b ★컨트롤 stdout 버리고 stderr 만 .sh 로 → 침묵" \
91
+ "$(printf '%s' '{"session_id":"e2b","tool_input":{"command":"make test > /dev/null 2> /tmp/failures.sh"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
92
+ chk "E3 ★컨트롤 프로젝트 밖 리포트 git diff > /tmp/review_delta.sh → 침묵" \
93
+ "$(printf '%s' '{"session_id":"e3","tool_input":{"command":"git diff > /tmp/review_delta.sh"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
94
+ chk "E4 ★컨트롤 미확장 변수 \$LOG_DIR/x.sh → 침묵 (리터럴로 존재검사하면 거짓이다)" \
95
+ "$(printf '%s' '{"session_id":"e4","tool_input":{"command":"echo more >> \"$LOG_DIR/x.sh\""}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
96
+ # 🟥 E1b/E2c — **프로젝트 «안»** 경로로 같은 두 결함을 친다. E1/E2 는 /tmp 라서
97
+ # 프로젝트-범위 규칙(ⓒ)만으로도 침묵했다 — 되돌림 실측에서 정규식 수리를 되돌려도
98
+ # 적색이 0 이었고(=장식), 그게 이 두 레인을 만든 이유다. 아래는 ⓒ 가 못 잡는 자리다.
99
+ rm -f "$D3/.claude/.prior_art_prompted_"*
100
+ chk "E1b ★컨트롤 프로젝트 안 tee_probe.sh 언급 → 침묵 (tee 뒤 경계를 격리해서 친다)" \
101
+ "$(printf '%s' '{"session_id":"e1b","tool_input":{"command":"rg tee_probe.sh scripts"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
102
+ chk "E2c ★컨트롤 프로젝트 안 stderr 2> scripts/errors.sh → 침묵 (리다이렉트 앞 경계를 격리해서 친다)" \
103
+ "$(printf '%s' '{"session_id":"e2c","tool_input":{"command":"pytest 2> scripts/errors.sh"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
104
+ # E5/E6 — session_id 는 신뢰할 수 없는 입력이다
105
+ _ESC=$(dirname "$D3")/pap_lane_escape_$$
106
+ rm -f "$_ESC"
107
+ printf '%s' "{\"session_id\":\"x/../../../$(basename "$_ESC")\",\"tool_input\":{\"file_path\":\"scripts/zz_new.sh\"}}" | bash "$H" >/dev/null 2>&1
108
+ chk "E5 session_id 경로 탈출 → 프로젝트 밖에 파일이 안 생긴다" \
109
+ "$([ -e "$_ESC" ] && echo escaped || echo none)" none
110
+ rm -f "$_ESC"
111
+ rm -f "$D3/.claude/.prior_art_prompted_"*
112
+ chk "E6 session_id 끝에 «/» → 두 번째 호출은 침묵 (스탬프가 실제로 써진다)" \
113
+ "$( { printf '%s' '{"session_id":"x/","tool_input":{"file_path":"scripts/zz_a.sh"}}' | bash "$H" >/dev/null 2>&1
114
+ printf '%s' '{"session_id":"x/","tool_input":{"file_path":"scripts/zz_b.sh"}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' '; } )" 0
115
+ # E7 — 🟥 «additionalContext 가 떴다» 는 파서가 **옳은 토큰**을 뽑았다는 증거가 아니다(cross-family 레인 지적).
116
+ rm -f "$D3/.claude/.prior_art_prompted_"*
117
+ chk "E7 추출된 대상이 실제 리다이렉트 대상이다 (파서가 엉뚱한 토큰을 뽑아도 통과하던 자리)" \
118
+ "$(printf '%s' '{"session_id":"e7","tool_input":{"command":"cat > scripts/uniq_target_name.sh <<EOF\nx\nEOF"}}' | bash "$H" 2>/dev/null | grep -c 'uniq_target_name.sh')" 1
119
+ unset CLAUDE_PROJECT_DIR; rm -rf "$D3"
120
+
121
+ echo "── CONTROL: 계기가 죽으면 조용히 통과 (advisory 는 세션을 못 죽인다) ──"
122
+ chk "C1 입력이 JSON 이 아니면 무음 exit 0" \
123
+ "$(printf 'not json' | bash "$H" >/dev/null 2>&1; echo $?)" 0
124
+ chk "C2 file_path 없으면 무음" "$(printf '{"tool_input":{}}' | bash "$H" 2>/dev/null | wc -c | tr -d ' ')" 0
125
+
126
+ echo
127
+ if [ "$FAIL" -eq 0 ]; then echo "════ prior-art-prompt lanes: $PASS passed · 0 failed ════"; exit 0
128
+ else echo "🟥 prior-art-prompt lanes: $FAIL 실패 / $((PASS+FAIL))"; exit 1; fi