@chrono-meta/fh-gate 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude/capabilities/adapters/gstack-content-safety.cap +110 -0
  2. package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
  3. package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
  4. package/.claude/capabilities/degrade-direction-scan.cap +24 -0
  5. package/.claude/capabilities/public-surface-scan.cap +24 -0
  6. package/.claude-plugin/marketplace.json +3 -3
  7. package/AGENTS.md +9 -3
  8. package/CHEATSHEET.md +17 -1
  9. package/CLAUDE.md +2 -2
  10. package/README.ja.md +55 -20
  11. package/README.ko.md +86 -20
  12. package/README.md +57 -20
  13. package/README.zh.md +49 -19
  14. package/knowledge/shared/harness-core/fh_three_layer_canon.md +96 -5
  15. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +1 -1
  16. package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +214 -0
  17. package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
  18. package/knowledge/shared/harness-core/ship_readiness_gate.md +29 -10
  19. package/knowledge/shared/learnings/subagent_invocations_log.yaml +62 -0
  20. package/package.json +10 -2
  21. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  22. package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
  23. package/plugins/fh-meta/CHANGELOG.md +17 -0
  24. package/scripts/adapters/gstack_content_safety.sh +186 -0
  25. package/scripts/adapters/mate_agent_boundary.sh +120 -0
  26. package/scripts/adapters/peer_resolve.sh +99 -0
  27. package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
  28. package/scripts/branch_claim.sh +14 -0
  29. package/scripts/capability_effect_probe.sh +340 -20
  30. package/scripts/capability_registry_check.sh +198 -10
  31. package/scripts/cluster_capability_scan.sh +629 -0
  32. package/scripts/fh_session_load.sh +70 -0
  33. package/scripts/portability_lint.sh +257 -0
  34. package/scripts/relay_channel.sh +70 -7
  35. package/scripts/selfcheck.sh +138 -1
  36. package/scripts/test_adapter_lanes.sh +296 -0
  37. package/scripts/test_capability_entrypoint_shipping.sh +8 -1
  38. package/scripts/test_relay_channel_lanes.sh +76 -0
  39. package/templates/.git-hooks/pre-commit +14 -2
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bash
2
+ # gstack_content_safety.sh — FH 소유 **어댑터**. peer `gstack` 의 redact 엔진을
3
+ # 클러스터 호출 가능한 typed capability 로 감싼다.
4
+ #
5
+ # ─────────────────────────────────────────────────────────────────────────────
6
+ # 어댑터가 왜 FH 안에 사나 (운영자 결정 2026-08-16)
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+ # 「없는 능력을 남의 하네스에서 호출해 쓴다」가 크로스하네스의 요점이고, 그 «쓰는 쪽»은
9
+ # FH 다. peer 레포 안에 선언을 심는 것은 **그 하네스를 소유하고 동시에 노출을 결정한**
10
+ # 특권적 경우이고, 일반 경로가 아니다. 그래서 선언과 래퍼가 여기 산다.
11
+ # gstack 레포에도 같은 목적의 래퍼(`scripts/content-safety-capability.sh`)가 있다 —
12
+ # 이 파일은 그것의 **FH 측 이식본**이다(로직·enum·근거를 옮겼다). peer 가 그 래퍼를
13
+ # 갖고 있든 아니든 돌게 하려고 엔진(`bin/gstack-redact`)을 직접 부른다.
14
+ #
15
+ # ─────────────────────────────────────────────────────────────────────────────
16
+ # 왜 `bin/gstack-redact` 를 그냥 선언하지 않나 (gstack 래퍼 헤더에서 옮겨온 근거)
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+ # 그 CLI 의 exit 은 {0 clean, 2 MEDIUM, 3 HIGH} 에 «git 레포가 아니다» 용 1 이 붙는다.
19
+ # **「아무것도 안 쟀다」를 뜻하는 값이 없다** — 빈 파일도, 스캔 대상이 0개인 디렉토리도,
20
+ # 인터프리터 부재도 전부 exit 0 = CLEAN 으로 렌더된다. 「PASS 가 no-op 과 구분 불가」는
21
+ # 등록 바가 정확히 거부하는 형태다(§ⓑ.4 B1).
22
+ # CLI 의 코드를 바꾸는 선택지는 **일부러 버렸다** — gstack 의 다른 스킬과 pre-push 훅이
23
+ # 그 코드를 소비한다. 설명하는 행위로 남의 레포 계약을 조용히 바꾸지 않는다.
24
+ #
25
+ # ─────────────────────────────────────────────────────────────────────────────
26
+ # VERDICT ENUM (선언과 정확히 일치해야 한다)
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+ # 0 CLEAN 실제 내용 1바이트 이상을 쟀고 HIGH·MEDIUM 둘 다 없다
29
+ # 1 BLOCK_HIGH HIGH 등급 findings 있음
30
+ # 2 REVIEW_MEDIUM MEDIUM 만 있음 — 자동 차단이 아니라 사람 판단
31
+ # 3 NO_TARGET 잴 게 없었다(빈 파일 · 후보 0개 디렉토리)
32
+ # *** CLEAN 이 아니다 — 스캔이 시작조차 안 했다 ***
33
+ # 10 HARNESS_ERROR 계기가 없거나 깨졌다(bun 부재 · CLI/엔진 부재 · 대상 불가독 ·
34
+ # 열거 실패 · 하위 CLI 가 자기 계약 밖 코드 반환)
35
+ # 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
36
+ #
37
+ # 🟥 셋은 서로 다른 명제다 — 접으면 안 된다:
38
+ # PEER_ABSENT = 그 노드가 이 머신에 없다 (클러스터에 대한 진술)
39
+ # HARNESS_ERROR = 노드는 있는데 계기가 깨졌다 (계기에 대한 진술)
40
+ # NO_TARGET = 계기는 멀쩡한데 잴 대상이 없다 (대상에 대한 진술)
41
+ # `verdict_binding` 에 셋 다 들어간다. 미측정이 통과로 렌더되면 이 어댑터는 장식이다.
42
+ #
43
+ # ⚠️ 하위 계기의 exit 을 **그대로 전파하지 않는다.** 아래 case 가 명시 재매핑이다
44
+ # (같은 숫자, 다른 사건 — gstack 의 3=HIGH 는 여기서 1=BLOCK_HIGH 다).
45
+ #
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # CLEANUP-TRAP 규율 (물려받은 실패 — 가정이 아니다)
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+ # 자매 하네스의 첫 capability 래퍼가 자기 레포의 `scripts/` 를 지웠다:
50
+ # `TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT` 를 걸고 다른 분기에서 $TMP 를 실제
51
+ # 레포 경로로 **재대입**했다. 트랩은 «만든 것» 이 아니라 «지금 가리키는 것» 을 지운다.
52
+ # 구조 처방: OWNED_TMP(내 것, 지운다)와 TARGET(재는 것, 절대 안 지운다)을 다른 변수로
53
+ # 두고, OWNED_TMP 는 최초 1회만 대입하며, 트랩이 임시경로인지 한 번 더 검문한다.
54
+ #
55
+ # 사용법
56
+ # gstack_content_safety.sh --target <file|dir>
57
+ # gstack_content_safety.sh --known-positive # 캘리브레이션 arm (expect BLOCK_HIGH)
58
+ # gstack_content_safety.sh --known-negative # 캘리브레이션 arm (expect CLEAN)
59
+ set -o pipefail
60
+
61
+ PEER_NAME="gstack"
62
+ RC_CLEAN=0; RC_BLOCK=1; RC_REVIEW=2; RC_NO_TARGET=3; RC_HARNESS=10; RC_PEER_ABSENT=20
63
+
64
+ _harness_error() { printf 'gstack-adapter: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
65
+ _peer_absent() { printf 'gstack-adapter: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
66
+
67
+ # ── peer 해석 — 절대경로를 이 파일에 싣지 않는다 ─────────────────────────────
68
+ # tracked 파일에 운영자 홈 절대경로가 들어가면 그건 공개 표면 유출이다.
69
+ _LIB="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/peer_resolve.sh"
70
+ [ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
71
+ # shellcheck source=/dev/null
72
+ . "$_LIB"
73
+
74
+ PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
75
+ case "$_rrc" in
76
+ 0) ;;
77
+ 1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(깨끗한 게 아니다)" ;;
78
+ 2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
79
+ *) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
80
+ esac
81
+
82
+ CLI="$PEER_ROOT/bin/gstack-redact"
83
+ ENGINE="$PEER_ROOT/lib/redact-engine.ts"
84
+
85
+ # ── 계기 전제 (계기의 부재는 결코 «깨끗함» 이 아니다) ────────────────────────
86
+ # peer 레포는 있는데 그 안의 엔진이 없다 = 계기 파손이지 노드 부재가 아니다.
87
+ [ -r "$CLI" ] || _harness_error "스캐너 CLI 부재: $CLI (peer 는 있으나 계기가 없다)"
88
+ [ -r "$ENGINE" ] || _harness_error "스캔 엔진 부재: $ENGINE"
89
+ command -v bun >/dev/null 2>&1 || _harness_error "bun 이 PATH 에 없다 — 스캐너를 돌릴 수 없다(깨끗한 결과가 아니다)"
90
+ command -v find >/dev/null 2>&1 || _harness_error "find 부재 — 대상을 열거할 수 없다(열거 불가는 부재가 아니다)"
91
+
92
+ OWNED_TMP="$(mktemp -d)" || _harness_error "mktemp 실패" # 1회 대입. 절대 재대입 금지.
93
+ _cleanup() {
94
+ case "$OWNED_TMP" in
95
+ /tmp/*|/var/folders/*|/private/var/folders/*)
96
+ [ -d "$OWNED_TMP" ] && rm -rf "$OWNED_TMP" ;;
97
+ *) printf 'gstack-adapter: cleanup 거부 — OWNED_TMP 가 임시경로가 아니다: %s\n' "$OWNED_TMP" >&2 ;;
98
+ esac
99
+ }
100
+ trap _cleanup EXIT
101
+
102
+ TARGET="" # 실제 레포 내용을 가리킬 수 있다. 절대 안 지우고 rm 에 넘기지 않는다.
103
+
104
+ case "${1:-}" in
105
+ --known-positive)
106
+ # 리터럴을 printf 인자로 **쪼갠다** — 이 소스 자신이 자격증명 형태 스캐너에 걸리면
107
+ # 픽스처가 자기 레포를 발행 불가로 만든다.
108
+ TARGET="$OWNED_TMP/known_positive.txt"
109
+ {
110
+ printf 'deploy notes\n'
111
+ printf 'aws_access_key_id=%s%s\n' 'AKIA' '1234567890ABCDEF'
112
+ printf 'contact ssn %s-%s-%s for payroll\n' '123' '45' '6789'
113
+ } > "$TARGET" || _harness_error "known-positive 픽스처를 쓸 수 없다"
114
+ ;;
115
+ --known-negative)
116
+ # 같은 형태·같은 크기대, 자격증명도 PII 도 없다. 컨트롤이 있어야 PASS 가
117
+ # 「이 계기가 가른다」이지 「이 계기가 조용하다」가 아니게 된다.
118
+ TARGET="$OWNED_TMP/known_negative.txt"
119
+ {
120
+ printf 'deploy notes\n'
121
+ printf 'the build pipeline reads its credentials from the platform keystore\n'
122
+ printf 'contact the payroll team through the internal ticket queue\n'
123
+ } > "$TARGET" || _harness_error "known-negative 픽스처를 쓸 수 없다"
124
+ ;;
125
+ --target)
126
+ TARGET="${2:-}"
127
+ [ -n "$TARGET" ] || _harness_error "--target 에 경로가 필요하다"
128
+ [ -e "$TARGET" ] || _harness_error "--target 경로가 없다: $TARGET (부재 대상은 빈 대상이 아니다)"
129
+ [ -r "$TARGET" ] || _harness_error "--target 경로를 읽을 수 없다: $TARGET"
130
+ ;;
131
+ '') _harness_error "대상 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
132
+ *) _harness_error "알 수 없는 모드 '$1'" ;;
133
+ esac
134
+
135
+ # ── 후보 열거 ────────────────────────────────────────────────────────────────
136
+ # 「열거 실패」와 「후보 0개」는 다른 사실이다. find 자체가 실패하면 대상을 **안 잰**
137
+ # 것이므로 HARNESS_ERROR 이지 NO_TARGET 이 아니다.
138
+ CAND_LIST="$OWNED_TMP/.candidates"
139
+ if [ -d "$TARGET" ]; then
140
+ _ferr=$(find -H "$TARGET" \( -name .git -o -name node_modules -o -name dist \) -prune -o \
141
+ -type f -print 2>&1 >"$CAND_LIST")
142
+ _frc=$?
143
+ if [ "$_frc" -ne 0 ] || [ -n "$_ferr" ]; then
144
+ _harness_error "대상 열거 실패(부재 대상이 아니다): ${_ferr:-find exited $_frc}"
145
+ fi
146
+ else
147
+ printf '%s\n' "$TARGET" > "$CAND_LIST"
148
+ fi
149
+
150
+ SCANNED=0
151
+ WORST=0 # 심각도 순서: CLEAN < REVIEW_MEDIUM < BLOCK_HIGH (숫자 순서가 아니다)
152
+ _rank() { case "$1" in 0) echo 0 ;; 2) echo 1 ;; 1) echo 2 ;; *) echo 9 ;; esac; }
153
+
154
+ while IFS= read -r f; do
155
+ [ -n "$f" ] || continue
156
+ [ -f "$f" ] && [ -r "$f" ] || continue
157
+ LC_ALL=C grep -Iq . "$f" 2>/dev/null || continue # 바이너리는 대상이 아니다
158
+ [ -n "$(tr -d '[:space:]' < "$f" 2>/dev/null | head -c 1)" ] || continue # 공백뿐 = 내용 없음
159
+
160
+ # 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
161
+ out=$(cd "$PEER_ROOT" && bun "$CLI" --from-file "$f" 2>&1); rc=$?
162
+ case "$rc" in
163
+ 0|2|3) ;;
164
+ *) _harness_error "스캐너가 자기 계약 {0,2,3} 밖의 $rc 를 반환했다: $f" ;;
165
+ esac
166
+ SCANNED=$((SCANNED + 1))
167
+ # 명시 재매핑 — gstack 3=HIGH → 1 · 2=MEDIUM → 2 · 0 → 0
168
+ case "$rc" in 3) mapped=1 ;; 2) mapped=2 ;; *) mapped=0 ;; esac
169
+ if [ "$(_rank "$mapped")" -gt "$(_rank "$WORST")" ]; then WORST="$mapped"; fi
170
+ if [ "$mapped" != "0" ]; then
171
+ printf 'gstack-adapter: %s\n' "$f"
172
+ printf '%s\n' "$out" | sed -n '2,6p'
173
+ fi
174
+ done < "$CAND_LIST"
175
+
176
+ if [ "$SCANNED" -eq 0 ]; then
177
+ printf 'gstack-adapter: NO_TARGET — %s 아래 스캔 가능한 파일 0개 (CLEAN 이 아니다. 아무것도 안 쟀다)\n' "$TARGET"
178
+ exit "$RC_NO_TARGET"
179
+ fi
180
+
181
+ case "$WORST" in
182
+ 0) printf 'gstack-adapter: CLEAN — %d개 파일 스캔, HIGH·MEDIUM 없음\n' "$SCANNED"; exit "$RC_CLEAN" ;;
183
+ 2) printf 'gstack-adapter: REVIEW_MEDIUM — %d개 파일 스캔, MEDIUM findings 있음\n' "$SCANNED"; exit "$RC_REVIEW" ;;
184
+ 1) printf 'gstack-adapter: BLOCK_HIGH — %d개 파일 스캔, HIGH findings 있음\n' "$SCANNED"; exit "$RC_BLOCK" ;;
185
+ *) _harness_error "내부 심각도 집계가 '$WORST' 를 냈다" ;;
186
+ esac
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ # mate_agent_boundary.sh — FH 소유 **어댑터**. peer `mate` 의 기존 회귀 앵커
3
+ # `scripts/blackbox_invariant_check.sh` 를 클러스터 호출 가능한 typed capability 로 감싼다.
4
+ #
5
+ # ─────────────────────────────────────────────────────────────────────────────
6
+ # 무엇을 재나
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+ # 에이전트 명세서가 **정보경계 불변식을 여전히 담고 있는가**. peer 쪽 앵커는 산문 규칙
9
+ # 11종(입력 어휘 allowlist · 출처 기준 금지 · 도구 무관 선언 · 신뢰도 상한 · 메모리 쓰기
10
+ # 금지 …)이 편집 한 번으로 조용히 되돌아가는 것을 막는다. 그 스크립트는 **peer 소유 코드**이고
11
+ # 이 어댑터가 만든 것이 아니다 — 여기서 하는 일은 호출과 enum 재매핑뿐이다.
12
+ #
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
+ # 어댑터가 왜 FH 안에 사나 (운영자 결정 2026-08-16)
15
+ # ─────────────────────────────────────────────────────────────────────────────
16
+ # 「없는 능력을 남의 하네스에서 호출해 쓴다」의 «쓰는 쪽»이 FH 다. peer 레포 안에 선언을
17
+ # 심는 것은 그 하네스를 소유하고 **동시에 노출을 결정한** 특권적 경우이지 일반 경로가 아니다.
18
+ #
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ # VERDICT ENUM (선언과 정확히 일치해야 한다)
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ # 0 PASS 전건 통과 — 불변식이 대상 문서에 살아 있다
23
+ # 1 FAIL 회귀 — 불변식 문장 중 하나 이상이 사라졌다
24
+ # 10 HARNESS_ERROR 계기가 없거나 대상을 못 읽었다
25
+ # 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
26
+ #
27
+ # 🟥 peer 스크립트 헤더가 이미 못 박아 뒀다 — *"exit 10 은 «위반 0» 이 아니다. 대상을 못 읽은
28
+ # 것이므로 검사가 시작조차 안 한 상태다."* 어댑터는 그 위에 **한 층 더** 구분을 얹는다:
29
+ # PEER_ABSENT = 그 하네스가 이 머신에 없다 (클러스터에 대한 진술)
30
+ # HARNESS_ERROR = 하네스는 있는데 못 쟀다 (계기/대상에 대한 진술)
31
+ # 둘을 접으면 「이 머신에 mate 를 안 깔았다」가 「mate 가 깨졌다」와 같은 얼굴이 된다.
32
+ # `verdict_binding` 에 PASS 를 뺀 전부가 들어간다 — 미측정을 통과로 렌더하지 않는다.
33
+ #
34
+ # ⚠️ 하위 계기의 exit 을 그대로 전파하지 않는다. 아래 case 가 명시 재매핑이다.
35
+ # 지금은 0→0 · 1→1 로 숫자가 같지만 **그건 우연이고 계약이 아니다** — peer 가 코드를
36
+ # 바꾸면 여기 case 가 갈라진다. 계약 밖 exit 은 PASS 로 흘리지 않고 HARNESS_ERROR 다.
37
+ #
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+ # 픽스처 — **어댑터가 자기 픽스처를 갖는다**
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ # `scripts/adapters/fixtures/mate_agent_boundary_known_{positive,negative}.md`.
42
+ # peer 레포의 같은 이름 파일에서 바이트 동일하게 복사했다(원본: peer 의
43
+ # `scripts/fixtures/agent_boundary_known_*.md`).
44
+ # ★왜 peer 의 실물 명세서를 arm 으로 안 쓰나: 실물은 팀이 계속 고치는 살아있는 문서라
45
+ # 그것을 양성 arm 으로 쓰면 이 어댑터의 판정이 **남의 편집에 따라 조용히 뒤집힌다**
46
+ # (감사 대상은 얼려야 한다). 실물은 인자 없는 기본 호출로 계속 검사된다.
47
+ # ★잔여(명명): 복사본이라 **드리프트한다.** peer 가 불변식을 늘리면 이 사본은 낡는다.
48
+ # 대가를 알고 고른 것이다 — peer 트리를 실행 시점에 읽으면 계기가 남의 편집에 흔들리고,
49
+ # 그건 캘리브레이션 arm 이 가져서는 안 되는 성질이다. 정합성 확인은 손으로 한다.
50
+ # ★residency: 두 픽스처는 **합성**이고 조직 식별자를 담지 않는다 — 이 레포에 tracked 로
51
+ # 들어오기 전에 손으로 전문을 읽고, peer 자신의 식별자 denylist grep(컨트롤 포함) ·
52
+ # 경로/URL/이메일/IP grep(컨트롤 포함) · 라틴 토큰 전수 열거로 확인했다.
53
+ #
54
+ # 사용법
55
+ # mate_agent_boundary.sh --target <file> # 임의 명세서 파일
56
+ # mate_agent_boundary.sh --peer-default # peer 의 기본 대상(실물 명세서)
57
+ # mate_agent_boundary.sh --known-positive # 캘리브레이션 arm (expect PASS)
58
+ # mate_agent_boundary.sh --known-negative # 캘리브레이션 arm (expect FAIL)
59
+ set -o pipefail
60
+
61
+ PEER_NAME="mate"
62
+ RC_PASS=0; RC_FAIL=1; RC_HARNESS=10; RC_PEER_ABSENT=20
63
+
64
+ _harness_error() { printf 'mate-adapter: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
65
+ _peer_absent() { printf 'mate-adapter: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
66
+
67
+ SELF_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
68
+ _LIB="$SELF_DIR/peer_resolve.sh"
69
+ [ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
70
+ # shellcheck source=/dev/null
71
+ . "$_LIB"
72
+
73
+ # ── peer 해석 — tracked 파일에 운영자 홈 절대경로를 싣지 않는다 ──────────────
74
+ PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
75
+ case "$_rrc" in
76
+ 0) ;;
77
+ 1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(통과가 아니다)" ;;
78
+ 2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
79
+ *) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
80
+ esac
81
+
82
+ ENTRY="$PEER_ROOT/scripts/blackbox_invariant_check.sh"
83
+ [ -r "$ENTRY" ] || _harness_error "peer 진입점 부재: $ENTRY (하네스는 있으나 계기가 없다)"
84
+
85
+ TARGET=""
86
+ case "${1:-}" in
87
+ --known-positive) TARGET="$SELF_DIR/fixtures/mate_agent_boundary_known_positive.md" ;;
88
+ --known-negative) TARGET="$SELF_DIR/fixtures/mate_agent_boundary_known_negative.md" ;;
89
+ --peer-default) TARGET="" ;; # peer 스크립트의 기본 대상에 맡긴다
90
+ --target)
91
+ TARGET="${2:-}"
92
+ [ -n "$TARGET" ] || _harness_error "--target 에 경로가 필요하다"
93
+ case "$TARGET" in /*) ;; *) TARGET="$PWD/$TARGET" ;; esac
94
+ ;;
95
+ '') _harness_error "모드 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
96
+ *) _harness_error "알 수 없는 모드 '$1'" ;;
97
+ esac
98
+ if [ -n "$TARGET" ]; then
99
+ [ -e "$TARGET" ] || _harness_error "대상 경로가 없다: $TARGET (부재 대상은 빈 대상이 아니다)"
100
+ [ -r "$TARGET" ] || _harness_error "대상 경로를 읽을 수 없다: $TARGET"
101
+ fi
102
+
103
+ # peer 트리를 **변이하지 않는다** — cwd 만 peer 로 두고 읽기만 한다.
104
+ # (기본 대상이 peer 레포 상대경로라 cwd 가 peer 여야 한다.)
105
+ # 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
106
+ if [ -n "$TARGET" ]; then
107
+ out=$(cd "$PEER_ROOT" && bash "$ENTRY" "$TARGET" 2>&1); rc=$?
108
+ else
109
+ out=$(cd "$PEER_ROOT" && bash "$ENTRY" 2>&1); rc=$?
110
+ fi
111
+ printf '%s\n' "$out"
112
+
113
+ # ── 명시 재매핑 (같은 숫자, 다른 사건 — 전파가 아니다) ───────────────────────
114
+ case "$rc" in
115
+ 0) printf 'mate-adapter: PASS — 정보경계 불변식 전건 통과\n'; exit "$RC_PASS" ;;
116
+ 1) printf 'mate-adapter: FAIL — 불변식 회귀\n'; exit "$RC_FAIL" ;;
117
+ 10) printf 'mate-adapter: HARNESS_ERROR — peer 계기가 대상을 못 읽었다(위반 0 이 아니다)\n' >&2
118
+ exit "$RC_HARNESS" ;;
119
+ *) _harness_error "peer 진입점이 자기 계약 {0,1,10} 밖의 $rc 를 반환했다" ;;
120
+ esac
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # peer_resolve.sh — **어댑터 공용** peer 레포 해석. `source` 전용(직접 실행하지 않는다).
3
+ #
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # 왜 이 파일이 따로 있나 — divergent-normalizer 를 안 만들기 위해
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # `scripts/cluster_capability_scan.sh` 가 이미 «track 이름 → 레포 루트» 를 푼다
8
+ # (`_resolve_root`, `_enumerate_harnesses`). 어댑터가 자기 규칙을 따로 쓰면 **두 계기가
9
+ # 서로 다른 트리를 본다** — 스캐너는 `~/projects/qasp-dev` 를 보고 어댑터는 못 찾는 식이다.
10
+ # 이 레포는 그 결함 계열을 «관대함 갈린 중복 정규화» 로 이름 붙여 두고 있다.
11
+ # 그래서 규칙을 **여기 한 벌만** 두고 세 어댑터가 전부 이걸 부른다.
12
+ #
13
+ # 🟥 규칙은 `cluster_capability_scan.sh` 를 읽고 그대로 옮긴 것이다:
14
+ # · 루트 목록 : `FH_CLUSTER_ROOTS`(콜론 구분 **레포 루트** 절대경로 목록)가 있으면 그것이
15
+ # 전부다. 없으면 `FH_PROJECTS_HOME`(기본 `$HOME/projects`) 아래를 본다.
16
+ # · 별칭 : `<name>` · `<name>-dev` · `<name>` 의 `_`→`-` — **닫힌 목록**.
17
+ # · 모호 : 둘 이상이 동시에 맞으면 **고르지 않는다.** 조용히 하나를 고르면 «어느
18
+ # 하네스를 쟀는지» 아무도 모르게 된다.
19
+ #
20
+ # ⚠️ 한 곳에서 의도적으로 갈린다 — 그리고 갈리는 이유를 적는다.
21
+ # 스캐너는 «루트 목록이 비었다» 를 `HARNESS_ERROR`(전제 파손)로 낸다. 어댑터도 같다.
22
+ # 다만 스캐너는 track 디렉토리를 열거하는 반면 어댑터는 **이름 하나를 찾는다** —
23
+ # 그래서 어댑터에는 「루트 목록은 멀쩡한데 그 안에 이 peer 가 없다」는 상태가 따로 있고,
24
+ # 그게 `PEER_ABSENT` 다. 스캐너에는 그 자리가 `UNREACHABLE` 로 있다(같은 명제, 다른 이름).
25
+ #
26
+ # 사용법
27
+ # . "$(dirname "${BASH_SOURCE[0]}")/peer_resolve.sh"
28
+ # if root="$(fh_peer_resolve gstack)"; then ... ; else rc=$?; ... ; fi
29
+ #
30
+ # fh_peer_resolve <name>
31
+ # stdout : 해석된 레포 루트 (rc=0 일 때만)
32
+ # rc=0 FOUND
33
+ # rc=1 ABSENT — 루트 목록은 읽었는데 이 peer 가 그 안에 없다
34
+ # rc=2 AMBIGUOUS — 별칭 여럿이 동시에 맞는다. 고르지 않는다
35
+ # rc=3 NO_ROOTS — 루트 목록 자체가 비었다(전제 파손. 「없다」가 아니다)
36
+ # stderr : rc!=0 일 때 한 줄 사유
37
+
38
+ # 별칭 후보 — 닫힌 목록. 스캐너의 `_resolve_root` 와 **같은 순서**여야 한다.
39
+ fh_peer_alias_candidates() { # $1=name → 후보를 줄 단위로
40
+ printf '%s\n' "$1" "$1-dev" "$(printf '%s' "$1" | tr '_' '-')"
41
+ }
42
+
43
+ fh_peer_resolve() { # $1=peer 이름
44
+ local n="${1:-}" c hits="" first="" nh
45
+ [ -n "$n" ] || { printf 'peer-resolve: 이름이 비었다\n' >&2; return 3; }
46
+
47
+ if [ -n "${FH_CLUSTER_ROOTS:-}" ]; then
48
+ # 명시 루트 목록. 각 항목이 **레포 루트 자체**다(스캐너와 같은 의미).
49
+ local r b usable=0
50
+ while IFS= read -r r; do
51
+ # 공백만 있는 항목은 항목이 아니다 — 스캐너가 자기 self-test 로 잡은 그 접힘.
52
+ r="$(printf '%s' "$r" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
53
+ [ -n "$r" ] || continue
54
+ usable=$((usable + 1))
55
+ b="$(basename "$r")"
56
+ while IFS= read -r c; do
57
+ [ "$b" = "$c" ] || continue
58
+ [ -d "$r" ] || continue
59
+ case " $hits " in *" $r "*) continue ;; esac
60
+ hits="$hits $r"; [ -n "$first" ] || first="$r"
61
+ done <<EOF
62
+ $(fh_peer_alias_candidates "$n")
63
+ EOF
64
+ done <<EOF
65
+ $(printf '%s\n' "$FH_CLUSTER_ROOTS" | tr ':' '\n')
66
+ EOF
67
+ if [ "$usable" -eq 0 ]; then
68
+ printf 'peer-resolve: FH_CLUSTER_ROOTS 에 쓸 수 있는 항목이 0개 — 어디를 볼지 자체가 없다(전제 파손이지 «peer 없음» 이 아니다)\n' >&2
69
+ return 3
70
+ fi
71
+ else
72
+ local home="${FH_PROJECTS_HOME:-$HOME/projects}"
73
+ if [ ! -d "$home" ]; then
74
+ printf 'peer-resolve: 프로젝트 홈이 없다: %s (전제 파손이지 «peer 없음» 이 아니다)\n' "$home" >&2
75
+ return 3
76
+ fi
77
+ while IFS= read -r c; do
78
+ [ -d "$home/$c" ] || continue
79
+ case " $hits " in *" $home/$c "*) continue ;; esac
80
+ hits="$hits $home/$c"; [ -n "$first" ] || first="$home/$c"
81
+ done <<EOF
82
+ $(fh_peer_alias_candidates "$n")
83
+ EOF
84
+ fi
85
+
86
+ nh=$(printf '%s' "$hits" | wc -w | tr -d ' ')
87
+ if [ "${nh:-0}" -gt 1 ]; then
88
+ printf 'peer-resolve: 한 이름에 레포 여럿 — 고르지 않는다: %s\n' \
89
+ "$(printf '%s' "$hits" | sed 's/^ //; s/ /, /g')" >&2
90
+ return 2
91
+ fi
92
+ if [ -z "$first" ]; then
93
+ printf 'peer-resolve: peer «%s» 가 이 머신에 없다(별칭 %s 전부 미해석)\n' \
94
+ "$n" "$(fh_peer_alias_candidates "$n" | tr '\n' '/' | sed 's|/$||')" >&2
95
+ return 1
96
+ fi
97
+ printf '%s' "$first"
98
+ return 0
99
+ }
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bash
2
+ # qasp_new_code_anchor.sh — FH 소유 **어댑터**. peer `qasp` 의
3
+ # `scripts/new_code_anchor_scan.sh` 를 클러스터 호출 가능한 typed capability 로 감싼다.
4
+ #
5
+ # ─────────────────────────────────────────────────────────────────────────────
6
+ # 무엇을 재나
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+ # **새로 추가된 실행 코드에 그것을 보는 앵커(테스트)가 있는가.** peer 헤더의 기원 실측:
9
+ # 앵커 0개인 신규 스크립트가 들어왔는데 CI 는 `success / 123 passed` 였다 — 그 123개 중
10
+ # 그 파일을 보는 게 하나도 없었을 뿐이다. 그 상태로 fail-open 3건이 같이 통과했다.
11
+ # 이 스캔이 닫는 것은 딱 하나: **「안 돌았다」가 「깨끗하다」로 읽히는 것.**
12
+ # 진입점은 **peer 소유 코드**이고 이 어댑터가 만든 것이 아니다.
13
+ #
14
+ # ─────────────────────────────────────────────────────────────────────────────
15
+ # VERDICT ENUM (선언과 정확히 일치해야 한다)
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ # 0 PASS 앵커 없는 신규 실행 코드 없음
18
+ # 1 VIOLATION 앵커 없는 신규 실행 코드가 있다
19
+ # 2 ARGS 인자 오류 — 판정이 아니다
20
+ # 3 HARNESS_ERROR ref 해석 불가 · git 실패 · 계기 부재 — 판정이 아니다
21
+ # 4 OUT_OF_SCOPE **다른 언어의 신규 실행 코드가 있어 검사되지 않았다** — 판정이 아니다
22
+ # 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
23
+ #
24
+ # 🟥 값 넷이 「안 쟀다」의 서로 다른 얼굴이다. peer 헤더가 같은 말을 한다 —
25
+ # *"스캔이 못 돌았는데 1 을 내면 «위반» 으로, 0 을 내면 «깨끗» 으로 읽힌다. 둘 다 틀렸다."*
26
+ # 어댑터는 그 위에 한 층을 더 얹는다:
27
+ # PEER_ABSENT = 그 하네스가 이 머신에 없다 (클러스터에 대한 진술)
28
+ # HARNESS_ERROR = 하네스는 있는데 못 쟀다 (계기에 대한 진술)
29
+ # OUT_OF_SCOPE = 계기는 돌았는데 대상이 사각지대다 (범위에 대한 진술)
30
+ # `verdict_binding` 에 PASS 를 뺀 전부가 들어간다.
31
+ #
32
+ # ⚠️ 하위 계기의 exit 을 그대로 전파하지 않는다 — 아래 case 가 명시 재매핑이다.
33
+ # peer 헤더 자신이 그걸 규율로 적어 뒀다: *"같은 숫자가 이 레포 안에서 다른 사건을
34
+ # 뜻한다 — 값을 옮겨 쓰지 마라."* (`judge_rer.sh` 의 4=BLOCKED · `pull_cli.py` 의
35
+ # 4=NO_CORPUS 와 여기의 4=OUT_OF_SCOPE 는 전부 다른 사건이다.)
36
+ #
37
+ # ─────────────────────────────────────────────────────────────────────────────
38
+ # 캘리브레이션 쌍 — **합성 레포**에서 돈다. 왜 peer 히스토리를 안 쓰나
39
+ # ─────────────────────────────────────────────────────────────────────────────
40
+ # ① peer 의 이 진입점은 **오늘 수리 중**이다(qasp PR #174, 브랜치
41
+ # `fix/anchor-scan-out-of-scope-repairs`). 고정 SHA 를 쌍으로 박으면 그 브랜치가
42
+ # squash 머지되는 순간 커밋이 orphan 이 되어 쌍이 해석 불가로 죽는다.
43
+ # ② 상대 ref(`HEAD~N`)는 커밋이 쌓이면 가리키는 구간이 조용히 움직인다 — 같은 선언이
44
+ # 다른 것을 재게 된다.
45
+ # ③ peer 는 `residency: company` 다. peer 히스토리를 arm 으로 쓰면 캘리브레이션 출력에
46
+ # 조직 경로명이 실린다. 합성 레포는 그 표면을 아예 만들지 않는다.
47
+ # 대가: 쌍이 재는 것은 «peer 의 히스토리» 가 아니라 **«peer 의 계기가 답 아는 두 케이스를
48
+ # 가르는가»** 다. 등록 바가 M4 에서 묻는 것이 정확히 후자이므로 맞는 표적이다.
49
+ # 양성: 신규 `src/plain_config.py`(앵커 0) → VIOLATION
50
+ # 음성: 같은 파일 + `tests/test_plain_config.py`(앵커) → PASS
51
+ # ★`src/plain_config.py` 라는 이름은 peer 자신의 B1 실측에서 온 **대조군**이다
52
+ # (`latest_config.py` 는 `test_` 부분문자열 때문에 무검사 통과했고, `plain_config.py`
53
+ # 는 정상 차단됐다). 즉 이 양성 arm 은 peer 가 이미 손으로 가른 케이스와 같은 것이다.
54
+ #
55
+ # ─────────────────────────────────────────────────────────────────────────────
56
+ # CLEANUP-TRAP 규율 (물려받은 실패)
57
+ # ─────────────────────────────────────────────────────────────────────────────
58
+ # OWNED_TMP(내가 만든 것, 지운다)와 peer 트리는 **다른 변수**다. OWNED_TMP 는 최초 1회만
59
+ # 대입하고 트랩이 임시경로인지 한 번 더 검문한다. peer 트리는 rm 에 절대 안 넘어간다.
60
+ #
61
+ # 사용법
62
+ # qasp_new_code_anchor.sh --range <base-ref> [<head-ref>] # peer 레포에서 실행
63
+ # qasp_new_code_anchor.sh --known-positive # expect VIOLATION
64
+ # qasp_new_code_anchor.sh --known-negative # expect PASS
65
+ set -o pipefail
66
+
67
+ PEER_NAME="qasp"
68
+ RC_PASS=0; RC_VIOLATION=1; RC_ARGS=2; RC_HARNESS=3; RC_OUT_OF_SCOPE=4; RC_PEER_ABSENT=20
69
+
70
+ _harness_error() { printf 'qasp-adapter: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
71
+ _args_error() { printf 'qasp-adapter: ARGS — %s\n' "$1" >&2; exit "$RC_ARGS"; }
72
+ _peer_absent() { printf 'qasp-adapter: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
73
+
74
+ SELF_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
75
+ _LIB="$SELF_DIR/peer_resolve.sh"
76
+ [ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
77
+ # shellcheck source=/dev/null
78
+ . "$_LIB"
79
+
80
+ # ── peer 해석 — tracked 파일에 운영자 홈 절대경로를 싣지 않는다 ──────────────
81
+ PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
82
+ case "$_rrc" in
83
+ 0) ;;
84
+ 1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(통과가 아니다)" ;;
85
+ 2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
86
+ *) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
87
+ esac
88
+
89
+ ENTRY="$PEER_ROOT/scripts/new_code_anchor_scan.sh"
90
+ [ -r "$ENTRY" ] || _harness_error "peer 진입점 부재: $ENTRY (하네스는 있으나 계기가 없다)"
91
+ command -v git >/dev/null 2>&1 || _harness_error "git 부재 — 이 계기는 git 없이는 못 돈다"
92
+
93
+ OWNED_TMP="" # 캘리브레이션 arm 에서만 만든다. 1회 대입, 재대입 금지.
94
+ _cleanup() {
95
+ [ -n "$OWNED_TMP" ] || return 0
96
+ case "$OWNED_TMP" in
97
+ /tmp/*|/var/folders/*|/private/var/folders/*)
98
+ [ -d "$OWNED_TMP" ] && rm -rf "$OWNED_TMP" ;;
99
+ *) printf 'qasp-adapter: cleanup 거부 — OWNED_TMP 가 임시경로가 아니다: %s\n' "$OWNED_TMP" >&2 ;;
100
+ esac
101
+ }
102
+ trap _cleanup EXIT
103
+
104
+ # 합성 레포를 세운다. $1 = "anchored" 면 앵커 파일을 같이 넣는다.
105
+ # stdout 은 오염시키지 않는다(경로는 전역 변수로 넘긴다).
106
+ FIXTURE_REPO=""; FIXTURE_BASE=""
107
+ _build_fixture_repo() {
108
+ local anchored="${1:-}"
109
+ OWNED_TMP="$(mktemp -d)" || _harness_error "mktemp 실패"
110
+ local R="$OWNED_TMP/fixture"
111
+ mkdir -p "$R/src" "$R/tests" || _harness_error "픽스처 레포를 만들 수 없다"
112
+ # `-c` 로 신원을 준다 — 전역 gitconfig 를 읽지도 쓰지도 않기 위해서다.
113
+ local G=(git -c user.email=fixture@localhost -c user.name=fixture -c commit.gpgsign=false)
114
+ ( cd "$R" && "${G[@]}" init -q . ) >/dev/null 2>&1 || _harness_error "픽스처 레포 init 실패"
115
+ printf 'fixture base\n' > "$R/README.md"
116
+ ( cd "$R" && "${G[@]}" add -A && "${G[@]}" commit -qm "base" ) >/dev/null 2>&1 \
117
+ || _harness_error "픽스처 base 커밋 실패"
118
+ FIXTURE_BASE="$( cd "$R" && git rev-parse HEAD 2>/dev/null )"
119
+ [ -n "$FIXTURE_BASE" ] || _harness_error "픽스처 base ref 해석 실패"
120
+ # 신규 실행 코드 1개. peer 의 대상 case(`src/*.py`)에 걸린다.
121
+ printf 'def compute(a, b):\n return a + b\n' > "$R/src/plain_config.py"
122
+ if [ "$anchored" = "anchored" ]; then
123
+ # peer 의 앵커 판정은 «HEAD 시점의 tests/ 가 이 파일의 stem 을 언급하는가» 다.
124
+ printf 'from src.plain_config import compute\n\n\ndef test_compute():\n assert compute(1, 2) == 3\n' \
125
+ > "$R/tests/test_plain_config.py"
126
+ fi
127
+ ( cd "$R" && "${G[@]}" add -A && "${G[@]}" commit -qm "add new code" ) >/dev/null 2>&1 \
128
+ || _harness_error "픽스처 head 커밋 실패"
129
+ FIXTURE_REPO="$R"
130
+ }
131
+
132
+ RUN_CWD=""; BASE=""; HEAD_REF=""
133
+ case "${1:-}" in
134
+ --known-positive) _build_fixture_repo ""; RUN_CWD="$FIXTURE_REPO"; BASE="$FIXTURE_BASE"; HEAD_REF="HEAD" ;;
135
+ --known-negative) _build_fixture_repo "anchored"; RUN_CWD="$FIXTURE_REPO"; BASE="$FIXTURE_BASE"; HEAD_REF="HEAD" ;;
136
+ --range)
137
+ BASE="${2:-}"; HEAD_REF="${3:-HEAD}"
138
+ [ -n "$BASE" ] || _args_error "--range 에 base-ref 가 필요하다"
139
+ RUN_CWD="$PEER_ROOT"
140
+ ;;
141
+ '') _args_error "모드 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
142
+ *) _args_error "알 수 없는 모드 '$1'" ;;
143
+ esac
144
+
145
+ # peer 트리를 **변이하지 않는다** — `--range` 에서는 cwd 만 peer 로 두고 읽기만 한다.
146
+ # 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
147
+ out=$(cd "$RUN_CWD" && bash "$ENTRY" "$BASE" "$HEAD_REF" 2>&1); rc=$?
148
+ printf '%s\n' "$out"
149
+
150
+ # ── 명시 재매핑 (같은 숫자, 다른 사건 — 전파가 아니다) ───────────────────────
151
+ case "$rc" in
152
+ 0) printf 'qasp-adapter: PASS — 앵커 없는 신규 실행 코드 없음\n'; exit "$RC_PASS" ;;
153
+ 1) printf 'qasp-adapter: VIOLATION — 앵커 없는 신규 실행 코드가 있다\n'; exit "$RC_VIOLATION" ;;
154
+ 2) printf 'qasp-adapter: ARGS — peer 계기가 인자를 거부했다(판정이 아니다)\n' >&2; exit "$RC_ARGS" ;;
155
+ 3) printf 'qasp-adapter: HARNESS_ERROR — ref 해석 불가/git 실패(판정이 아니다)\n' >&2; exit "$RC_HARNESS" ;;
156
+ 4) printf 'qasp-adapter: OUT_OF_SCOPE — 검사되지 않은 신규 실행 코드가 있다(깨끗함이 아니다)\n' >&2
157
+ exit "$RC_OUT_OF_SCOPE" ;;
158
+ *) _harness_error "peer 진입점이 자기 계약 {0,1,2,3,4} 밖의 $rc 를 반환했다" ;;
159
+ esac
@@ -21,6 +21,19 @@
21
21
  # 세션 정체성은 배관이 필요 없다. 실측(2026-08-09): git 훅은 Claude Code 가 주입한
22
22
  # `CLAUDE_CODE_SESSION_ID` · `CLAUDE_PID` 를 그대로 상속한다.
23
23
  #
24
+ # `show` 를 읽는 사람(사람이든 세션이든)에게: claim 수 ≠ 실제 동시편집 스레드 수 ★ 관측 보강, 2026-08-16
25
+ # `show` 는 `.git/fh-claims/*` 를 그대로 나열하고 pid 생존만 본다. 그래서 **claim 이 "live"라는
26
+ # 것은 «그 pid 가 아직 살아있다»는 뜻이지 «그 세션이 지금 이 브랜치를 편집 중」이란 뜻이 아니다.**
27
+ # 실측(2026-08-16, 이 감사-remediation 세션 자신): `show` 가 이 브랜치에 live claim 5개를 냈다.
28
+ # 교차확인(peer 에게 직접 SendMessage) 결과 5개 중 실질 편집 스레드는 **1~2개** 뿐이었다 —
29
+ # 나머지는 ⓐ idle bg 세션(작업 없음) ⓑ pid 는 살아있지만 **다른 레포로 이동한 세션의 stale claim**
30
+ # (release 를 안 하고 옮겨감) ⓒ 이미 handoff 되어 ListAgents 에도 안 잡히는 완료 세션의 잔존 파일.
31
+ # 즉 이 스크립트의 판정(§판정 키 = 세션)은 여전히 옳다 — «내 claim vs HEAD» 비교는 claim 수와
32
+ # 무관하게 성립한다. 틀렸던 건 **그 옆에서 `show` 출력을 읽고 "N개 세션이 동시에 이 브랜치를
33
+ # 건드리고 있다"고 위험도를 추정하는 것**이다. 그 추정을 하려면 `show` 가 아니라 ⓐ`reap` 으로
34
+ # 죽은 claim 을 먼저 걷어내고 ⓑ 남은 claim 의 세션에 **직접 물어라**(ListAgents+SendMessage) —
35
+ # claim 파일 자체엔 "지금 이 트리를 실제로 만지고 있는가"를 답할 필드가 없다.
36
+ #
24
37
  # WHAT THIS DOES / DOESN'T
25
38
  # 막는다 : «내가 마지막으로 claim 한 브랜치» ≠ HEAD 인 채로 커밋
26
39
  # 안 막는다 : `git switch` 자체 (git 에 switch 훅이 없다 — 막을 자리가 없다)
@@ -223,6 +236,7 @@ cmd_show() {
223
236
  "$(_field "$f" branch)" "$(_field "$f" label)" "$alive"
224
237
  done
225
238
  else echo " (기록 없음)"; fi
239
+ echo " (live = pid 생존일 뿐, 지금 이 트리를 편집 중이란 뜻 아님 — 위험도 추정은 reap 후 직접 문의)"
226
240
  }
227
241
 
228
242
  # 0 = 통과, 1 = 차단