@chrono-meta/fh-gate 2.3.0 → 2.5.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 (39) hide show
  1. package/.claude/rules/fh_4axis_gate.md +25 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CLAUDE.md +107 -7
  4. package/README.ja.md +14 -2
  5. package/README.ko.md +14 -1
  6. package/README.md +16 -2
  7. package/README.zh.md +12 -1
  8. package/knowledge/shared/harness-core/fh_three_layer_canon.md +18 -6
  9. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +262 -7
  10. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
  11. package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
  12. package/knowledge/shared/harness-core/sonnet_floor_doctrine.md +16 -0
  13. package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -0
  14. package/knowledge/shared/rules/multi_session_close_protocol.md +124 -2
  15. package/package.json +8 -1
  16. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  17. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  18. package/plugins/fh-meta/CHANGELOG.md +105 -0
  19. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
  20. package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
  21. package/scripts/adapters/mate_agent_boundary.sh +27 -7
  22. package/scripts/capability_effect_probe.sh +173 -5
  23. package/scripts/capability_registry_check.sh +69 -3
  24. package/scripts/digest_landing_check.sh +168 -13
  25. package/scripts/fh_session_load.sh +71 -0
  26. package/scripts/relay_channel.sh +151 -6
  27. package/scripts/selfcheck.sh +47 -0
  28. package/scripts/target_freeze.sh +239 -0
  29. package/scripts/test_adapter_lanes.sh +17 -0
  30. package/scripts/test_evidence_root_psa_lanes.sh +78 -0
  31. package/scripts/test_marker_standpoint_lanes.sh +83 -0
  32. package/scripts/test_marker_thirdparty_lanes.sh +73 -0
  33. package/scripts/test_regression_guard_ci_lanes.sh +170 -0
  34. package/scripts/test_relay_channel_lanes.sh +146 -17
  35. package/scripts/test_satellite_publish_gate_lanes.sh +339 -0
  36. package/scripts/test_target_freeze_lanes.sh +203 -0
  37. package/templates/.git-hooks/pre-commit +306 -6
  38. package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
  39. package/templates/regression_guard.sh +32 -4
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env bash
2
+ # target_freeze.sh — 발주한 감사의 **대상을 동결**하고, 회수 시 그게 같은 대상인지 대조한다.
3
+ #
4
+ # ## 왜 (N=3, 2026-08-17 운영자 승인)
5
+ #
6
+ # 같은 결함이 하루에 세 번 났다: ① cross-family 리뷰에 **수리 이전** diff 발송 ② 리뷰 출력을
7
+ # `tail` 로 잘라 지적·핀 유실 ③ 스캔 후 파일 편집으로 라인 번호 밀림. 셋 다 「보낸 것/읽은
8
+ # 것이 지금 트리가 맞나」를 안 찍어서 생겼고, **산문 규율은 이미 있었는데 세 번 다 안 걸렸다**
9
+ # (`steel-quench/SKILL.md §PIN THE TARGET`). 그래서 산문이 아니라 스크립트다.
10
+ #
11
+ # ## 🟥 선행 실패 2종을 피한다 — 이건 3번째 시도다
12
+ #
13
+ # 같은 SKILL.md 가 두 번의 기계화 실패를 이미 기록해 뒀다:
14
+ # ① 훅 advisory → 마커 100% 에서 발화 = 배경 소음, 수렴 탐지기가 어휘를 못 맞춤 → 제거
15
+ # ② `base-SHA + diff 줄 수` 지문 → **in-place 편집에 불변**(산문 수리의 기본 형태!)이고
16
+ # untracked 에 구조적으로 맹목 → 제거
17
+ # ⇒ 그래서 여기서는 **줄 수가 아니라 내용을 해시**하고(①의 반대), untracked 를 **따로**
18
+ # 센다(②의 반대). 훅에 안 건다 — 발주자가 명시적으로 부르는 두 커맨드다.
19
+ #
20
+ # ## 설계 제약 (전부 이 레포의 기존 실패에서 나왔다)
21
+ # ⓐ tracked 변경 + untracked 를 **content-address** (steel-quench L450)
22
+ # ⓑ `assume-unchanged`/`skip-worktree` 은폐 검사 필수 (publish_freshness_check.sh:61)
23
+ # ⓒ git 실패 = **UNKNOWN**, 절대 clean 아님 (session_close_check.sh:38-48)
24
+ # ⓓ `.git` 전체를 지문에 넣지 않는다 — `index` 가 정당하게 갱신돼 거짓 불일치
25
+ # (capability_effect_probe.sh:253)
26
+ # ⓔ 판정 키는 트리가 아니라 **세션** (branch_claim.sh:13-20)
27
+ # ⓕ 사이드카는 트리를 못 읽는 경우가 많다 → **발주자 측 대조**여야 한다
28
+ # (auto-decorrelation/SKILL.md:191)
29
+ #
30
+ # ## 사용
31
+ # bash scripts/target_freeze.sh pin <repo> <label> # 발주 직전. 지문 토큰을 stdout 에
32
+ # bash scripts/target_freeze.sh verify <repo> <label> # 회수 직후. 불일치면 WRONG-TARGET
33
+ # bash scripts/target_freeze.sh show <repo> <label>
34
+ #
35
+ # ## 종료 코드
36
+ # 0 MATCH — 대상이 그대로다
37
+ # 1 WRONG-TARGET — 대상이 움직였다. **그 라운드의 판정을 무효화하라**
38
+ # 10 UNKNOWN — 잴 수 없었다(git 실패·해시 도구 부재·핀 없음). 미측정이지 통과 아님
39
+ #
40
+ # 🟥 **이 스크립트는 «내가 보낸 내용»이 아니라 «대상 트리»를 잰다.** 사이드카에 파일을
41
+ # 인라인해 보냈다면 그 인라인 내용의 해시도 따로 결박해야 한다(ⓕ) — 여기서는 안 한다.
42
+ # 범위를 넓게 주장하지 않으려고 명시한다.
43
+
44
+ set -u
45
+
46
+ _die_unknown() { printf 'UNKNOWN — %s\n' "$1" >&2; exit 10; }
47
+
48
+ # 🟥 **`git -C` 는 `GIT_DIR`/`GIT_WORK_TREE` 를 못 이긴다** (cross-family agy, 2026-08-18).
49
+ # 호출자 환경에 그 변수가 남아 있으면 이 스크립트는 **엉뚱한 레포**를 재고도 «정상 경로를
50
+ # 동결했다» 고 보고한다. 지문 계기가 대상을 틀리는 형태라 다른 모든 지적보다 상위다.
51
+ _git() { env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE git "$@"; }
52
+
53
+ _sha() {
54
+ # chamber_witness.sh:84 와 같은 형태(macOS/linux 폴백). 도구 없으면 fail-closed.
55
+ #
56
+ # 🟥 **`| awk` 로 잘라내지 않는다** (cross-family codex, 2026-08-18, 재현 확인).
57
+ # `shasum | awk` 는 shasum 의 rc 를 잃는다 — 해시 도구가 특정 입력에서 실패해도
58
+ # awk 는 rc 0 을 내고 stdout 이 비어, 서로 다른 내용이 **같은 빈 지문**을 받아 MATCH 가
59
+ # 된다. 게이트 안에서 이 형태가 fail-open 이다.
60
+ local out rc
61
+ if command -v shasum >/dev/null 2>&1; then out=$(shasum -a 256); rc=$?
62
+ elif command -v sha256sum >/dev/null 2>&1; then out=$(sha256sum); rc=$?
63
+ else return 3; fi
64
+ [ "$rc" -eq 0 ] || return 3
65
+ out=${out%% *}
66
+ [ -n "$out" ] || return 3
67
+ printf '%s\n' "$out"
68
+ }
69
+
70
+ _label_key() {
71
+ # 🟥 문자 치환으로 파일명을 만들지 않는다 — `a/b` 와 `a_b` 가 **같은 핀 파일**이 되어,
72
+ # 핀한 적 없는 라벨을 verify 하면 MATCH 가 나온다(cross-family codex, 재현 확인).
73
+ # 해시는 충돌하지 않으므로 라벨 공간이 파일명 공간에 안전하게 들어간다.
74
+ printf '%s' "$1" | _sha
75
+ }
76
+
77
+ _fingerprint() {
78
+ # stdout: <64자 해시> · 실패 시 비영 종료 + stdout 은 **빈다**
79
+ # 🟥 명령이 실패하면서 stdout 에 무언가를 뱉는 형태를 만들지 않는다 — 그러면 폴백 없이도
80
+ # 쓰레기 값이 지문이 되고, 대조가 «항상 일치» 로 조용히 무너진다.
81
+ local repo="$1" head diff_patch untracked hidden combined subs files rc
82
+
83
+ # 🟥 `HEAD` 가 아니라 **트리 해시**를 쓴다 (cross-family codex, 재현 확인). 이 지문의
84
+ # 주장은 «감사한 내용이 그대로인가» 이고, 빈 커밋(`--allow-empty`)은 내용을 안 바꾸는데
85
+ # HEAD 만 움직여 **정상 조작이 WRONG-TARGET** 이 됐다. 오탐이 잦으면 이 게이트는
86
+ # override 를 습관화시키고 그건 폐기 경로다.
87
+ head=$(_git -C "$repo" rev-parse "HEAD^{tree}" 2>&1) || { printf '%s\n' "$head" >&2; return 1; }
88
+
89
+ # ⓑ 은폐 비트 — 이게 켜져 있으면 tracked 변경이 diff 에 안 나온다. 지문이 거짓말을 하므로
90
+ # 「일치」를 낼 자격이 없다.
91
+ hidden=$(_git -C "$repo" ls-files -v 2>&1) || { printf '%s\n' "$hidden" >&2; return 1; }
92
+ if printf '%s\n' "$hidden" | grep -qE '^[hS]'; then
93
+ printf 'assume-unchanged/skip-worktree 비트가 켜져 있다 — 지문이 변경을 못 본다\n' >&2
94
+ return 2
95
+ fi
96
+
97
+ # ⓐ-1 tracked 변경: **줄 수가 아니라 패치 내용**. staged 포함(HEAD 기준).
98
+ diff_patch=$(_git -C "$repo" diff HEAD 2>&1) || { printf '%s\n' "$diff_patch" >&2; return 1; }
99
+
100
+ # ⓐ-2 서브모듈: `git diff HEAD` 는 서브모듈 포인터만 보고 **그 안의 dirty 내용은 못 본다**
101
+ # (cross-family codex, 재현 확인 — 서브모듈 파일을 고쳐도 MATCH 였다).
102
+ # `--recursive` 상태 문자열이 dirty 표식(`+`)과 SHA 를 함께 낸다.
103
+ # 🟥 `submodule status` **만으로는 부족하다**: 그 줄은 서브모듈의 HEAD 를 말하므로,
104
+ # 서브모듈 워킹트리를 고치기만 하면(커밋 없이) SHA 가 안 바뀌어 여전히 MATCH 였다
105
+ # (수리 1차본이 그랬고, 재현으로 잡았다). 각 서브모듈 **안으로 내려가** 상태와
106
+ # 패치 내용을 함께 해시에 넣는다.
107
+ subs=$(_git -C "$repo" submodule status --recursive 2>&1) || { printf '%s\n' "$subs" >&2; return 1; }
108
+ if [ -n "$subs" ]; then
109
+ # 🟥 미초기화 서브모듈(`-<sha> path`)은 `foreach` 가 **건너뛴다**(agy 지적). 그 안의
110
+ # 내용은 지문에 한 글자도 안 들어가므로 «같다» 를 낼 자격이 없다.
111
+ if printf '%s\n' "$subs" | grep -q '^-'; then
112
+ printf '미초기화 서브모듈이 있다 — 그 안을 못 잰다(git submodule update --init 후 재시도)\n' >&2
113
+ return 2
114
+ fi
115
+ local subdetail
116
+ subdetail=$(_git -C "$repo" submodule foreach --recursive --quiet \
117
+ 'git status --porcelain --untracked-files=all && git diff HEAD' 2>&1) \
118
+ || { printf '%s\n' "$subdetail" >&2; return 1; }
119
+ subs="${subs}"$'\n'"${subdetail}"
120
+ fi
121
+
122
+ # ⓐ-3 untracked(무시되지 않은 것): 경로 + 내용 해시.
123
+ # 🟥 **파이프 안에서 목록을 만들지 않는다** — `git ls-files -o | while … | sort` 는 git 의
124
+ # rc 를 마지막 `sort` 의 rc 로 덮는다. 목록 취득이 실패해도 «untracked 없음» 으로 접혀
125
+ # MATCH 가 나왔다(재현 확인). 목록을 먼저 변수로 받고 rc 를 본다.
126
+ # ★NUL 구분자는 `$( )` 가 삼킨다 — 목록은 **임시 파일**로 받아야 rc 도 보고 구분자도 산다.
127
+ files=$(mktemp) || return 3
128
+ _git -C "$repo" ls-files -o --exclude-standard -z >"$files" 2>/dev/null; rc=$?
129
+ if [ "$rc" -ne 0 ]; then
130
+ rm -f "$files"
131
+ printf 'ls-files -o 실패(rc=%s) — untracked 를 못 쟀다\n' "$rc" >&2
132
+ return 1
133
+ fi
134
+
135
+ untracked=""
136
+ while IFS= read -r -d '' f; do
137
+ local entry
138
+ if [ -L "$repo/$f" ]; then
139
+ # 🟥 심볼릭 링크는 **가리키는 문자열**이 내용이다. `-f` 로만 보면 깨진 링크의
140
+ # 재지정(retarget)을 통째로 놓친다(재현 확인).
141
+ entry="LINK $(readlink "$repo/$f" 2>/dev/null || printf 'UNREADABLE')"
142
+ elif [ -d "$repo/$f" ]; then
143
+ # 🟥 untracked **디렉토리**는 `ls-files -o` 가 한 항목으로 접는다(중첩 git repo 가
144
+ # 대표 사례). 그 안의 내용 변경을 이 지문은 못 본다 ⇒ «놓쳤다» 를 «같다» 로
145
+ # 렌더하지 않고 **잴 수 없음(UNKNOWN)으로 fail-closed** 한다.
146
+ printf 'untracked 디렉토리를 만났다(%s) — 내용까지 못 잰다\n' "$f" >&2
147
+ rm -f "$files"; return 2
148
+ elif [ -f "$repo/$f" ]; then
149
+ # 🟥 **모드도 내용이다** (agy 지적). tracked 파일은 `git diff` 가 `old mode/new mode`
150
+ # 를 내지만 untracked 는 스트림만 해시해서, `chmod +x` 가 지문에 안 잡혔다.
151
+ # 이 레포는 실행 비트 소실로 훅이 조용히 무장해제된 이력이 있다
152
+ # ([[feedback_exec_bit_loss_disarms_hooks_silently]]).
153
+ local mode; if [ -x "$repo/$f" ]; then mode=x; else mode=-; fi
154
+ entry="$mode $(_sha <"$repo/$f")" || { rm -f "$files"; return 3; }
155
+ case "$entry" in "$mode ") rm -f "$files"; return 3 ;; esac
156
+ else
157
+ entry="NOTAFILE"
158
+ fi
159
+ untracked="${untracked}${f} ${entry}"$'\n'
160
+ done <"$files"
161
+ rm -f "$files"
162
+ untracked=$(printf '%s' "$untracked" | LC_ALL=C sort)
163
+
164
+ # 🟥 **필드를 이어붙이고 한 번 해시하지 않는다** (agy 지적): `---` 는 diff 패치의 표준
165
+ # 헤더(`--- a/file`)라 필드 경계가 내용에 나타날 수 있고, 그러면 서로 다른 상태가 같은
166
+ # 결합 문자열을 만들 여지가 생긴다. 각 필드를 **먼저 해시**해 길이를 고정한 뒤 잇는다.
167
+ local h_head h_diff h_subs h_untracked
168
+ h_head=$(printf '%s' "$head" | _sha) || return 3
169
+ h_diff=$(printf '%s' "$diff_patch" | _sha) || return 3
170
+ h_subs=$(printf '%s' "$subs" | _sha) || return 3
171
+ h_untracked=$(printf '%s' "$untracked" | _sha) || return 3
172
+
173
+ # 🟥 **TOCTOU** (agy 지적): 위 단계들은 원자적 스냅샷이 아니다. 1단계 뒤에 누가
174
+ # `git commit -a` 를 하면 «옛 트리 + 빈 diff» 라는 **한 순간도 존재한 적 없는 상태**가
175
+ # 핀으로 저장되고, 그 뒤 아무 변경이 없어도 verify 가 WRONG-TARGET 을 낸다.
176
+ # ⇒ 끝에서 트리를 다시 읽어 **읽는 동안 움직였으면 지문을 내지 않는다**(fail-closed).
177
+ local head2
178
+ head2=$(_git -C "$repo" rev-parse "HEAD^{tree}" 2>/dev/null) || return 1
179
+ if [ "$head2" != "$head" ]; then
180
+ printf '지문 계산 도중 트리가 움직였다 — 스냅샷이 일관되지 않는다\n' >&2
181
+ return 2
182
+ fi
183
+
184
+ combined=$(printf '%s %s %s %s\n' "$h_head" "$h_diff" "$h_subs" "$h_untracked" | _sha) || return 3
185
+ [ -n "$combined" ] || return 3
186
+ printf '%s\n' "$combined"
187
+ }
188
+
189
+ _pin_path() {
190
+ local repo="$1" label="$2" gitdir sid
191
+ gitdir=$(_git -C "$repo" rev-parse --git-common-dir 2>/dev/null) || return 1
192
+ case "$gitdir" in /*) : ;; *) gitdir="$repo/$gitdir" ;; esac
193
+ # ⓔ 세션 키 — branch_claim.sh:86-91 과 같은 상속 경로.
194
+ sid="${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-local}}"
195
+ printf '%s/fh-targets/%s\n' "$gitdir" "$sid"
196
+ }
197
+
198
+ main() {
199
+ local cmd="${1:-}" repo="${2:-}" label="${3:-}"
200
+ [ -n "$cmd" ] && [ -n "$repo" ] && [ -n "$label" ] || {
201
+ printf 'usage: target_freeze.sh {pin|verify|show} <repo> <label>\n' >&2; exit 10; }
202
+ [ -d "$repo" ] || _die_unknown "레포 경로가 없다: $repo"
203
+ _git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || _die_unknown "git 레포가 아니다: $repo"
204
+
205
+ local dir fp rc
206
+ dir=$(_pin_path "$repo" "$label") || _die_unknown "git-common-dir 를 못 읽었다"
207
+ local lkey; lkey=$(_label_key "$label") || _die_unknown "라벨 키를 못 만들었다(해시 도구 부재)"
208
+ local file="$dir/${lkey}.fp"
209
+
210
+ case "$cmd" in
211
+ pin)
212
+ fp=$(_fingerprint "$repo"); rc=$?
213
+ [ "$rc" -eq 0 ] && [ -n "$fp" ] || _die_unknown "지문을 못 냈다 (rc=$rc)"
214
+ mkdir -p "$dir" || _die_unknown "핀 저장 디렉토리를 못 만들었다"
215
+ printf '%s\n' "$fp" >"$file" || _die_unknown "핀을 못 썼다"
216
+ # 발주 프롬프트에 그대로 박을 한 줄.
217
+ printf 'TARGET-FINGERPRINT %s %s\n' "$label" "${fp:0:16}"
218
+ ;;
219
+ verify)
220
+ [ -f "$file" ] || _die_unknown "핀이 없다 — 발주 전에 pin 을 안 했다 ($label)"
221
+ local pinned; pinned=$(cat "$file") || _die_unknown "핀을 못 읽었다"
222
+ fp=$(_fingerprint "$repo"); rc=$?
223
+ [ "$rc" -eq 0 ] && [ -n "$fp" ] || _die_unknown "지문을 못 냈다 (rc=$rc)"
224
+ if [ "$fp" = "$pinned" ]; then
225
+ printf 'MATCH %s %s\n' "$label" "${fp:0:16}"; exit 0
226
+ fi
227
+ printf 'WRONG-TARGET %s — 핀 %s / 현재 %s\n' "$label" "${pinned:0:16}" "${fp:0:16}" >&2
228
+ printf '🟥 이 라운드의 판정을 **무효화**하라. 대상이 발주 이후 움직였다.\n' >&2
229
+ exit 1
230
+ ;;
231
+ show)
232
+ [ -f "$file" ] || _die_unknown "핀이 없다 ($label)"
233
+ printf 'PINNED %s %s\n' "$label" "$(cat "$file")"
234
+ ;;
235
+ *) _die_unknown "알 수 없는 커맨드: $cmd" ;;
236
+ esac
237
+ }
238
+
239
+ main "$@"
@@ -145,6 +145,23 @@ if [ -n "$MT_ROOT" ]; then
145
145
  else
146
146
  echo " ⏭ M4 [resolve-alias ] mate 실물 루트 없음 — SKIP (통과 아님)"
147
147
  fi
148
+ # 🟥 M2b/M2c — **대상 인자가 실제로 먹히는가** (2026-08-18 신설).
149
+ # 초판은 대상을 peer 진입점에 `$1` 로 넘겼는데 **peer 는 그 인자를 안 읽는다**
150
+ # (`TARGET` 하드코딩). 그래서 양성·음성 두 arm 이 **같은 파일**(peer 실물)을 재고 있었고,
151
+ # 음성의 초록은 판별력이 아니라 **실물이 마침 실패 상태**여서였다 — 캘리브레이션 쌍이
152
+ # 죽어 있었다. M2/M3 만으로는 그 상태를 못 가른다(둘 다 «기대대로» 나올 수 있다).
153
+ # ⇒ **픽스처를 변형해 판정이 따라오는지**를 본다. peer 실물 상태와 무관하게 성립한다.
154
+ if [ -n "$MT_ROOT" ]; then
155
+ _MUT="$T/mutated_positive.md"
156
+ # 양성 픽스처에서 T11(finding_id 어휘 제약) 한 줄만 뺀다.
157
+ grep -v '\^\[a-f0-9\]{6,64}\$' "$(dirname "$M")/fixtures/mate_agent_boundary_known_positive.md" >"$_MUT"
158
+ _lane M2b target-honored "양성에서 한 조항만 빼면 → FAIL (대상 인자가 실제로 먹힌다)" 1 \
159
+ "$(_rc bash "$M" --target "$_MUT")"
160
+ _lane M2c target-honored "원본 양성은 그대로 PASS (변형 레인의 컨트롤)" 0 \
161
+ "$(_rc bash "$M" --known-positive)"
162
+ else
163
+ echo " ⏭ M2b/M2c [target-honored] mate 실물 루트 없음 — SKIP (통과 아님)"
164
+ fi
148
165
  _lane_peer "$MT_ROOT" M5 enum "없는 대상 → HARNESS_ERROR(10). peer 헤더가 «위반 0 이 아니다» 라고 못 박은 값" 10 \
149
166
  "$(_rc bash "$M" --target "$T/does_not_exist.md")"
150
167
  _lane_peer "$MT_ROOT" M6 enum "알 수 없는 모드 → HARNESS_ERROR(10)" 10 "$(_rc bash "$M" --bogus-mode)"
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # test_evidence_root_psa_lanes.sh — pre-commit 의 기밀성 스캐너가 **워크트리에서도 오버라이드를
3
+ # 찾는가** (R4, 2026-08-18).
4
+ #
5
+ # WHY
6
+ # `tracks/` 는 `--git-common-dir` 기준 `EVIDENCE_ROOT` 로 옮겼는데, **똑같이 gitignored 인**
7
+ # `.claude/rules/.public-surface-patterns` 는 `$REPO_ROOT` 에 남아 있었다. 그래서 워크트리
8
+ # 커밋이 매번 **defaults-only** 로 돌았다 — 회사명·실명 토큰 클래스가 통째로 UNSCANNED 고,
9
+ # 그 상태는 **비차단 경고**라 조용히 지나간다. 반쪽-픽스의 전형
10
+ # ([[feedback_half_fix_propagation_boundary]]) 이고, EVIDENCE_ROOT 에는 **레인이 하나도
11
+ # 없었다** — 그래서 반쪽인 채로 출하됐다.
12
+ #
13
+ # Lanes
14
+ # E1 워크트리에서 커밋해도 오버라이드를 찾는다 (경고 배너 없음)
15
+ # E1n CONTROL — 오버라이드를 지우면 같은 실행에서 배너가 **뜬다**(계기가 살아 있다는 증거)
16
+ # E2 CONTROL — 메인 트리는 종전 그대로 동작한다 (회귀 없음)
17
+ set -u
18
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
+ pass=0; fail=0
20
+ t() { if [ "$2" = "$3" ]; then echo " ✅ $1"; pass=$((pass+1)); else echo " ❌ $1 — got [$3] want [$2]"; fail=$((fail+1)); fi }
21
+
22
+ BANNER='CONFIDENTIALITY COVERAGE REDUCED'
23
+
24
+ setup() { # $1 = base dir
25
+ local b="$1"
26
+ mkdir -p "$b/main"
27
+ ( cd "$b/main" && git init -q . && git config user.email t@t && git config user.name t
28
+ mkdir -p .claude/rules scripts templates/.git-hooks tracks/_meta
29
+ cp "$ROOT/scripts/psa_scan_lib.sh" scripts/
30
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" .claude/rules/ 2>/dev/null
31
+ cp "$ROOT/templates/.git-hooks/pre-commit" templates/.git-hooks/
32
+ printf 'tracks/\n.claude/rules/.public-surface-patterns\n' > .gitignore
33
+ printf 'x\n' > seed.md
34
+ git add -A && git commit -qm base ) >/dev/null 2>&1
35
+ # gitignored 오버라이드 — **메인 트리에만** 둔다 (실제 배치가 그렇다)
36
+ printf 'HIGH\tZZ_R4_OVERRIDE_TOKEN_ZZ\n' > "$b/main/.claude/rules/.public-surface-patterns"
37
+ }
38
+
39
+ # 🟥 고정 `/tmp` 경로 금지 (보안 패스 2026-08-18, MED·재현됨). `>/tmp/.r4out` 은 이름이
40
+ # 완전 고정이라, 공유 `/tmp` 를 쓰는 CI 러너·다중사용자 호스트에서 공격자가 먼저 심볼릭
41
+ # 링크를 걸어두면 `>` 가 링크를 따라가 **호출자 권한으로 임의 파일을 truncate + overwrite**
42
+ # 한다(sticky bit 는 «남의 파일 삭제»만 막지 «먼저 만든 링크»는 못 막는다). 이 스위트는
43
+ # 출하되고 `selfcheck.sh` 에 배선돼 있어 소비자가 셀프체크만 돌려도 발동한다.
44
+ R4OUT="$(mktemp "${TMPDIR:-/tmp}/fh_r4out.XXXXXX")" || exit 1
45
+ trap 'rm -f "$R4OUT"' EXIT
46
+
47
+ run_hook() { # $1 = 실행 디렉터리 → echoes banner|clean
48
+ local out
49
+ ( cd "$1" && printf 'hello\n' > probe.md && git add probe.md \
50
+ && bash templates/.git-hooks/pre-commit ) >"$R4OUT" 2>&1
51
+ if grep -q "$BANNER" "$R4OUT"; then echo banner; else echo clean; fi
52
+ }
53
+
54
+ B=$(mktemp -d); setup "$B"
55
+ ( cd "$B/main" && git worktree add -q "$B/wt" -b wtbranch ) >/dev/null 2>&1
56
+ # 워크트리에는 templates/ 가 체크아웃돼 있다(tracked). 오버라이드는 없다(gitignored).
57
+ t "E1 워크트리에서도 오버라이드를 찾는다" "clean" "$(run_hook "$B/wt")"
58
+ t "E2 control — 메인 트리 회귀 없음" "clean" "$(run_hook "$B/main")"
59
+ mv "$B/main/.claude/rules/.public-surface-patterns" "$B/main/.claude/rules/.psa.bak"
60
+ t "E1n control — 오버라이드가 진짜 없으면 배너가 뜬다" "banner" "$(run_hook "$B/wt")"
61
+
62
+ # ── E3 · «있지만 무효» (cross-family/codex 지적, 2026-08-18) ─────────────────
63
+ # 🟥 지적: E1/E1n 은 **파일 없음**만 재고, EVIDENCE_ROOT 쪽 파일이 «있지만 비었거나 손상»인
64
+ # 경로를 안 잰다. 실측해 보니 기존 기계가 이미 **소리내며** 처리한다 —
65
+ # 빈 파일 → PSA_OVERRIDE_PRESENT=0 → 커버리지 축소 배너
66
+ # 손상 행 → PSA_BAD_ROWS>0 → «gate INACTIVE/INCOMPLETE»
67
+ # 🟥 그리고 **폴백하지 않는 것이 옳은 방향**이다: 워크트리에 있는 다른 파일로 조용히
68
+ # 갈아타면 «어느 패턴셋으로 쟀는지»가 로그에서 사라진다. 시끄럽게 알리는 쪽을 고정한다.
69
+ printf 'HIGH\tZZ_WT_LOCAL_ZZ\n' > "$B/wt/.claude/rules/.public-surface-patterns" # 워크트리 로컬본
70
+ : > "$B/main/.claude/rules/.public-surface-patterns" # EVIDENCE_ROOT = 빈 파일
71
+ t "E3 EVIDENCE_ROOT 가 비었으면 조용히 폴백하지 않고 알린다" "banner" "$(run_hook "$B/wt")"
72
+ printf 'no-tab-garbage-row\n' > "$B/main/.claude/rules/.public-surface-patterns"
73
+ out3=$( ( cd "$B/wt" && printf 'z\n' > p2.md && git add p2.md && bash templates/.git-hooks/pre-commit ) 2>&1 )
74
+ case "$out3" in *"INACTIVE/INCOMPLETE"*) r=announced ;; *) r=silent ;; esac
75
+ t "E3b 손상 행은 «게이트 무효»로 알린다(조용한 통과 아님)" "announced" "$r"
76
+ rm -rf "$B"
77
+
78
+ echo "----"; echo "evidence-root psa: $pass passed, $fail failed"; [ "$fail" -eq 0 ]
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # test_marker_standpoint_lanes.sh — regression fixtures for pre-commit's
3
+ # validate_standpoint_leg (typed standpoint verdict, 2026-08-17).
4
+ #
5
+ # WHY: `standpoint:` shipped 2026-08-14 with its value DELIBERATELY unvalidated — the doctrine
6
+ # said "mechanize on the first recorded false value, not before". That value has now been
7
+ # recorded (three, in fact): a `release_2.3.0` marker wrote `not-applicable` on a delta whose own
8
+ # grounds line concedes "소비자 install 의 게이트 수용은 바뀐다 (BREAKING 2건)", and two
9
+ # 2026-08-14 deltas that altered shipped gate scripts / a shipped SKILL.md carried no line at all.
10
+ # The threshold the doctrine set for this field is met; this lane is that mechanization.
11
+ #
12
+ # SCOPE — channel, not judgment (CLAUDE.md §Mechanization Boundary). These lanes assert the
13
+ # RECORD's properties: present · single · a member of the closed enum · not vacuous · not wearing
14
+ # the OTHER axis's tokens. They never assert the value is CORRECT.
15
+ #
16
+ # 🟥 NAMED RESIDUAL — the execution claim is WARN, not BLOCK. `tier2`/`tier2b`/`tier3` assert that
17
+ # something was EXECUTED in the target, and the doctrine calls that "the load-bearing half". A
18
+ # blocking check for it needs a vocabulary grep, and on first contact with the real corpus that
19
+ # grep over-blocked a legitimate marker whose grounds read "그 레포에서 실제로 호출해 양·음 arm 을
20
+ # 확인했다" — it simply did not know 「호출」. Over-blocking trains `--no-verify`, which would
21
+ # disarm the Destructive-Op gate living in the same hook, so the check warns. The cost is stated
22
+ # rather than hidden: a fabricated `tier2` with a fluent reason PASSES this lane. That hole is
23
+ # what §4-b (cross-family reads the marker) is for — it is not closed here.
24
+ #
25
+ # Fixtures assert BOTH directions (known-pair): every intended shape is admitted, and every hole
26
+ # the lane closes still blocks. Asserting only BLOCK cannot tell "blocked" from "blocked for the
27
+ # wrong reason"; asserting only PASS cannot see a lane that admits everything.
28
+ #
29
+ # Usage: bash scripts/test_marker_standpoint_lanes.sh Exit: 0 = all behave; 1 = regression.
30
+
31
+ set -uo pipefail
32
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
33
+ HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
34
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
35
+
36
+ sed -n '/^validate_standpoint_leg()/,/^}/p' "$HOOK" > "$T/fn.sh"
37
+
38
+ # Instrument calibration — an empty extraction would let every fixture "pass" against nothing.
39
+ if ! grep -q 'DEGRADED_NO_TARGET_ACCESS' "$T/fn.sh"; then
40
+ echo "❌ HARNESS-ERROR — validate_standpoint_leg did not extract from $HOOK."
41
+ echo " Fixtures below would measure an empty function. Aborting rather than reporting green."
42
+ exit 1
43
+ fi
44
+
45
+ FAIL=0
46
+ lane() { # $1 = id $2 = expect(BLOCK|PASS) $3 = marker body
47
+ local id="$1" expect="$2" body="$3" rc out
48
+ printf '%s\n' "$body" > "$T/m.marker"
49
+ out=$( bash -c 'set -uo pipefail; . "$1"; validate_standpoint_leg "$2"' _ "$T/fn.sh" "$T/m.marker" 2>&1 ); rc=$?
50
+ local got=PASS; [ $rc -ne 0 ] && got=BLOCK
51
+ if [ "$got" = "$expect" ]; then
52
+ printf ' ✅ %-34s %s\n' "$id" "$got"
53
+ else
54
+ printf ' ❌ %-34s expected %s, got %s\n %s\n' "$id" "$expect" "$got" "$(printf '%s' "$out" | head -2)"
55
+ FAIL=1
56
+ fi
57
+ }
58
+
59
+ echo "== standpoint lanes — BLOCK side (holes that must stay closed) =="
60
+ lane S1-missing BLOCK 'axis2-model: opus'
61
+ lane S2-duplicate BLOCK 'standpoint: tier1
62
+ standpoint: not-applicable — 나중에 고침'
63
+ lane S3-bare-na BLOCK 'standpoint: not-applicable'
64
+ lane S4-unknown-value BLOCK 'standpoint: tier9(qasp) — 새 등급을 지어냈다'
65
+ lane S5-crossfamily BLOCK 'standpoint: DEGRADED_SINGLE_FAMILY — codex 안 붙었다'
66
+ lane S6-panel-token BLOCK 'standpoint: panel(codex) — 축을 헷갈렸다'
67
+ lane S7-ungrounded-unk BLOCK 'standpoint: UNKNOWN'
68
+ lane S8-ungrounded-deg BLOCK 'standpoint: DEGRADED_NOT_RUN — 안 했음'
69
+
70
+ echo "== standpoint lanes — PASS side (legitimate shapes must not over-block) =="
71
+ lane N1-tier1 PASS 'standpoint: tier1'
72
+ lane N2-na-grounded PASS 'standpoint: not-applicable — FH 내부 문서이고 출하 표면·peer 계약을 안 건드린다(확인함)'
73
+ lane N3-tier1b PASS 'standpoint: tier1b(pmh-dev) — pmh-dev 정본 사본 직독 + 마커 42건 grep, 재분류 대상 0'
74
+ lane N4-tier2-exec PASS 'standpoint: tier2(qasp-dev) — 그 레포에서 스위트 실행, rc=0 / 3176 passed 출력 확인'
75
+ lane N5-deg-grounded PASS 'standpoint: DEGRADED_NOT_RUN — 표적 접근 가능했으나 안 돌렸다, 다음 라운드 이월'
76
+ lane N6-deg-targets PASS 'standpoint: DEGRADED_NOT_RUN(qasp-dev · pmh-dev) — 전파 자산이라 해당되나 대상 레포에서 아무것도 실행 안 했다'
77
+ lane N7-quoted-value PASS 'standpoint: "tier2(pmh-dev) — 그 레포 로컬 클론에서 실제로 실행, 양·음 arm 확인"'
78
+
79
+ echo "== WARN side — recorded, not blocking (the named residual above) =="
80
+ lane N8-tier2-vague PASS 'standpoint: tier2(pmh-dev) — 그쪽 레포 기준으로 판단했다'
81
+
82
+ [ $FAIL -eq 0 ] && { echo "✅ all standpoint lanes behave"; exit 0; }
83
+ echo "❌ standpoint lane regression"; exit 1
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # test_marker_thirdparty_lanes.sh — regression fixtures for pre-commit's validate_thirdparty_leg.
3
+ #
4
+ # WHY: `thirdparty:` shipped 2026-08-17 with presence checked and the VALUE unvalidated. Within the
5
+ # same day the author wrote it wrong TWICE — free prose about peer-session contact — while a peer
6
+ # session used the enum correctly 4/4. ⓓ3자대면 is confronting third-party PRIOR ART (does this
7
+ # claimed-new thing already exist outside the repo), not "I talked to another session". Nothing
8
+ # caught the substitution because nothing read the value.
9
+ #
10
+ # 🟥 THE AXIS HAS TWO HALVES and the field originally encoded only one — ① prior art
11
+ # (checked/none-found) and ② harness-level adversarial review (peer-review): put a model in a THIRD
12
+ # harness, wearing that harness's persona, and have it review the target's change. FH is the
13
+ # governor — it creates the situation, observes, judges; the 4-axis gate verifies the OPINION that
14
+ # came back, locally. Because ② had no value, records that meant ② leaked into free prose. That is
15
+ # the very defect this field was created to fix, recurring one level in.
16
+ #
17
+ # SCOPE — channel, not judgment. These lanes assert the record is an enum member with non-vacuous
18
+ # grounds. They never assert the search was thorough, and never assert that what a third party
19
+ # said is TRUE: the axis exists to surface information you could not predict, and adjudicating what
20
+ # comes back belongs to the other axes. That separation is the point of the 4-axis gate.
21
+ #
22
+ # Fixtures assert BOTH directions. The real corpus supplied both arms on its own — that is unusual
23
+ # and worth stating: peer-authored markers were the known-negatives, the author's own malformed
24
+ # ones were the known-positives.
25
+ #
26
+ # Usage: bash scripts/test_marker_thirdparty_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
+ sed -n '/^validate_thirdparty_leg()/,/^}/p' "$HOOK" > "$T/fn.sh"
33
+ if ! grep -q 'DEGRADED_NO_ACCESS' "$T/fn.sh"; then
34
+ echo "❌ HARNESS-ERROR — validate_thirdparty_leg did not extract from $HOOK."
35
+ echo " Fixtures below would measure an empty function. Aborting rather than reporting green."
36
+ exit 1
37
+ fi
38
+ FAIL=0
39
+ lane() { # $1=id $2=expect $3=body
40
+ local rc out got
41
+ printf '%s\n' "$3" > "$T/m.marker"
42
+ out=$( bash -c 'set -uo pipefail; . "$1"; validate_thirdparty_leg "$2"' _ "$T/fn.sh" "$T/m.marker" 2>&1 ); rc=$?
43
+ got=PASS; [ $rc -ne 0 ] && got=BLOCK
44
+ if [ "$got" = "$2" ]; then printf ' ✅ %-32s %s\n' "$1" "$got"
45
+ else printf ' ❌ %-32s expected %s, got %s\n %s\n' "$1" "$2" "$got" "$(printf '%s' "$out"|head -1)"; FAIL=1; fi
46
+ }
47
+
48
+ echo "== BLOCK side =="
49
+ lane T1-wrong-axis-semantics BLOCK 'thirdparty: 병렬 챔버 세션과 실교신 — 상호 정정 3건'
50
+ lane T2-bare-checked BLOCK 'thirdparty: checked'
51
+ lane T3-vacuous-grounds BLOCK 'thirdparty: none-found(없음)'
52
+ lane T4-standpoint-token BLOCK 'thirdparty: tier1b(pmh-dev) — 축을 헷갈렸다'
53
+ lane T5-crossfamily-token BLOCK 'thirdparty: panel(codex) — 축을 헷갈렸다'
54
+ lane T6-not-in-enum BLOCK 'thirdparty: yes'
55
+ lane T7-duplicate BLOCK 'thirdparty: UNKNOWN
56
+ thirdparty: none-found(나중에 고침)'
57
+
58
+ echo "== PASS side =="
59
+ lane N1-unknown-bare PASS 'thirdparty: UNKNOWN'
60
+ lane N2-checked-grounded PASS 'thirdparty: checked(promptfoo·DeepEval·garak 훑음 — 겹치는 선행 없음)'
61
+ lane N3-none-found PASS 'thirdparty: none-found(「등록 시점 검사기를 호출 시점에 배선」 조합의 외부 선행)'
62
+ lane N4-degraded-not-run PASS 'thirdparty: DEGRADED_NOT_RUN(외부 선행 미조사 — 필드 설계가 먼저였다)'
63
+ lane N5-not-applicable PASS 'thirdparty: not-applicable(이 저장소 고유 taxonomy 의 내부 규정 변경)'
64
+ lane N6-multiline-parenthetical PASS 'thirdparty: none-found(「정체성 승급 기준의 residency-제약 하 측정」이라는
65
+ 조합으로 외부 선행을 찾았고 겹치는 것이 없었다)'
66
+ # ② 하네스 단위 적대검증 — 이 축의 «다른 반쪽». 규격에 값이 없어서 산문으로 새던 자리다.
67
+ lane N8-peer-review PASS 'thirdparty: peer-review(gstack/harness-persona → qasp, 경계 가정 2건을 그쪽 입장에서 반증)'
68
+ lane N9-peer-review-degraded PASS 'thirdparty: DEGRADED_NO_ACCESS(gstack 로컬 클론 없음 — 제3 하네스 페르소나를 세울 수 없었다)'
69
+ lane T8-peer-review-vacuous BLOCK 'thirdparty: peer-review(했음)'
70
+ lane N7-absent PASS 'axis2-model: opus'
71
+
72
+ [ $FAIL -eq 0 ] && { echo "✅ all thirdparty lanes behave"; exit 0; }
73
+ echo "❌ thirdparty lane regression"; exit 1