@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.
- package/.claude/rules/fh_4axis_gate.md +26 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +28 -3
- package/CLAUDE.md +146 -168
- package/README.ja.md +144 -24
- package/README.ko.md +135 -22
- package/README.md +204 -27
- package/README.zh.md +126 -21
- package/docs/ETHOS.md +10 -3
- package/knowledge/shared/dialogue/ai_dialogue_playbook.md +131 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +90 -0
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +75 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -3
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +92 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +196 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/knowledge/shared/rules/multi_session_close_protocol.md +7 -3
- package/package.json +15 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +157 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -0
- package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +37 -0
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
- package/scripts/adapters/peer_resolve.sh +58 -6
- package/scripts/branch_claim.sh +3 -1
- package/scripts/cluster_capability_scan.sh +42 -13
- package/scripts/digest_landing_check.sh +142 -2
- package/scripts/fh_hub_identity.sh +83 -0
- package/scripts/fh_session_load.sh +53 -5
- package/scripts/fh_track_resolve.sh +114 -0
- package/scripts/field_canon_preload.sh +50 -5
- package/scripts/listing_watch.sh +187 -0
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/prior_art_prompt.sh +168 -0
- package/scripts/psa_scan_lib.sh +201 -15
- package/scripts/residency_admission_check.sh +204 -0
- package/scripts/selfcheck.sh +103 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -0
- package/scripts/test_listing_watch_lanes.sh +162 -0
- package/scripts/test_marker_defense_lanes.sh +152 -0
- package/scripts/test_marker_soul_check_lanes.sh +211 -0
- package/scripts/test_prior_art_prompt_lanes.sh +128 -0
- package/scripts/test_psa_singlefile_lanes.sh +351 -1
- package/scripts/test_residency_admission_lanes.sh +60 -0
- package/scripts/test_track_resolve_lanes.sh +158 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +40 -0
- package/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- 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
|