@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
@@ -51,7 +51,10 @@ set -f # noglob — 선언 파일의 값은 데이터지 파일 패턴이 아
51
51
 
52
52
  RC_OK=0; RC_REJECT=1; RC_HARNESS=10
53
53
 
54
- CLOSED_KEYS="id entry requires_cwd verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream approval reversibility residency degrade tier_floor writes judge verdict_binding calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
54
+ # `summary`/`tags` **추천 전용**(2026-08-16, cluster-wizard). 판정에는 쓰이고
55
+ # `cluster_capability_scan.sh recommend` 의 어휘 매칭에만 쓰인다. 닫힌 목록에 넣는 이유는
56
+ # 이 목록의 목적이 «오타 축 무음 드롭 방지» 이기 때문이다 — 안 넣으면 정당한 키가 SCHEMA 로 막힌다.
57
+ CLOSED_KEYS="id entry requires_cwd summary tags verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream approval reversibility residency degrade tier_floor writes judge verdict_binding calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
55
58
 
56
59
  # 「안 돌았다」를 뜻하는 이름들 — 추가조항(§ⓑ.4 B1)이 요구하는 구분항
57
60
  DIDNOTRUN_NAMES="DID_NOT_RUN DIDNOTRUN NOT_RUN NO_TARGET SKIPPED UNMEASURED NOT_CONFIGURED HARNESS_ERROR"
@@ -67,7 +70,10 @@ _parse() {
67
70
  CAP_id=""; CAP_entry=""; CAP_requires_cwd=""; CAP_verdict_channel=""
68
71
  CAP_verdict_enum=""; CAP_verdict_stdout_key=""; CAP_judge=""; CAP_writes=""
69
72
  CAP_cal_pos_args=""; CAP_cal_pos_expect=""; CAP_cal_neg_args=""; CAP_cal_neg_expect=""
70
- UNKNOWN_KEYS=""
73
+ UNKNOWN_KEYS=""; CAP_requires_cwd_was_self=0
74
+ # 「선언했는데 값이 비었다」와 「선언 자체가 없다」는 다른 사실이다 — 관측 범위 블록이
75
+ # 둘을 구분해 적으려면 파서가 그 구분을 보존해야 한다(빈 문자열 하나로는 못 나눈다).
76
+ CAP_cal_pos_args_seen=0; CAP_cal_neg_args_seen=0
71
77
  local line key val
72
78
  while IFS= read -r line || [ -n "$line" ]; do
73
79
  case "$line" in ''|'#'*) continue ;; esac
@@ -84,9 +90,9 @@ _parse() {
84
90
  requires_cwd) CAP_requires_cwd="$val" ;; verdict_channel) CAP_verdict_channel="$val" ;;
85
91
  verdict_enum) CAP_verdict_enum="$val" ;; verdict_stdout_key) CAP_verdict_stdout_key="$val" ;;
86
92
  judge) CAP_judge="$val" ;; writes) CAP_writes="$val" ;;
87
- calibration_positive_args) CAP_cal_pos_args="$val" ;;
93
+ calibration_positive_args) CAP_cal_pos_args="$val"; CAP_cal_pos_args_seen=1 ;;
88
94
  calibration_positive_expect) CAP_cal_pos_expect="$val" ;;
89
- calibration_negative_args) CAP_cal_neg_args="$val" ;;
95
+ calibration_negative_args) CAP_cal_neg_args="$val"; CAP_cal_neg_args_seen=1 ;;
90
96
  calibration_negative_expect) CAP_cal_neg_expect="$val" ;;
91
97
  esac
92
98
  done < "$1"
@@ -121,15 +127,88 @@ _run_arm() { # $1=extra args → ARM_RC / ARM_NAME. 파이프로 읽지 않는
121
127
  ARM_NAME="$(_enum_name_of "$ARM_RC" "$CAP_verdict_enum")"
122
128
  }
123
129
 
130
+ # ── 관측 범위 블록 (2026-08-16) — «판정» 이 아니라 «채널» ────────────────────────
131
+ #
132
+ # 🟥 이 블록은 **exit code 에 아무 영향을 주지 않는다.** `_fail` 을 부르지 않고, 어떤 축도
133
+ # 새로 판정하지 않는다. 순수 정보 추가다 — 이 레포의 규율("기계는 비가역 경계와 채널에만,
134
+ # 판단은 안 짓는다")대로, 여기서 하는 일은 **무엇을 관측했고 무엇을 안 했는지 적는 것**뿐이다.
135
+ #
136
+ # 왜 필요한가 — 2026-08-16 실측 두 건이 같은 얼굴을 하고 있었다:
137
+ # ① **M4 는 enum↔구현 일치를 구조적으로 안 본다.** 같은 stale 선언이 캘리브레이션 쌍에
138
+ # 따라 판정이 갈린다 — 쌍이 우연히 «선언 밖 exit» 을 밟으면 REJECTED, 구 enum 안에서만
139
+ # 갈리면 M1–M6 전부 통과. 즉 M4 는 «자기가 부른 두 번의 실행에서 본 것» 만 판정한다.
140
+ # ② **M6 도 선언된 arm 만 관측한다.** `pmh-dev:merge-noop-check` 는 분기 입력에서 git 객체를
141
+ # 쓰지만(격리 클론 실측: `.git/objects` 2145 → 2146), 선언된 두 arm 이 그 경로를 안 지나므로
142
+ # `writes: read-only` 로 **거짓 선언해도 통과한다**(실측 확인).
143
+ #
144
+ # 둘 다 «판정이 틀렸다» 가 아니라 **«판정의 관측 범위가 출력에 안 적혀 있다»** 이다. 그래서
145
+ # 고치는 방향은 새 판정 축이 아니라 이 블록이다: 읽는 사람이 초록을 «선언 전체가 참이다» 로
146
+ # 읽지 않게, 이 실행이 실제로 밟은 곳을 이름으로 적는다.
147
+ # ★ 「없음」도 **반드시 찍는다.** 침묵은 «관측되지 않은 값이 없다» 와 «계산하지 않았다» 를
148
+ # 구분하지 못한다 — 미측정을 0 으로 렌더하지 않는다([[feedback_not_found_is_not_zero_family]]).
149
+ _observation_scope() {
150
+ printf ' ── 관측 범위 (이 판정이 «무엇을 안 봤는지») ──\n'
151
+ printf ' M4/M6 는 **선언된 캘리브레이션 arm 의 실행에서 본 것만** 판정한다.\n'
152
+
153
+ local _pa _na
154
+ if [ "${CAP_cal_pos_args_seen:-0}" = "1" ]; then
155
+ [ -n "$CAP_cal_pos_args" ] && _pa="$CAP_cal_pos_args" || _pa='<빈 값 — 인자 없이 실행>'
156
+ else _pa='<미선언>'; fi
157
+ if [ "${CAP_cal_neg_args_seen:-0}" = "1" ]; then
158
+ [ -n "$CAP_cal_neg_args" ] && _na="$CAP_cal_neg_args" || _na='<빈 값 — 인자 없이 실행>'
159
+ else _na='<미선언>'; fi
160
+ printf ' 실행된 arm: 양성 «%s» / 음성 «%s»\n' "$_pa" "$_na"
161
+
162
+ if [ "${OBS_STATE:-none}" = "run" ]; then
163
+ printf ' 관측된 exit: 양성 rc=%s(%s)%s / 음성 rc=%s(%s)\n' \
164
+ "$OBS_POS_RC" "${OBS_POS_NAME:-<enum 밖>}" \
165
+ "$( [ "$OBS_POS_RC" = "$OBS_POS_RC2" ] && printf ' ×2회 동일' || printf ' ×2회째 rc=%s(갈림)' "$OBS_POS_RC2" )" \
166
+ "$OBS_NEG_RC" "${OBS_NEG_NAME:-<enum 밖>}"
167
+ else
168
+ printf ' 관측된 exit: 없음 — arm 을 실행하지 않았다(%s)\n' "${OBS_WHY:-사유 미기록}"
169
+ fi
170
+
171
+ # 미관측 enum = 선언 enum − 이 실행에서 실제로 나온 rc. **계산해서** 적는다.
172
+ if [ -z "$CAP_verdict_enum" ]; then
173
+ printf ' ⚠️ 선언된 enum 이 없어 「관측되지 않은 값」을 계산할 수 없다(미측정이지 0 이 아니다 — M2 가 이미 실패로 기록했다)\n'
174
+ else
175
+ local _pair _code _unobs="" _seenrc=""
176
+ [ "${OBS_STATE:-none}" = "run" ] && _seenrc=" $OBS_POS_RC $OBS_POS_RC2 $OBS_NEG_RC "
177
+ for _pair in $CAP_verdict_enum; do
178
+ _code="${_pair%%=*}"
179
+ case "$_seenrc" in *" $_code "*) ;; *) _unobs="$_unobs $_pair" ;; esac
180
+ done
181
+ if [ -n "$_unobs" ]; then
182
+ printf ' ⚠️ 선언된 enum 중 이 실행에서 **관측되지 않은** 값:%s\n' "$_unobs"
183
+ else
184
+ printf ' · 선언된 enum 중 이 실행에서 관측되지 않은 값: 없음 (선언된 전 값이 이 실행에서 관측됐다)\n'
185
+ fi
186
+ fi
187
+ printf ' ⚠️ 이 실행이 지나지 않은 코드 경로의 부작용(M6 의 `writes:` 축 포함)은 관측 대상이 아니다.\n'
188
+ }
189
+
124
190
  _check_one() {
125
191
  local f="$1"
126
192
  [ -r "$f" ] || _die "capfile 도달 불가: $f"
127
193
  _parse "$f"
128
194
  FILE_FAILED=0
195
+ # 관측 원장 — 「안 돌았다」가 디폴트다. 실행한 arm 만 이 값들을 채운다.
196
+ OBS_STATE=none; OBS_WHY=""; OBS_POS_RC=""; OBS_POS_RC2=""; OBS_NEG_RC=""
197
+ OBS_POS_NAME=""; OBS_NEG_NAME=""
129
198
  printf '\n── %s (%s)\n' "${CAP_id:-<id 미선언>}" "$f"
130
199
 
131
200
  [ -n "$UNKNOWN_KEYS" ] && _fail "SCHEMA" "닫힌 키 목록 밖:$UNKNOWN_KEYS (오타 축 무음드롭 방지 — 무시하지 않는다)"
132
201
 
202
+ # ── `requires_cwd: SELF` 를 **M1 보다 먼저** 푼다 (2026-08-16 순서 수리) ────
203
+ # 초판은 M5 에서 풀었는데, M1 이 그보다 먼저 돌면서 **상대 entry 를 현재 cwd 기준으로** 찾았다.
204
+ # FH 로컬 등록부 시절엔 entry 가 늘 절대경로라 안 터졌다 — 하네스 자기 선언(상대 entry +
205
+ # SELF)이 그 잠재 가정을 처음 실행에 노출시킨 것이다([[feedback_wiring_surfaces_hidden_failures]]).
206
+ # 해석 실패는 여기서 조용히 넘기고 M5 가 판정한다(한 결함을 두 축에서 두 번 세지 않는다).
207
+ if [ "$CAP_requires_cwd" = "SELF" ]; then
208
+ local _sr; _sr="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
209
+ [ -n "$_sr" ] && [ -d "$_sr" ] && CAP_requires_cwd="$_sr" && CAP_requires_cwd_was_self=1
210
+ fi
211
+
133
212
  # ── M1 실행 가능한 진입점 ──────────────────────────────────────────────────
134
213
  local first second
135
214
  first="$(printf '%s' "$CAP_entry" | awk '{print $1}')"
@@ -139,8 +218,15 @@ _check_one() {
139
218
  _fail "M1" "entry 가 argv 가 아니라 셸 문자열이다(파이프/리다이렉트/치환 포함) — 인젝션 표면" ;;
140
219
  '') _fail "M1" "entry 미선언" ;;
141
220
  *)
221
+ # 상대 경로는 **선언된 cwd 기준**으로도 본다. 선언은 자기 레포를 기준으로 쓰는 것이
222
+ # 자연스럽고(그래야 이식된다), 검사기가 자기 cwd 로만 보면 정당한 선언을 못 읽는다.
223
+ local _second_abs="$second"
224
+ case "$second" in
225
+ /*) ;;
226
+ *) [ -n "$CAP_requires_cwd" ] && [ -r "$CAP_requires_cwd/$second" ] && _second_abs="$CAP_requires_cwd/$second" ;;
227
+ esac
142
228
  if [ -x "$first" ] 2>/dev/null; then _ok "M1" "실행 가능: $first"
143
- elif command -v "$first" >/dev/null 2>&1 && [ -r "$second" ]; then
229
+ elif command -v "$first" >/dev/null 2>&1 && [ -r "$_second_abs" ]; then
144
230
  _ok "M1" "선언된 인터프리터($first) + 읽을 수 있는 스크립트: $second"
145
231
  else _fail "M1" "entry 를 셸이 모델 없이 실행할 수 없다: $CAP_entry"; fi ;;
146
232
  esac
@@ -182,25 +268,64 @@ _check_one() {
182
268
 
183
269
  # ── M5 선언된 cwd (M4 를 그 자리에서 돌리므로 먼저) ────────────────────────
184
270
  case "$CAP_requires_cwd" in
185
- /*) [ -d "$CAP_requires_cwd" ] && _ok "M5" "requires_cwd 실재: $CAP_requires_cwd" \
186
- || _fail "M5" "requires_cwd 절대경로지만 존재하지 않는다: $CAP_requires_cwd" ;;
271
+ /*)
272
+ # `SELF` 위(M1 앞)에서 이미 레포 루트로 풀렸다. 그 사실을 판정문에 남긴다 —
273
+ # 안 남기면 독자가 선언 원문(`SELF`)과 검사 결과(절대경로)를 연결하지 못한다.
274
+ if [ -d "$CAP_requires_cwd" ]; then
275
+ if [ "${CAP_requires_cwd_was_self:-0}" = "1" ]; then
276
+ _ok "M5" "requires_cwd: SELF → 선언을 품은 레포 루트: $CAP_requires_cwd"
277
+ else
278
+ _ok "M5" "requires_cwd 실재: $CAP_requires_cwd"
279
+ fi
280
+ else
281
+ _fail "M5" "requires_cwd 가 절대경로지만 존재하지 않는다: $CAP_requires_cwd"
282
+ fi ;;
283
+ SELF)
284
+ # ★`SELF` = «이 선언을 품은 레포» (2026-08-16 신설, 정체성 ①-(a) cluster-wizard).
285
+ #
286
+ # 왜 필요했나: 이 스키마는 원래 **FH 로컬 등록부**(gitignored)를 위한 것이라 절대경로가
287
+ # 문제없었다. 그런데 (c) 를 닫으려면 선언이 **그 하네스 자신의 레포에 tracked 로** 살아야
288
+ # 하고, 그 순간 절대경로는 두 가지로 깨진다:
289
+ # ① 이식 불가 — 다른 머신·다른 체크아웃에서 그 경로는 없다
290
+ # ② **공개표면 위반** — 운영자 홈 경로가 tracked 파일에 실린다
291
+ # 그래서 «자기 자신» 을 가리키는 이식 가능한 표현이 필요했다.
292
+ #
293
+ # 해석은 **위치 가정이 아니라 git 레포 루트**다. `../../..` 같은 상대 오프셋으로 풀면
294
+ # `.claude/capabilities/` 배치를 하드코딩하게 되고, 배치가 바뀌는 순간 조용히 엉뚱한
295
+ # 디렉토리를 가리킨다. 레포 루트는 그 파일이 어디 놓이든 같은 답을 준다.
296
+ local _self_root
297
+ _self_root="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
298
+ if [ -n "$_self_root" ] && [ -d "$_self_root" ]; then
299
+ CAP_requires_cwd="$_self_root"
300
+ _ok "M5" "requires_cwd: SELF → 선언을 품은 레포 루트로 해석: $_self_root"
301
+ else
302
+ # 🟥 fail-closed. git 레포 밖의 선언은 «자기» 가 무엇인지 정의되지 않는다 —
303
+ # 추측해서 cwd 를 정하면 M4 가 **엉뚱한 트리에서** 진입점을 돌린다.
304
+ _fail "M5" "requires_cwd: SELF 인데 이 선언이 git 레포 안에 없다 — «자기» 를 해석할 수 없다"
305
+ fi ;;
187
306
  '') _fail "M5" "requires_cwd 미선언 — 콕핏이 어디서 부를지 알 수 없다" ;;
188
- *) _fail "M5" "requires_cwd 가 절대경로가 아니다: $CAP_requires_cwd" ;;
307
+ *) _fail "M5" "requires_cwd 가 절대경로도 SELF 도 아니다: $CAP_requires_cwd" ;;
189
308
  esac
190
309
 
191
310
  # ── M4 캘리브레이션 쌍 — 선언 + **실행** ──────────────────────────────────
192
311
  if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
193
312
  _fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
313
+ OBS_WHY="캘리브레이션 쌍 미선언"
194
314
  elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
195
315
  printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
316
+ OBS_WHY="앞선 축 실패로 M4 생략"
196
317
  elif ! _validate_arm_args "$CAP_cal_pos_args" || ! _validate_arm_args "$CAP_cal_neg_args"; then
197
318
  printf ' ⏭ M4 — args 검문 실패로 arm 을 실행하지 않았다(SKIPPED, PASS 아님)\n'
319
+ OBS_WHY="args 검문 실패"
198
320
  else
199
- local pos_name neg_name pos_rc neg_rc pos_name2
321
+ local pos_name neg_name pos_rc neg_rc pos_name2 pos_rc2
200
322
  _run_arm "$CAP_cal_pos_args"; pos_rc="$ARM_RC"; pos_name="$ARM_NAME"
201
323
  [ "$pos_rc" = "127" ] && _fail "M4" "requires_cwd 로 진입 실패 — arm 을 돌릴 수 없다"
202
- _run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME" # reps=2 (M3 부분 방어)
324
+ _run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME"; pos_rc2="$ARM_RC" # reps=2 (M3 부분 방어)
203
325
  _run_arm "$CAP_cal_neg_args"; neg_rc="$ARM_RC"; neg_name="$ARM_NAME"
326
+ # 관측 원장에 남긴다 — 판정과 무관하게, 이 실행이 **실제로 밟은 exit** 만 적는다.
327
+ OBS_STATE=run; OBS_POS_RC="$pos_rc"; OBS_POS_RC2="$pos_rc2"; OBS_NEG_RC="$neg_rc"
328
+ OBS_POS_NAME="$pos_name"; OBS_NEG_NAME="$neg_name"
204
329
 
205
330
  if [ -z "$pos_name" ]; then
206
331
  _fail "M4" "양성 arm 의 exit $pos_rc 가 선언된 enum 밖 — enum 밖 값은 HARNESS_ERROR 이지 PASS 가 아니다"
@@ -254,6 +379,7 @@ _check_one() {
254
379
  _hops=$((_hops+1))
255
380
  if [ "$_hops" -gt 40 ]; then
256
381
  _fail "M6" "심링크가 40회를 넘어간다 — 순환으로 본다. 프로브 경로를 못 정한다"
382
+ _observation_scope # 이 이른 return 에서도 관측 범위는 찍힌다(조건부로 찍히면 채널이 아니다)
257
383
  return 0 # _fail 이 이미 기록했다. 판정 불가를 통과로 렌더하지 않는다
258
384
  fi
259
385
  _dir=$(cd -P "$(dirname "$_src")" >/dev/null 2>&1 && pwd) || break
@@ -279,6 +405,10 @@ _check_one() {
279
405
  esac
280
406
  fi
281
407
  fi
408
+
409
+ # ★무조건 찍는다 — PASS 든 REJECTED 든, arm 을 돌렸든 안 돌렸든. 관측 범위가 조건부로
410
+ # 찍히면 그건 채널이 아니라 또 하나의 판정이 된다.
411
+ _observation_scope
282
412
  }
283
413
 
284
414
  # ── self-test (known-pair: 통과해야 할 선언 1 · 막혀야 할 선언 6) ─────────────
@@ -324,6 +454,23 @@ EOF
324
454
  sed 's|^requires_cwd: .*|requires_cwd: relative/path|' "$T/good.cap" > "$T/relcwd.cap"
325
455
  _t "M5: requires_cwd 상대경로" 1 "$T/relcwd.cap"
326
456
 
457
+ # ── M5 `SELF` (2026-08-16) — 하네스가 **자기 레포에 tracked 로** 선언할 수 있게 하는 값 ──
458
+ # known-pair 를 **양쪽 다** 세운다. 양성만 세우면 「SELF 를 그냥 통과시킨다」와
459
+ # 「SELF 를 옳게 해석한다」가 구분되지 않는다.
460
+ local SR="$T/selfrepo"
461
+ mkdir -p "$SR/.claude/capabilities"
462
+ ( cd "$SR" && git init -q . && git config user.email t@t && git config user.name t \
463
+ && echo x > seed.txt && git add seed.txt && git commit -qm init ) >/dev/null 2>&1
464
+ cp "$T/probe.sh" "$SR/probe.sh" 2>/dev/null || true
465
+ sed -e 's|^requires_cwd: .*|requires_cwd: SELF|' \
466
+ -e "s|^entry: .*|entry: bash $SR/probe.sh|" "$T/good.cap" > "$SR/.claude/capabilities/self.cap"
467
+ _t "M5: SELF 는 레포 루트로 해석된다" 0 "$SR/.claude/capabilities/self.cap"
468
+
469
+ # known-negative(=fail-closed 방향): git 레포 **밖**의 SELF 는 해석 불가여야 한다.
470
+ # 이 레인이 없으면 「SELF 면 무조건 통과」가 되고, 그건 M5 를 없앤 것과 같다.
471
+ sed -e 's|^requires_cwd: .*|requires_cwd: SELF|' "$T/good.cap" > "$T/self_outside.cap"
472
+ _t "M5: git 밖의 SELF 는 fail-closed" 1 "$T/self_outside.cap"
473
+
327
474
  # ── M6: 선언 진위 (2026-08-16) — 2026-08-11 실사고 형태를 그대로 재현한다 ──
328
475
  # `writes: read-only` 를 선언하고 진입점이 `rm -rf scripts` 를 한다. 그날 이 검사기는
329
476
  # M1–M5 를 전부 통과시켰다. 이제 M6 가 막는다.
@@ -368,6 +515,47 @@ LIAR
368
515
  else
369
516
  fail=$((fail+1)); printf ' ❌ %-34s — rc 는 1 인데 M6 가 막은 게 아니다(다른 축이 먼저 걸렸다)\n' "M6: 차단 귀속"
370
517
  fi
518
+ # ── 관측 범위 블록 (2026-08-16) — 「무엇을 안 봤는지」가 출력에 남는가 ───────────
519
+ # 이 레인들은 **판정을 재지 않는다**(rc 는 위 레인들이 이미 고정한다). 재는 것은
520
+ # «채널이 켜져 있나 · 계산이 맞나 · 조건부로 꺼지지 않나» 셋이다.
521
+ _tg() { # $1=이름 $2=기대 rc $3=capfile $4=반드시 포함할 문자열
522
+ local o r hit; o="$(bash "$0" "$3" 2>&1)"; r=$?
523
+ if printf '%s' "$o" | grep -qF "$4"; then hit=1; else hit=0; fi
524
+ if [ "$r" = "$2" ] && [ "$hit" = "1" ]; then
525
+ pass=$((pass+1)); printf ' ✅ %-34s rc=%s\n' "$1" "$r"
526
+ else
527
+ fail=$((fail+1))
528
+ printf ' ❌ %-34s rc=%s (기대 %s) · 부분문자열 «%s» %s\n%s\n' \
529
+ "$1" "$r" "$2" "$4" "$( [ "$hit" = 1 ] && printf 있음 || printf 없음 )" "$o"
530
+ fi
531
+ }
532
+
533
+ # ⓐ 부분 관측: 선언 enum 4값 · 이 실행이 밟는 건 2값 → **나머지 2값을 이름으로** 적어야 한다.
534
+ # (`good.cap` 의 probe 는 ok→0 / 그 외→1 만 낸다. 3·7 은 이 실행이 갈 수 없는 값이다.)
535
+ sed 's/^verdict_enum: .*/verdict_enum: 0=PASS 1=FAIL 3=DID_NOT_RUN 7=NO_TARGET/' \
536
+ "$T/good.cap" > "$T/obs_partial.cap"
537
+ _tg "관측범위: 미관측 enum 을 이름으로" 0 "$T/obs_partial.cap" \
538
+ "관측되지 않은** 값: 3=DID_NOT_RUN 7=NO_TARGET"
539
+
540
+ # ⓑ 🟥**컨트롤** — 전부 관측되면 「없음」을 찍어야 한다. 이게 없으면 「늘 경고하는 계기」와
541
+ # 「실제로 계산하는 계기」가 구분되지 않는다(경고만 하는 계기는 판별력이 0 이다).
542
+ # 선언 전 값이 관측되려면 음성 arm 이 「안 돌았다」 코드로 떨어져야 한다(M2+ 가 그 값을
543
+ # 요구하므로) — 그래서 전용 probe 를 쓴다.
544
+ printf '#!/bin/sh\n[ "$1" = ok ] && exit 0\nexit 3\n' > "$T/probe_allobs.sh"
545
+ sed -e 's/^verdict_enum: .*/verdict_enum: 0=PASS 3=DID_NOT_RUN/' \
546
+ -e "s|^entry: .*|entry: bash $T/probe_allobs.sh|" \
547
+ -e 's/^calibration_negative_expect: .*/calibration_negative_expect: DID_NOT_RUN/' \
548
+ "$T/good.cap" > "$T/obs_all.cap"
549
+ _tg "관측범위: 전부 관측되면 「없음」" 0 "$T/obs_all.cap" \
550
+ "관측되지 않은 값: 없음"
551
+
552
+ # ⓒ REJECTED 에서도 블록이 찍힌다 + arm 을 안 돌렸다는 사실과 사유가 적힌다.
553
+ # (`nocal.cap` = 캘리브레이션 키 전체 제거 → args 는 「미선언」, 관측 exit 은 「없음」.)
554
+ _tg "관측범위: REJECTED 에서도 찍힌다" 1 "$T/nocal.cap" \
555
+ "관측된 exit: 없음 — arm 을 실행하지 않았다(캘리브레이션 쌍 미선언)"
556
+ _tg "관측범위: 미선언 args 를 미선언이라 적는다" 1 "$T/nocal.cap" \
557
+ "실행된 arm: 양성 «<미선언>» / 음성 «<미선언>»"
558
+
371
559
  printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
372
560
  rm -rf "$T"
373
561
  [ "$fail" -eq 0 ] || return 1