@chrono-meta/fh-gate 2.7.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-commons",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-meta",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -8,6 +8,51 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
8
8
 
9
9
  ---
10
10
 
11
+ ### [2.8.0] — 2026-08-22
12
+
13
+ **자릿수 근거 (minor)**: 새 게이트 레인 · 행동을 바꾸는 교리 · 소비자 install 동작 변경.
14
+ major 아님(정체성 다섯이 전부 🟢 도, capability class 신설도 아니다).
15
+ **`BREAKING (gate)` 없음** — 새로 등록되는 훅은 `exit 0` 로 끝나는 표면화 훅이고 아무것도 차단하지 않는다.
16
+
17
+ #### 🟥 install-wizard 가 PriorArt 스니펫을 «항상 조용히 SKIP» 하고 있었다
18
+
19
+ `templates/settings.PriorArt.snippet.json` 이 형제 스니펫 넷과 달리 `project_settings_json`
20
+ 래퍼 없이 출하되고 있었고, install-wizard 머지 코드는 그 키가 없으면
21
+ `SKIP (no project_settings_json) … continue` 로 건너뛴다.
22
+ ⇒ **이 훅은 install-wizard 를 돌려도 한 번도 등록된 적이 없었다.**
23
+ 전수 실측: Compaction·FieldCanon·PreToolUse·SessionStart 래퍼 O · PriorArt 혼자 부재.
24
+
25
+ **소비자 영향**: 이제 install-wizard 가 이 훅을 실제로 등록한다. 새 메커니즘을 짓기 직전에
26
+ 「책장 먼저, 없으면 도서관」을 표면화하는 PreToolUse 훅이고, **차단하지 않는다**.
27
+
28
+ 회귀 앵커: `scripts/test_wizard_snippet_merge_lanes.sh` 에 **SHIP-1**(출하 스니펫 전수 스키마,
29
+ 0개 스캔 시 FAIL — 부재를 통과로 안 렌더) + **SHIP-2**(판별력 컨트롤) 신설. 되돌림 3단 검증됨.
30
+
31
+ #### door ④ 가 하네스 레벨 이식쌍을 기본값으로 낸다 — Step 3-b
32
+
33
+ `cross-ecosystem-synergy-detection` 이 «설치된 플러그인/스킬» 층만 냈다. 매핑 트랙 2+ 환경에서
34
+ **매핑된 필드 하네스끼리의 거버넌스 이식쌍**을 조건부 기본값으로 낸다. 발화 트리거 4행(단독 호출)
35
+ + Done When 2행(`harness-error` 는 NON-PASS, 세 번째 허용 상태가 아니다).
36
+ ⚠️ 재발 N=2 로 **이 레포 임계(N≥3) 미달**이며 **운영자 지시로** 기계화했다. 본문에 명시.
37
+
38
+ #### 「워크트리에서 4축 증거가 구조적으로 부재」 철회
39
+
40
+ `pre-commit` 이 `--git-common-dir` 로 메인 트리 증거를 잡는다(2026-08-18 수리, 08-22 첫 실사용).
41
+ 🟥 **운영 규칙은 불변 — 워크트리에서 FH 자산을 커밋하지 않는다.** 남은 것은 성립한 근거가 아니라
42
+ 열린 위험 영역이고 이유로 열거하지 않는다(초판이 열거했다가 적대검증에서 추가 주장 대부분에 결함).
43
+
44
+ #### 정체성 ④ 트리거 재정의 (운영자)
45
+
46
+ 「의문 표명」 예시 넷 → **요청·질문·제안 ∧ «조금이라도» 불확실**의 곱. ①은 채널·②는 판정.
47
+ 결말 셋 신설: **숙고만으로 닫힘** / 책장 / 도서관.
48
+
49
+ #### 기타
50
+
51
+ 디스패치 원장 3엔트리(66건 무기록 해소) · README 4언어 · `listing_watch.sh` · 3층 정본 ·
52
+ `docs/ETHOS.md` (2.7.0 이후 누적).
53
+
54
+ ---
55
+
11
56
  ## Plugin Level
12
57
 
13
58
  ### [2.7.0] — 2026-08-21
@@ -186,6 +231,7 @@ BREAKING (gate): edit_manifest.yaml 에 `date` 만 있는 엔트리가 있으면
186
231
  실측: 이 레포 매니페스트 411건 중 date-only **0건** — **건강한 매니페스트를 가진 소비자는 안 막힌다.** 막히는 것은 동시 append 로 손상된 경우이고, 그 손상은 종전에 **조용히 통과**했다.
187
232
 
188
233
  **동작 변경(차단 아님)**: 워크트리에서 FH 자산 커밋이 **가능해진다**(evidence-root 를 `--git-common-dir` 로 해석). 종전엔 `tracks/` 가 안 따라와 Axis 2+3·Axis 4 가 구조적으로 부재했다 — 즉 이 항목은 **완화**다.
234
+ > 🟥 **정정 (2026-08-22) — 위 줄을 「워크트리에서 커밋해도 된다」로 읽지 마라.** 기술적으로 *가능해진* 것은 맞으나, **운영 규칙은 여전히 «워크트리에서 FH 자산을 커밋하지 않는다»** 이다(`CLAUDE.md §Agent Dispatch Operation`). 이 항목이 없앤 것은 «증거에 도달 못 한다»는 **우연한 장벽** 하나뿐이고, 마커 provenance·동시 append 같은 남은 근거는 그대로다. 이 줄이 없으면 소비자는 출하 문서에서 내부 규칙과 반대되는 지시를 읽는다.
189
235
  ⚠️ **위 둘은 같은 사건의 앞뒤다.** 경로 변경으로 워크트리 소비자에게 게이트가 **처음 도달**하는데, 그 순간 `core.hooksPath` 가 **상대 경로**면 워크트리가 **자기 훅 사본**을 돌아 자기무력화가 가능하다.
190
236
  권고: `git config core.hooksPath "$(git rev-parse --show-toplevel)/templates/.git-hooks"`
191
237
 
@@ -39,6 +39,13 @@ Automatically discovers cross-invocable pairs in environments with multiple inst
39
39
  | "My tools feel disconnected and inconvenient" | Detect namespace/cwd fragmentation |
40
40
  | "Wouldn't combining these plugins be more powerful?" | Explore component combination synergies |
41
41
  | "I feel like there's synergy here — find it" | Automatic cross-invocation pair discovery |
42
+ | "Do my harnesses have synergy with each other?" | **Step 3-b standalone** — harness-level scan |
43
+ | "Can I transplant a governance pattern from another project?" | **Step 3-b standalone** |
44
+ | "Could I bring that project's gate over here?" | **Step 3-b standalone** |
45
+ | "하네스끼리 시너지" · "거버넌스 패턴 이식" · "다른 프로젝트 게이트 가져올 수 있나" | **Step 3-b standalone** |
46
+
47
+ > **Step 3-b fires standalone.** The last four rows call that step alone; they do not require the full
48
+ > door-④ pass. A request about *harnesses* is not a request about *installed plugins*.
42
49
 
43
50
  ## Processing Steps (7-step)
44
51
 
@@ -90,6 +97,34 @@ Call mechanism compatibility:
90
97
  - **Hook**: Auto-triggered (no direct cross-component calls)
91
98
  - **MCP**: Per-tool calls (namespace separated)
92
99
 
100
+ ### Step 3-b. Harness-level pattern-transplant scan (conditional default)
101
+
102
+ **Condition**: 2+ mapped field-harness tracks exist under `tracks/` (underscore-prefixed meta dirs
103
+ such as `_meta`/`_audit`/`_contrib`/`_chamber` do not count). Otherwise skip with one line.
104
+
105
+ Steps 1–3 look at **installed plugins/skills**. This step looks one layer up: the **governance
106
+ mechanisms of the mapped field harnesses themselves** — verification axes, verdict enums, gate exit
107
+ codes, persona systems — and finds pairs where one harness's mechanism is transplantable into another.
108
+
109
+ - Read each mapped track's **canon** (`CLAUDE.md` / `README.md`), not its code. Field-harness
110
+ vocabulary must not be normalized into general concepts — that is this repo's habitual failure.
111
+ - Emit **transplantable pairs only**: the two must share the *same problem shape*. Merely overlapping
112
+ subject matter is excluded.
113
+ - 🟥 Declare the read depth. "Read the top ~150 lines of CLAUDE.md" is a different claim from "read the
114
+ file"; say which, and mark anything not opened as **unverified**, never as absent.
115
+ - Store under its **own section** in the Step 6 result file, separate from the skill/agent pair table —
116
+ the two layers answer different questions and merging them hides which one produced a finding.
117
+
118
+ > **Boundary with Step 7**: Step 7 (proactive) may *trigger* Step 3-b; it does not replace its scan.
119
+ > Step 7 decides *when to offer*, Step 3-b decides *what to read and which transplant pairs come out*.
120
+ >
121
+ > **Why this is a default and not an extra**: door ④ was run live on 2026-08-21 and 2026-08-22 and both
122
+ > times produced only the skill/agent layer, while the operator was asking for this one. The wiring
123
+ > existed; its target was pinned to the old object. Provenance: `tracks/_meta/fh_signal_2026-08-21_unused-skills-are-unwired.md`
124
+ > (cause ⓓ) and `tracks/_meta/fh_signal_2026-08-22_operator.md`.
125
+ > ⚠️ **Mechanized at N=2 by explicit operator instruction; do not count as threshold-triggered
126
+ > mechanization** (`[[feedback_mechanize_at_repetition_prose_before]]`).
127
+
93
128
  ### Step 4. Synergy Grade Derivation (★~★★★)
94
129
 
95
130
  | Grade | Compatibility conditions |
@@ -245,6 +280,8 @@ External users automatically derive their own inventory via Step 1 `installed_pl
245
280
  | Condition | Check class |
246
281
  |---|---|
247
282
  | Steps 1~6 completed | **mandatory-pass** |
283
+ | **Step 3-b reported as `run(n pairs)` or `skipped(<2 mapped tracks)`** — never silently omitted; read depth declared per track | **mandatory-pass** |
284
+ | **`harness-error` is a NON-PASS, not a third accepted state.** Reporting the error satisfies the anti-silence rule and nothing else — it does not clear this row. Surface it and stop; do not let a status string stand in for a result | **mandatory-pass** |
248
285
  | Synergy ranking table (★~★★★) output | **mandatory-pass** |
249
286
  | **Step 7 state reported as one of `run` / `not-configured` / `harness-error`** — never silently omitted, and `harness-error` never reported as a skip | **mandatory-pass** |
250
287
  | Step 5 drift reported as `MATCH / DRIFTED / DRIFT-UNKNOWN(n=…)`, with `DRIFT-UNKNOWN` excluded from the match count; Step 2 `ABSENT` asset classes carried through as `ABSENT`, not `0` | **measured** (counts come from the Step 1/2 output, not from recall) |
@@ -40,7 +40,9 @@
40
40
  # 🟥 근본 해결이 아니다. 이건 **완화책**이다. 공유 체크아웃은 미커밋 워킹트리 혼입/유실도
41
41
  # 낳고(그건 커밋 게이트로 안 닫힌다), 근본 처방은 **세션당 worktree** 다.
42
42
  # (worktree 를 쓰면 이 claim 도 자동으로 트리별로 갈린다 — `--git-dir` 이 갈리므로.
43
- # 단 FH 자산 커밋은 worktree 에서 4축 증거가 구조적으로 부재하다는 별개 문제가 있다.)
43
+ # 단 FH 자산 커밋은 worktree 에서 따로 하지 마라. 🟥 「4축 증거가 구조적으로 부재」라는
44
+ # 옛 이유는 2026-08-22 철회다(pre-commit 의 evidence-root 가 --git-common-dir 로 메인 트리를
45
+ # 잡는다). 규칙은 그대로이고 남은 근거는 열려 있다 — CLAUDE.md §Agent Dispatch Operation.)
44
46
  #
45
47
  # 🟥 TOCTOU 잔여 (cross-family 지적, 미해결): `check` 와 git 이 실제로 ref 를 갱신하는 순간
46
48
  # 사이에 창이 있다. 그 사이 peer 가 switch 하면 이 게이트를 통과한 채 사고가 난다.
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # listing_watch.sh — 외부 큐레이션 목록에 낸 제출의 «조용한 상태 전이»를 잡는다.
3
+ #
4
+ # 왜 있나 (실측 근거, 2026-08-19):
5
+ # `awesome-claude-code` #1961 이 2026-06-07 에 validation-PASSED 였다가, 우리가 아무것도
6
+ # 안 건드린 채 2026-08-15 에 **validation-FAILED 로 뒤집혔다** — 그쪽이 허용 카테고리
7
+ # 목록을 바꾸고 봇이 기존 이슈를 재검증하면서 떨어진 것이다. 아무도 안 알려줬고,
8
+ # `validation-failed` 는 «리뷰 대기열에서 빠진» 상태다. 4일 뒤 **우연히** 발견했다.
9
+ # ⇒ 통과는 영구적이지 않고, 알림은 오지 않는다. 그래서 이쪽에서 스냅샷을 떠서 대조한다.
10
+ #
11
+ # 설계 규율 (전부 이 저장소의 실측에서 나온 것):
12
+ # · **항상 exit 0 + stdout 보고** — 훅은 비영 종료 시 stdout 이 버려지고 stderr 도 안 온다
13
+ # (`feedback_hook_nonzero_exit_is_silent`). 이 스크립트는 판정을 «출력»하지 종료코드로
14
+ # 말하지 않는다.
15
+ # · **미측정을 0 으로 접지 않는다** — 상태는 넷이다: DISABLED / SKIPPED / FAILED / EXECUTED
16
+ # (`feedback_not_found_is_not_zero_family`). `gh` 가 없으면 «통과» 가 아니라 «SKIPPED» 다.
17
+ # · **파이프로 판정하지 않는다** — `out=$(cmd); rc=$?` 형태만 쓴다
18
+ # (`feedback_pipefail_fallback_disarms_guard`).
19
+ # · **부재 주장엔 컨트롤** — `--selftest` 가 known-positive/known-negative 를 돌린다
20
+ # (`feedback_absence_measurement_needs_control`).
21
+ # · bash 3.2 (macOS 기본) 호환 — 연관배열 안 쓴다.
22
+ #
23
+ # 채널 목록은 **이 파일에 안 박는다** — 운영자의 실제 제출은 사설 정보다.
24
+ # 매니페스트: tracks/_meta/listing_watch_channels.tsv (gitignored)
25
+ # 형식(TAB 구분, `#` 주석): owner/repo <TAB> number <TAB> pr|issue <TAB> 목록에서_찾을_문자열
26
+ #
27
+ # 사용: bash scripts/listing_watch.sh [--force] [--selftest]
28
+
29
+ HUB="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
30
+ MANIFEST="$HUB/tracks/_meta/listing_watch_channels.tsv"
31
+ STATE="$HUB/tracks/_meta/.listing_watch_state.tsv"
32
+ THROTTLE_H=24
33
+ FORCE=0; SELFTEST=0
34
+ for a in "$@"; do
35
+ case "$a" in --force) FORCE=1 ;; --selftest) SELFTEST=1 ;; esac
36
+ done
37
+
38
+ say() { printf '%s\n' "$*"; }
39
+
40
+ # 🟥 **`stat -f %m` 을 «먼저» 쓰면 리눅스에서 조용히 깨진다** — GNU stat 의 `-f` 는
41
+ # «파일시스템 정보»라 **실패가 아니라 성공하며 다른 값을 뱉고**, 그래서 `||` 폴백이 안 탄다.
42
+ # CI 가 실제로 그렇게 잡았다(`Type: ext2/ext3 … syntax error in expression`).
43
+ # ⇒ 순서를 뒤집는 것으로 끝내지 않고 **숫자인지 검증**한다. 순서만 바꾸면 반대 플랫폼에서
44
+ # 같은 얼굴이 다시 난다. `[[feedback_wiring_surfaces_hidden_failures]]`
45
+ _mtime() {
46
+ local m
47
+ m=$(stat -c %Y "$1" 2>/dev/null)
48
+ case "$m" in ''|*[!0-9]*) m=$(stat -f %m "$1" 2>/dev/null) ;; esac
49
+ case "$m" in ''|*[!0-9]*) m=0 ;; esac
50
+ printf '%s' "$m"
51
+ }
52
+
53
+ # 🟥 grep 패턴의 `\t` 는 **이식되지 않는다** — GNU grep 은 리터럴 `t` 로 읽어 0건이 되고,
54
+ # 그러면 「FAILED 를 매번 보고한다」는 규율이 리눅스에서만 조용히 죽는다(CI 적발).
55
+ # 실제 탭 문자를 만들어 `-F` 로 쓴다.
56
+ TAB=$(printf '\t')
57
+
58
+ # 🟥 테스트 seam: 레인이 «gh 없는 호스트» 를 흉내내려고 PATH 를 깎으면 호스트에 따라
59
+ # 결과가 갈린다(리눅스의 /usr/bin 에 gh 가 있을 수 있다 — cross-family 지목).
60
+ # 명령 이름을 주입 가능하게 두면 레인이 호스트와 무관해진다.
61
+ GH="${LISTING_WATCH_GH:-gh}"
62
+
63
+ # ── 도구 가용성: 없으면 «SKIPPED, not passed» ─────────────────────────────
64
+ if ! command -v "$GH" >/dev/null 2>&1; then
65
+ say " ⚠️ [listing-watch] SKIPPED (not passed) — \`$GH\` 없음. 등재 상태를 **확인하지 못했다**."
66
+ exit 0
67
+ fi
68
+
69
+ # ── 한 채널의 현재 상태를 한 줄로 (owner/repo#num<TAB>STATE<TAB>labels<TAB>listed) ──
70
+ probe() { # $1=repo $2=num $3=kind $4=needle
71
+ local repo="$1" num="$2" kind="$3" needle="$4" st lab listed out rc
72
+ # 🟥 라벨 «개수» 를 같이 싣는다 — join(",") 만으로는 ["a,b"](1개) 와 ["a","b"](2개) 가
73
+ # 구분이 안 돼 진짜 라벨 전이가 스냅샷에서 같게 비친다(cross-family 지목).
74
+ out=$($GH api "repos/$repo/issues/$num" --jq '"\(.state)|\(.pull_request.merged_at // "null")|\([.labels[].name]|length)|\([.labels[].name] | sort | join(","))"' 2>&1); rc=$?
75
+ if [ $rc -ne 0 ]; then printf '%s#%s\tFAILED\t-\t-\n' "$repo" "$num"; return 0; fi
76
+ st=$(printf '%s' "$out" | cut -d'|' -f1)
77
+ local merged n_lab; merged=$(printf '%s' "$out" | cut -d'|' -f2); n_lab=$(printf '%s' "$out" | cut -d'|' -f3)
78
+ lab=$(printf '%s' "$out" | cut -d'|' -f4-)
79
+ [ "$merged" != "null" ] && st="MERGED"
80
+ [ -z "$lab" ] && lab="-"
81
+ lab="${n_lab}:${lab}"
82
+ listed="unknown"
83
+ if [ -n "$needle" ] && [ "$needle" != "-" ]; then
84
+ local body brc dec
85
+ body=$($GH api "repos/$repo/contents/README.md" --jq '.content' 2>/dev/null); brc=$?
86
+ if [ $brc -eq 0 ] && [ -n "$body" ]; then
87
+ # base64 플래그가 갈린다: GNU/신 macOS 는 -d, 구 macOS 는 -D. 둘 다 시도한다.
88
+ dec=$(printf '%s' "$body" | base64 -d 2>/dev/null) || true
89
+ [ -z "$dec" ] && dec=$(printf '%s' "$body" | base64 -D 2>/dev/null) || true
90
+ if [ -z "$dec" ]; then
91
+ listed="unreadable" # 🟥 디코드 실패를 «absent» 로 접으면 계기 고장이 «미등재» 로 읽힌다
92
+ elif printf '%s' "$dec" | grep -qF -- "$needle"; then listed="LISTED"
93
+ else listed="absent"; fi
94
+ else
95
+ listed="unreadable" # ← «없음» 이 아니라 «못 읽었음». 접지 않는다
96
+ fi
97
+ fi
98
+ printf '%s#%s\t%s\t%s\t%s\n' "$repo" "$num" "$st" "$lab" "$listed"
99
+ }
100
+
101
+ # ── 컨트롤: 계기가 살아 있는지 (known-positive / known-negative) ──────────
102
+ if [ "$SELFTEST" = "1" ]; then
103
+ say "[listing-watch selftest] 컨트롤 2종"
104
+ kp=$(probe "walkinglabs/awesome-harness-engineering" 33 pr "forge-harness")
105
+ say " known-positive walkinglabs#33 (MERGED 로 알려짐): $kp"
106
+ case "$kp" in *MERGED*) say " ✅ 양성 검출" ;; *) say " ❌ 계기 고장 — 알려진 MERGED 를 못 읽었다" ;; esac
107
+ kn=$(probe "walkinglabs/awesome-harness-engineering" 99999999 pr "-")
108
+ say " known-negative 없는 번호 #99999999: $kn"
109
+ case "$kn" in *FAILED*) say " ✅ 없는 것을 «없다»로 (지어내지 않음)" ;; *) say " ❌ 없는 것에 상태를 만들어냈다" ;; esac
110
+ exit 0
111
+ fi
112
+
113
+ # ── 매니페스트 없으면 DISABLED (기능 자체가 안 켜진 것 — 실패가 아니다) ──
114
+ if [ ! -f "$MANIFEST" ]; then
115
+ say " ℹ️ [listing-watch] DISABLED — 채널 매니페스트 없음 ($MANIFEST)."
116
+ say " 켜려면 TAB 구분으로: owner/repo <TAB> 번호 <TAB> pr|issue <TAB> 목록에서_찾을_문자열"
117
+ exit 0
118
+ fi
119
+
120
+ # ── 스로틀: 24h 안에 돌았으면 조용히 넘어간다 ────────────────────────────
121
+ if [ "$FORCE" != "1" ] && [ -f "$STATE" ]; then
122
+ now=$(date +%s)
123
+ mt=$(_mtime "$STATE")
124
+ if [ $(( (now - mt) / 3600 )) -lt "$THROTTLE_H" ]; then exit 0; fi
125
+ fi
126
+
127
+ NEW=$(mktemp); CHANGED=0; ROWS=0
128
+ # 🟥 `IFS=$'\t' read` 를 쓰지 않는다 — 탭은 IFS 공백류라 **연속 탭이 합쳐져** 빈 칸이 있는
129
+ # 행에서 필드가 밀린다(cross-family 재현). 줄을 통째로 읽고 `cut` 으로 자른다.
130
+ # 🟥 `|| [ -n "$line" ]` 이 없으면 **개행 없는 마지막 행이 통째로 누락**된다(재현: 2행 중 1행).
131
+ while IFS= read -r line || [ -n "$line" ]; do
132
+ case "$line" in ''|\#*) continue ;; esac
133
+ repo=$(printf '%s' "$line" | cut -f1)
134
+ num=$(printf '%s' "$line" | cut -f2)
135
+ kind=$(printf '%s' "$line" | cut -f3)
136
+ needle=$(printf '%s' "$line" | cut -f4)
137
+ case "$repo" in ''|\#*) continue ;; esac
138
+ [ -z "$num" ] && continue
139
+ ROWS=$((ROWS+1))
140
+ line=$(probe "$repo" "$num" "${kind:-pr}" "${needle:--}")
141
+ printf '%s\n' "$line" >> "$NEW"
142
+ key=$(printf '%s' "$line" | cut -f1)
143
+ if [ -f "$STATE" ]; then
144
+ old=$(grep -F "$key " "$STATE" 2>/dev/null | head -1)
145
+ if [ -n "$old" ] && [ "$old" != "$line" ]; then
146
+ [ "$CHANGED" = "0" ] && say "" && say "🔔 [listing-watch] **등재 채널 상태가 바뀌었다** — 아무도 안 알려주는 종류다"
147
+ CHANGED=$((CHANGED+1))
148
+ say " $key"
149
+ say " 이전: $(printf '%s' "$old" | cut -f2-)"
150
+ say " 지금: $(printf '%s' "$line" | cut -f2-)"
151
+ fi
152
+ fi
153
+ done < "$MANIFEST"
154
+
155
+ if [ "$ROWS" = "0" ]; then
156
+ say " ℹ️ [listing-watch] DISABLED — 매니페스트에 유효한 채널 행이 0개다."
157
+ rm -f "$NEW"; exit 0
158
+ fi
159
+
160
+ # 🟥 **FAILED 는 «변화» 가 아니라 «상태» 로 매번 보고한다.** 안 그러면 인증 만료가 첫 실행에
161
+ # 스냅샷으로 굳고, 이후 FAILED==FAILED 라 **영구 무출력**이 된다 — 이 스크립트 헤더가
162
+ # 스스로 금지한 `not_found ≠ 0` 을 스크립트 자신이 저지르고 있었다(cross-family 지목, 재현됨).
163
+ # 🟥 `grep -c ... || echo 0` 을 쓰지 않는다: grep -c 는 무매치에서 **"0" 을 출력하고 exit 1** 이라
164
+ # 폴백이 "0\n0" 을 만들고 `[` 가 «integer expression expected» 로 죽는다(내 수리가 낸 결함).
165
+ # `[[feedback_pipefail_fallback_disarms_guard]]` 일가 — 값을 먼저 잡고 따로 판정한다.
166
+ NFAIL=$(grep -cF "${TAB}FAILED${TAB}" "$NEW" 2>/dev/null)
167
+ case "$NFAIL" in ''|*[!0-9]*) NFAIL=0 ;; esac
168
+ if [ "$NFAIL" -gt 0 ]; then
169
+ say ""
170
+ say " ⚠️ [listing-watch] 채널 $NFAIL 개를 **읽지 못했다**(FAILED) — «변화 없음» 이 아니다."
171
+ grep -F "${TAB}FAILED${TAB}" "$NEW" 2>/dev/null | while IFS= read -r l; do say " $(printf '%s' "$l" | cut -f1)"; done
172
+ say " 흔한 원인: gh 인증 만료(\`gh auth status\`) · 레포/번호 오타 · 비공개 전환."
173
+ say " 🟥 이 줄이 안 보일 때까지는 그 채널의 상태를 **모르는 것**이다."
174
+ fi
175
+
176
+ if [ ! -f "$STATE" ]; then
177
+ say " ℹ️ [listing-watch] 최초 실행 — 채널 $ROWS 개 스냅샷을 떴다. 다음 실행부터 «변화»를 낸다."
178
+ elif [ "$CHANGED" -gt 0 ]; then
179
+ say " ⚠️ 통과는 영구적이지 않다 — 남의 스키마가 바뀌면 과거 제출이 소급 무효가 된다."
180
+ say " 새로 내기 전에 라벨 이력부터: gh api repos/<r>/issues/<n>/timeline"
181
+ fi
182
+ if ! mv "$NEW" "$STATE" 2>/dev/null; then
183
+ say " ⚠️ [listing-watch] 스냅샷 **저장 실패** ($STATE) — 다음 실행이 이번 관측을 못 본다."
184
+ say " 변화 검출이 이 시점부터 **멈춘 것**이지 «변화 없음» 이 아니다."
185
+ rm -f "$NEW"
186
+ fi
187
+ exit 0
@@ -1064,6 +1064,21 @@ else
1064
1064
  fail=1
1065
1065
  fi
1066
1066
 
1067
+ # listing_watch — 같은 형태. 🟥 이 블록을 «레인을 짓는 같은 커밋에서» 붙인다: 바로 위 주석이
1068
+ # 기록한 「레인은 지었는데 selfcheck 배선을 안 했다」 재발이 최소 세 번이고, 그 셋 다 CI 나
1069
+ # lane-runner 가 뒤늦게 잡았다. 순서를 바꾸는 것이 유일한 처방이라 여기서 그렇게 한다.
1070
+ if [ ! -f scripts/listing_watch.sh ]; then
1071
+ echo "FAIL scripts/listing_watch.sh is in package.json files[] but absent — a deleted subject, not a skip"
1072
+ fail=1
1073
+ elif [ -f scripts/test_listing_watch_lanes.sh ]; then
1074
+ if ! bash scripts/test_listing_watch_lanes.sh; then
1075
+ fail=1
1076
+ fi
1077
+ else
1078
+ echo "FAIL test_listing_watch_lanes.sh: listing_watch.sh present but its anchor is missing"
1079
+ fail=1
1080
+ fi
1081
+
1067
1082
  # residency_admission — 같은 형태. 🟥 그리고 **또 같은 얼굴이었다**: 2026-08-20 에 이 레인을
1068
1083
  # 지으면서 여기 배선을 안 했고, `lane-runner` 가 «돌리는 게 없는 스위트는 산문이다» 로 CI 에서
1069
1084
  # 잡았다. 아래 target_freeze 주석이 기록한 그 형태의 **최소 세 번째**다 — 즉 이건 개인 부주의가
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ # test_listing_watch_lanes.sh — listing_watch.sh 의 레인.
3
+ # 🟥 네트워크를 안 쓴다: PATH 앞에 `gh` 스텁을 놓아 응답을 고정한다.
4
+ # (실망 사용 검증은 `--selftest` 가 known-pair 로 따로 한다 — 그건 네트워크가 필요하다)
5
+ HUB="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
6
+ SUT="$HUB/scripts/listing_watch.sh"
7
+ PASS=0; FAIL=0
8
+ ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
9
+ ng(){ FAIL=$((FAIL+1)); printf ' ❌ %s\n 기대: %s\n 실제: %s\n' "$1" "$2" "$3"; }
10
+
11
+ SB=$(mktemp -d); trap 'rm -rf "$SB"' EXIT
12
+ mkdir -p "$SB/tracks/_meta" "$SB/bin"
13
+
14
+ mkstub(){ # $1 = state|merged|badexit
15
+ cat > "$SB/bin/gh" <<STUB
16
+ #!/usr/bin/env bash
17
+ case "\$*" in
18
+ *"contents/README.md"*) printf '%s' "\$(printf 'chrono-meta/forge-harness' | base64)" ;;
19
+ # 🟥 본체 jq 는 4필드다: state|merged|라벨개수|라벨목록. 스텁이 3필드면 계기가 대상과 어긋난다.
20
+ *"issues/33"*) [ "$1" = "merged" ] && printf 'open|2026-08-19T06:48:10Z|0|' || printf 'open|null|1|validation-passed' ;;
21
+ *"issues/999"*) exit 1 ;;
22
+ *) exit 1 ;;
23
+ esac
24
+ STUB
25
+ chmod +x "$SB/bin/gh"
26
+ }
27
+
28
+ run(){ CLAUDE_PROJECT_DIR="$SB" PATH="$SB/bin:$PATH" bash "$SUT" "$@" 2>&1; }
29
+
30
+ # ── L1: gh 부재 → SKIPPED (pass 아님) ─────────────────────────────
31
+ printf 'walkinglabs/x\t33\tpr\tchrono-meta/forge-harness\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
32
+ # 🟥 두 번 틀린 자리다. ① 초판: `PATH=/nonexistent` 가 `bash` 까지 지워 «계기» 를 시험했다.
33
+ # ② 2판: `/bin:/usr/bin` 은 **호스트 의존**이다 — 리눅스에선 거기 진짜 gh 가 있을 수 있고
34
+ # 그러면 이 레인이 «대상» 이 아니라 «그 머신» 을 재게 된다(cross-family 지목).
35
+ # ③ 정본: 본체에 주입 seam(`LISTING_WATCH_GH`)을 두고 존재하지 않는 이름을 넣는다.
36
+ o=$(CLAUDE_PROJECT_DIR="$SB" LISTING_WATCH_GH="gh-does-not-exist-$$" bash "$SUT" --force 2>&1); r=$?
37
+ case "$o" in *SKIPPED*) ok "L1 gh 부재 → SKIPPED (not passed)" ;; *) ng "L1" "SKIPPED" "$o" ;; esac
38
+ [ "$r" = "0" ] && ok "L1b gh 부재여도 exit 0 (훅 무음 방지)" || ng "L1b" "0" "$r"
39
+
40
+ # ── L2: 매니페스트 부재 → DISABLED (실패 아님) ────────────────────
41
+ mkstub state
42
+ mv "$SB/tracks/_meta/listing_watch_channels.tsv" "$SB/m.bak"
43
+ o=$(run --force); case "$o" in *DISABLED*) ok "L2 매니페스트 부재 → DISABLED" ;; *) ng "L2" "DISABLED" "$o" ;; esac
44
+ mv "$SB/m.bak" "$SB/tracks/_meta/listing_watch_channels.tsv"
45
+
46
+ # ── L3: 주석만 있는 매니페스트 → DISABLED (0행을 «정상»으로 안 읽는다) ──
47
+ cp "$SB/tracks/_meta/listing_watch_channels.tsv" "$SB/m.bak"
48
+ printf '# only a comment\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
49
+ o=$(run --force); case "$o" in *DISABLED*) ok "L3 유효행 0 → DISABLED" ;; *) ng "L3" "DISABLED" "$o" ;; esac
50
+ cp "$SB/m.bak" "$SB/tracks/_meta/listing_watch_channels.tsv"
51
+
52
+ # ── L4: 최초 실행 → 스냅샷 생성 + 변화 «없음» ─────────────────────
53
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
54
+ o=$(run --force)
55
+ case "$o" in *최초*) ok "L4 최초 실행을 «변화» 로 오보하지 않는다" ;; *) ng "L4" "최초 실행 안내" "$o" ;; esac
56
+ [ -s "$SB/tracks/_meta/.listing_watch_state.tsv" ] && ok "L4b 스냅샷 파일 생성" || ng "L4b" "비어있지 않은 스냅샷" "(없음)"
57
+
58
+ # ── L5: 무변화 → 무출력 (오탐 0) ──────────────────────────────────
59
+ o=$(run --force)
60
+ [ -z "$o" ] && ok "L5 무변화 → 무출력" || ng "L5" "(빈 출력)" "$o"
61
+
62
+ # ── L6: 상태 전이 → 검출 ★ 이 레인이 본체다 ──────────────────────
63
+ mkstub merged
64
+ o=$(run --force)
65
+ # 🟥 배너 문구만 보면 «라벨만 바뀐 버그» 도 통과한다 — **관측된 전이 자체**를 단언한다.
66
+ case "$o" in *"바뀌었다"*) ok "L6 변화 배너 발화" ;; *) ng "L6" "변화 보고" "$o" ;; esac
67
+ case "$o" in *MERGED*) ok "L6b 전이 «내용»이 MERGED 로 찍힌다" ;; *) ng "L6b" "지금: … MERGED" "$o" ;; esac
68
+ case "$o" in *"이전:"*) ok "L6c 이전 상태도 같이 보여준다" ;; *) ng "L6c" "이전: …" "$o" ;; esac
69
+
70
+ # ── L7: 스로틀 — 24h 안이면 조용 ──────────────────────────────────
71
+ o=$(run) # --force 없음
72
+ [ -z "$o" ] && ok "L7 스로틀 동작 (24h 내 재실행 조용)" || ng "L7" "(빈 출력)" "$o"
73
+
74
+ # ── L8: API 실패를 «없음» 이 아니라 FAILED 로 ─────────────────────
75
+ printf 'walkinglabs/x\t999\tpr\t-\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
76
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
77
+ run --force >/dev/null
78
+ s=$(cat "$SB/tracks/_meta/.listing_watch_state.tsv" 2>/dev/null)
79
+ case "$s" in *FAILED*) ok "L8 API 실패 → FAILED (0 으로 안 접는다)" ;; *) ng "L8" "FAILED" "$s" ;; esac
80
+
81
+ # ── L9: 🟥 FAILED 반복이 «무출력» 이 되면 안 된다 (자기 헤더 배반 방지) ──
82
+ cat > "$SB/bin/gh" <<'STUB'
83
+ #!/usr/bin/env bash
84
+ exit 1
85
+ STUB
86
+ chmod +x "$SB/bin/gh"
87
+ printf 'r/x\t1\tpr\t-\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
88
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
89
+ run --force >/dev/null 2>&1 # 1회차: 스냅샷이 FAILED 로 굳는다
90
+ o=$(run --force) # 2회차: 변화는 «없다» — 그래도 말해야 한다
91
+ case "$o" in *FAILED*|*"읽지 못했다"*) ok "L9 FAILED 반복도 매번 보고 (인증만료≠변화없음)" ;; *) ng "L9" "FAILED 보고" "${o:-(무출력)}" ;; esac
92
+
93
+ # ── L10: 연속 탭(빈 필드)에서 needle 이 밀리지 않는다 ──
94
+ mkstub state
95
+ printf 'walkinglabs/x\t33\t\tchrono-meta/forge-harness\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
96
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
97
+ run --force >/dev/null 2>&1
98
+ s9=$(cat "$SB/tracks/_meta/.listing_watch_state.tsv" 2>/dev/null)
99
+ case "$s9" in *LISTED*) ok "L10 빈 필드 있어도 needle 이 제자리 (IFS 탭 축약 방지)" ;; *) ng "L10" "LISTED" "$s9" ;; esac
100
+
101
+ # ── L11: 개행 없는 마지막 행도 읽는다 ──
102
+ printf 'walkinglabs/x\t33\tpr\tchrono-meta/forge-harness\nwalkinglabs/y\t33\tpr\tchrono-meta/forge-harness' > "$SB/tracks/_meta/listing_watch_channels.tsv"
103
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
104
+ run --force >/dev/null 2>&1
105
+ n=$(grep -c . "$SB/tracks/_meta/.listing_watch_state.tsv" 2>/dev/null || echo 0)
106
+ [ "$n" = "2" ] && ok "L11 개행 없는 마지막 행 누락 안 함 (2/2)" || ng "L11" "2행" "${n}행"
107
+
108
+ # ── L12: 라벨 개수를 실어 ["a,b"] 와 ["a","b"] 를 가른다 ──
109
+ case "$s9" in *" "[0-9]*:*) ok "L12 라벨 필드가 «개수:목록» 형태" ;; *) ng "L12" "N:labels" "$s9" ;; esac
110
+
111
+ # ── L13/L14: 🟥 **반대 플랫폼 흉내** — 이 둘이 이 스위트의 하중선이다.
112
+ # 2026-08-21 실측: 로컬(macOS) 16/16 초록인데 **CI(리눅스)에서 L7·L9 가 죽었다.**
113
+ # 원인 둘 다 «한 플랫폼에서만 나는» 형태였다:
114
+ # ⓐ `stat -f %m` 은 GNU 에서 **실패가 아니라 성공하며 파일시스템 정보를 뱉는다**
115
+ # → `||` 폴백이 안 타고 산술식이 깨진다
116
+ # ⓑ grep 패턴의 `\t` 를 GNU grep 은 **리터럴 t** 로 읽는다 → FAILED 보고가 0건
117
+ # 로컬 초록이 증거가 못 된다는 뜻이므로, **여기서 반대 플랫폼을 스텁으로 만든다.**
118
+ mkstub state
119
+ printf 'walkinglabs/x\t33\tpr\tchrono-meta/forge-harness\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
120
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
121
+
122
+ # L13: `stat -f` 가 «성공하면서 쓰레기» 를 내는 플랫폼(=GNU)에서도 스로틀이 산다
123
+ cat > "$SB/bin/stat" <<'STUB'
124
+ #!/usr/bin/env bash
125
+ # GNU 흉내: -f 는 성공하며 파일시스템 정보(비숫자)를 낸다 · -c %Y 만 mtime
126
+ if [ "$1" = "-f" ]; then echo " ID: deadbeef Namelen: 255 Type: ext2/ext3"; exit 0; fi
127
+ if [ "$1" = "-c" ]; then echo 1000000000; exit 0; fi
128
+ exit 1
129
+ STUB
130
+ chmod +x "$SB/bin/stat"
131
+ run --force >/dev/null 2>&1 # 스냅샷 생성
132
+ o=$(run 2>&1) # --force 없음 → 스로틀이 조용해야
133
+ case "$o" in
134
+ *"syntax error"*|*"ext2"*) ng "L13 GNU-stat 흉내에서 스로틀 붕괴" "(빈 출력)" "$o" ;;
135
+ "") ok "L13 GNU-stat 흉내에서도 스로틀 정상 (비숫자 mtime 방어)" ;;
136
+ *) ng "L13" "(빈 출력)" "$o" ;;
137
+ esac
138
+ rm -f "$SB/bin/stat"
139
+
140
+ # L14: grep 패턴의 `\t` 를 리터럴 t 로 읽는 플랫폼(=GNU grep)에서도 FAILED 가 보고된다
141
+ # → 실제 탭을 쓰는지 확인. 스크립트가 `\t` 를 패턴에 쓰면 여기서 죽는다.
142
+ cat > "$SB/bin/gh" <<'STUB'
143
+ #!/usr/bin/env bash
144
+ exit 1
145
+ STUB
146
+ chmod +x "$SB/bin/gh"
147
+ cat > "$SB/bin/grep" <<'STUB'
148
+ #!/usr/bin/env bash
149
+ # GNU 흉내: 패턴 안의 백슬래시-t 는 «리터럴 t» 다 (탭이 아니다)
150
+ args=(); for a in "$@"; do case "$a" in *'\t'*) a="${a//\\t/t}" ;; esac; args+=("$a"); done
151
+ exec /usr/bin/grep "${args[@]}"
152
+ STUB
153
+ chmod +x "$SB/bin/grep"
154
+ printf 'r/x\t1\tpr\t-\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
155
+ rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
156
+ run --force >/dev/null 2>&1
157
+ o=$(run --force 2>&1)
158
+ case "$o" in *"읽지 못했다"*|*FAILED*) ok "L14 GNU-grep 흉내에서도 FAILED 보고 (실제 탭 사용)" ;; *) ng "L14 grep \\t 이식성" "FAILED 보고" "${o:-(무출력)}" ;; esac
159
+ rm -f "$SB/bin/grep"
160
+
161
+ printf '\nLISTING_WATCH_LANES: %d passed, %d failed\n' "$PASS" "$FAIL"
162
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -282,6 +282,46 @@ SessionStart REGISTRATION of the script that reports on them. So the wizard's un
282
282
  message (GAP 1) is never contradicted by anything, and the check whose job is announcing an unwired \
283
283
  machine can itself be unwired silently."
284
284
 
285
+ # ── SHIP-1/2: 출하되는 모든 스니펫이 머지 코드가 읽는 키를 갖고 있나 (2026-08-22 신설) ─────
286
+ # 🟥 왜 이 레인이 뒤늦게 생겼나 — 이 파일은 «머지 코드가 옳게 동작하나» 만 재고 있었고,
287
+ # «출하물이 그 코드가 읽는 모양인가» 는 아무도 안 봤다. 그 사이
288
+ # templates/settings.PriorArt.snippet.json 이 project_settings_json 래퍼 **없이** 출하됐고,
289
+ # 머지 코드는 그 키가 없으면 `SKIP (no project_settings_json)` 로 **조용히 건너뛴다**.
290
+ # 결과: install-wizard 를 돌려도 그 훅은 **한 번도 등록된 적이 없었다.**
291
+ # 실측 2026-08-22: 스니펫 5개 중 4개 래퍼 O · PriorArt 만 부재(전수, 컨트롤 = 나머지 넷).
292
+ # ⇒ 레인이 «구현» 만 보고 «출하물» 을 안 보면 이 클래스는 영원히 초록이다.
293
+ echo
294
+ echo "── SHIP: shipped-snippet schema conformance ──"
295
+ _ship_total=0; _ship_bad=0
296
+ for _snip in "$ROOT"/templates/settings.*.snippet.json; do
297
+ [ -f "$_snip" ] || continue
298
+ _ship_total=$((_ship_total+1))
299
+ if ! python3 -c "import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if isinstance(d.get('project_settings_json'),dict) else 1)" "$_snip" 2>/dev/null; then
300
+ _ship_bad=$((_ship_bad+1))
301
+ _fail "SHIP-1 $(basename "$_snip") has no dict 'project_settings_json' — install-wizard will SKIP it silently"
302
+ fi
303
+ done
304
+ if [ "$_ship_total" -eq 0 ]; then
305
+ # 부재를 통과로 렌더하지 않는다 — 0 개를 스캔하고 PASS 하면 계기가 죽은 것이다
306
+ _fail "SHIP-1 scanned ZERO snippets — instrument dead, not a pass (glob or ROOT wrong)"
307
+ elif [ "$_ship_bad" -eq 0 ]; then
308
+ _pass "SHIP-1 all $_ship_total shipped snippet(s) carry a dict 'project_settings_json'"
309
+ fi
310
+
311
+ # SHIP-2 컨트롤 — 이 검사가 실제로 판별하나. 래퍼를 뗀 픽스처는 반드시 걸려야 한다.
312
+ _ctl=$(mktemp -t fh_ship_ctl.XXXXXX) || _ctl=""
313
+ if [ -n "$_ctl" ]; then
314
+ printf '%s\n' '{"hooks":{"PreToolUse":[]}}' > "$_ctl"
315
+ if python3 -c "import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if isinstance(d.get('project_settings_json'),dict) else 1)" "$_ctl" 2>/dev/null; then
316
+ _fail "SHIP-2 CONTROL — a wrapper-less fixture PASSED the check; the check does not discriminate"
317
+ else
318
+ _pass "SHIP-2 CONTROL — wrapper-less fixture is caught (check discriminates)"
319
+ fi
320
+ rm -f "$_ctl"
321
+ else
322
+ _fail "SHIP-2 CONTROL could not run (mktemp failed) — reported as a gap, not a pass"
323
+ fi
324
+
285
325
  echo
286
326
  echo "──────────────────────────────────────────────"
287
327
  if [ "$FAILED" -ne 0 ]; then
@@ -1,15 +1,44 @@
1
1
  {
2
- "hooks": {
3
- "PreToolUse": [
4
- {
5
- "matcher": "Write|Bash",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/prior_art_prompt.sh\""
10
- }
11
- ]
12
- }
13
- ]
2
+ "_README": [
3
+ "prior-art 프롬프트 훅 스니펫 — install-wizard 가 사용자 로컬 .claude/settings.json 에 머지하는",
4
+ "tracked SOURCE 다. 이 레포의 .claude/settings*.json 은 전부 gitignored 라 **훅 등록 자체를",
5
+ "tracked 로 둘 수 없다.** 머지 단계가 없으면 훅은 돌지 않는다 —",
6
+ "**스크립트를 출하하는 것과 배선하는 것은 같은 명제가 아니다.**",
7
+ "",
8
+ "🟥 왜 이 래퍼가 뒤늦게 생겼나 (2026-08-22 실측, 컨트롤 동반):",
9
+ " 파일은 형제 스니펫 넷과 달리 `project_settings_json` 래퍼 **없이** 출하되고 있었다.",
10
+ " install-wizard 의 실제 머지 코드(install-wizard/SKILL_detail.md:642-645)는 그 키가 없으면",
11
+ " print(\"SKIP (no project_settings_json)\", ...); continue",
12
+ " 로 **조용히 건너뛴다.** 즉 이 훅은 「아직 안 돌렸다」가 아니라",
13
+ " **「install-wizard 를 돌려도 항상 스킵돼서 한 번도 등록된 적이 없다」** 였다.",
14
+ " 전수 실측: Compaction·FieldCanon·PreToolUse·SessionStart = 래퍼 O · PriorArt = 혼자 부재.",
15
+ " 머지 코드를 그대로 복사해 실행한 수리 전/후 대조로 SKIP→등록 전이를 확인했다.",
16
+ " ⇒ built_but_not_wired 보다 나쁘다. **설치기가 자기 출하물을 조용히 건너뛰는** 형태다.",
17
+ " 회귀 앵커: scripts/test_wizard_snippet_merge_lanes.sh 의 전수 래퍼 검사(이 수리와 같이 들어갔다).",
18
+ "",
19
+ "무엇을 하나: 새 메커니즘을 짓기 직전에 «책장 먼저, 없으면 도서관» 을 띄운다.",
20
+ "정체성 ④(프런티어 답습)의 **빌드 시점 격발 표면**이다.",
21
+ "🟥 명시된 잔여: matcher 가 `Write|Bash` 라도 **auto-mode 세션이 heredoc 으로 파일을 만드는",
22
+ "경로는 Bash 로 잡히지만**, 그 안에서 실제로 새 메커니즘을 짓는지까지는 훅이 모른다.",
23
+ "표면화까지가 훅의 몫이고 판정은 사람/세션이 한다 — 판정하는 훅을 만들면",
24
+ "§Mechanization Boundary 가 금하는 «결론을 단언하는 체크»가 된다.",
25
+ "",
26
+ "PRIVATE PATH 없음 → .claude/settings.json(프로젝트 로컬)에 속한다.",
27
+ "머지하되 덮어쓰지 마라: 기존 PreToolUse 엔트리를 보존하고 스크립트 이름을 키로 FH 것만 교체."
28
+ ],
29
+ "project_settings_json": {
30
+ "hooks": {
31
+ "PreToolUse": [
32
+ {
33
+ "matcher": "Write|Bash",
34
+ "hooks": [
35
+ {
36
+ "type": "command",
37
+ "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/prior_art_prompt.sh\""
38
+ }
39
+ ]
40
+ }
41
+ ]
42
+ }
14
43
  }
15
44
  }