@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.
- package/.claude/rules/fh_4axis_gate.md +25 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +107 -7
- package/README.ja.md +14 -2
- package/README.ko.md +14 -1
- package/README.md +16 -2
- package/README.zh.md +12 -1
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +18 -6
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +262 -7
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
- package/knowledge/shared/harness-core/sonnet_floor_doctrine.md +16 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +124 -2
- package/package.json +8 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +105 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
- package/scripts/adapters/mate_agent_boundary.sh +27 -7
- package/scripts/capability_effect_probe.sh +173 -5
- package/scripts/capability_registry_check.sh +69 -3
- package/scripts/digest_landing_check.sh +168 -13
- package/scripts/fh_session_load.sh +71 -0
- package/scripts/relay_channel.sh +151 -6
- package/scripts/selfcheck.sh +47 -0
- package/scripts/target_freeze.sh +239 -0
- package/scripts/test_adapter_lanes.sh +17 -0
- package/scripts/test_evidence_root_psa_lanes.sh +78 -0
- package/scripts/test_marker_standpoint_lanes.sh +83 -0
- package/scripts/test_marker_thirdparty_lanes.sh +73 -0
- package/scripts/test_regression_guard_ci_lanes.sh +170 -0
- package/scripts/test_relay_channel_lanes.sh +146 -17
- package/scripts/test_satellite_publish_gate_lanes.sh +339 -0
- package/scripts/test_target_freeze_lanes.sh +203 -0
- package/templates/.git-hooks/pre-commit +306 -6
- package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
- package/templates/regression_guard.sh +32 -4
|
@@ -85,13 +85,77 @@ FH="${CLAUDE_PROJECT_DIR:-$(cd "$SELF_DIR/.." && pwd)}"
|
|
|
85
85
|
# ⚠️ 검증기는 **도구**이고 FH 는 **대상**이다 — 두 축을 같은 변수로 묶으면 격리 픽스처에서
|
|
86
86
|
# 도구까지 임시 트리에서 찾다가 죽는다(실측). 도구는 언제나 이 스크립트 옆에 있다.
|
|
87
87
|
CHECKER="$SELF_DIR/utterance_landing_check.sh"
|
|
88
|
+
|
|
89
|
+
# ── 착지 스코프 (2026-08-18, R1) ────────────────────────────────────────────
|
|
90
|
+
# 기본 스코프는 **FH 자신의 문법**이다(`knowledge` · `CLAUDE.md` · gitignored `tracks/_meta`).
|
|
91
|
+
# 위성이 겨눈 대상 하네스에는 그 셋이 없어서 타깃이 0건 → rc=10 이 매 런 찍힌다. 그건 정직한
|
|
92
|
+
# 값이지만(미측정≠0), 계기가 **구조적으로 죽은 채** 배선된 것과 같다.
|
|
93
|
+
# 🟥 스코프를 «러너가 손으로 목록을 만들어 넘기는» 식으로 풀지 않는다 — 그러면 선후 필터가
|
|
94
|
+
# 두 벌이 되고 관대함이 갈리는 순간 한쪽만 통과하는 입력이 무음 드롭된다
|
|
95
|
+
# ([[feedback_divergent_leniency_duplicate_normalizers]]). 필터는 여기 한 곳에 두고
|
|
96
|
+
# **무엇을 볼지만** 밖에서 정한다.
|
|
97
|
+
# 기본값은 종전 그대로 — FH 경로 무변경.
|
|
98
|
+
# ⚠️ **명명된 잔여 (cross-family/codex F4)**: 이 값은 **공백 분리**된 다중 pathspec 이다.
|
|
99
|
+
# 따라서 **경로에 공백이 있으면 갈린다** — `docs public` 은 «한 경로»가 아니라 두 경로가 된다.
|
|
100
|
+
# 다중 경로 지원과의 트레이드오프라 오늘은 **문서화만** 했고 기계 검사는 없다.
|
|
101
|
+
DLC_TRACKED_PATHS="${DLC_TRACKED_PATHS:-knowledge CLAUDE.md}" # git 추적축 (커밋시각)
|
|
102
|
+
DLC_IGNORED_DIR="${DLC_IGNORED_DIR-tracks/_meta}" # gitignored 축 (mtime). 빈 값 = 그 축 없음
|
|
88
103
|
SECTION_RE='^## .*Immediate Application Candidates'
|
|
89
104
|
|
|
90
105
|
# 후보 표에서 (id, 판별토큰 OR 정규식) 을 뽑는다. 토큰 없으면 id 만 내고 정규식은 빈 값.
|
|
91
106
|
_extract() { # $1 = digest path
|
|
92
107
|
awk -v sec="$SECTION_RE" '
|
|
108
|
+
# 판별자 추출 — 표/문단 **두 경로가 공유**한다(중복 정규화기 방지).
|
|
109
|
+
function harvest(body, toks, tmp, w, t) {
|
|
110
|
+
tmp=body
|
|
111
|
+
while (match(tmp, /\[\[[^]]+\]\]/)) {
|
|
112
|
+
w=substr(tmp, RSTART+2, RLENGTH-4); tmp=substr(tmp, RSTART+RLENGTH)
|
|
113
|
+
if (length(w) >= 5) toks = (toks == "" ? w : toks "|" w)
|
|
114
|
+
}
|
|
115
|
+
tmp=body
|
|
116
|
+
while (match(tmp, /`[^`]+`/)) {
|
|
117
|
+
t=substr(tmp, RSTART+1, RLENGTH-2); tmp=substr(tmp, RSTART+RLENGTH)
|
|
118
|
+
sub(/[: ].*$/, "", t)
|
|
119
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", t)
|
|
120
|
+
if (length(t) < 5) continue
|
|
121
|
+
if (t ~ /[][(){}.*+?^$\\|]/) { if (t ~ /^[A-Za-z0-9_.-]+$/) { } else continue }
|
|
122
|
+
gsub(/\./, "\\.", t)
|
|
123
|
+
toks = (toks == "" ? t : toks "|" t)
|
|
124
|
+
}
|
|
125
|
+
return toks
|
|
126
|
+
}
|
|
93
127
|
$0 ~ sec { inblk=1; next }
|
|
94
|
-
inblk && /^## / { inblk=0 }
|
|
128
|
+
inblk && /^## / { if (pid != "") { print pid "\t" ptoks; pid="" } inblk=0; inpara=0 }
|
|
129
|
+
# ── 문단 형식 (2026-08-18 신설) ───────────────────────────────────────
|
|
130
|
+
# 🟥 이 계기는 후보를 **표**로만 읽었다. 실측: 다이제스트 60건 중 표 형식은 **4건**,
|
|
131
|
+
# 나머지는 `**① …**` `**[A] …**` `**1. …**` `**#1 …**` 같은 **문단 형식**이다.
|
|
132
|
+
# 즉 계기가 실물의 6.7% 만 읽을 수 있었고, 그래서 첫 실측이 rc=10(«후보 절을 못 찾았다»)
|
|
133
|
+
# 을 냈다. 안 드러난 이유는 **프로덕션 호출부가 0** 이었기 때문 — 아무도 안 돌렸다.
|
|
134
|
+
# ⚠️ 머리 표기는 여러 종이라 한 모양에 맞추지 않는다(실측: ① · [A] · 1. · #1 · 그 외).
|
|
135
|
+
# **`**` 로 열리는 줄**을 항목 시작으로 보고, 다음 항목/절까지를 본문으로 모은다.
|
|
136
|
+
# 🟥 판별자 추출은 **표 경로와 같은 코드**를 쓴다 — 두 벌로 갈리면 관대함이 어긋나
|
|
137
|
+
# 한쪽만 통과하는 입력이 무음 드롭된다(divergent leniency).
|
|
138
|
+
# 항목 머리 **네** 형식을 다 받는다. 실측(60건 후보 절 전수, 하나씩 열어서 확인):
|
|
139
|
+
# `**…**` 74 · 표 `|` 28 · `### [M] 1.` 22 · **번호 리스트 `1. **…**`**(초기 다이제스트의 주 형식)
|
|
140
|
+
# 🟥 초판은 `**` 만 받았고, 표가 섞인 절에서 **표 앞의 `**` 줄을 먼저 삼켜** 표 행을
|
|
141
|
+
# UNCHECKABLE 로 떨어뜨렸다(회귀 실측). 그래서 **표 행은 항상 표 경로가 먼저 가져간다**.
|
|
142
|
+
inblk && (/^###+ / || /^\*\*/ || /^[0-9]+\. / || /^- \*\*/) {
|
|
143
|
+
if (pid != "") { print pid "\t" ptoks }
|
|
144
|
+
pid=$0; ptoks=""
|
|
145
|
+
sub(/^###+ /, "", pid); sub(/^[0-9]+\. /, "", pid); sub(/^- /, "", pid)
|
|
146
|
+
sub(/^\*\*/, "", pid); sub(/\*\*.*$/, "", pid)
|
|
147
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", pid)
|
|
148
|
+
# 🟥 substr 로 자르지 않는다 — awk 는 로케일에 따라 **바이트** 단위라 한글/기호를
|
|
149
|
+
# 중간에서 쪼개고 그 출력이 UTF-8 로 안 읽힌다(실측: 집계 스크립트가 UnicodeDecodeError).
|
|
150
|
+
# id 는 표시용이므로 자르지 말고 **공백 기준 앞 6낱말**만 쓴다.
|
|
151
|
+
if (split(pid, _w, /[[:space:]]+/) > 6) {
|
|
152
|
+
pid=""; for (_i=1; _i<=6; _i++) pid = pid (_i>1 ? " " : "") _w[_i]
|
|
153
|
+
}
|
|
154
|
+
ptoks=harvest($0, ptoks)
|
|
155
|
+
inpara=1; next
|
|
156
|
+
}
|
|
157
|
+
# 표 행은 아래 표 경로가 처리한다 — 여기서 안 먹는다(우선순위 명시).
|
|
158
|
+
inblk && inpara && !/^\|/ && !/^###+ / && !/^[0-9]+\. / && !/^- \*\*/ { ptoks=harvest($0, ptoks); next }
|
|
95
159
|
inblk && /^\|/ {
|
|
96
160
|
line=$0
|
|
97
161
|
# 헤더/구분선 제외
|
|
@@ -135,6 +199,7 @@ _extract() { # $1 = digest path
|
|
|
135
199
|
}
|
|
136
200
|
print id "\t" toks
|
|
137
201
|
}
|
|
202
|
+
END { if (pid != "") print pid "\t" ptoks }
|
|
138
203
|
' "$1"
|
|
139
204
|
}
|
|
140
205
|
|
|
@@ -143,8 +208,30 @@ do_extract() { # $1 = digest
|
|
|
143
208
|
[ -f "$d" ] || { echo "❌ digest 없음: $d" >&2; return 10; }
|
|
144
209
|
# 컨트롤 — 이 계기가 살아 있는지 증명한다. 대상 트리에 확실히 존재하는 토큰.
|
|
145
210
|
# 컨트롤이 죽으면 착지 검증기가 rc=10 을 내고 타깃 결과를 인쇄하지 않는다.
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
# 🟥 R1-b (2026-08-18) — 컨트롤이 `forge-harness`/`session` 으로 **하드코딩**이었다.
|
|
212
|
+
# 위성이 겨눈 레포에는 그 두 토큰이 없으므로 **컨트롤이 매번 죽고** 계기는 rc=10 만 낸다.
|
|
213
|
+
# R1 의 인자 버그를 고쳐도 이게 남아 있으면 계기는 여전히 구조적으로 죽은 채다 — 그 사실을
|
|
214
|
+
# 이 레인이 잡았다(수리가 신규 결함의 출처인 것과 같은 얼굴: 반쪽 수리).
|
|
215
|
+
# 기본값은 **자기 레포 이름**이라 FH 에서는 종전과 동일(`forge-harness`)하다.
|
|
216
|
+
# 🟥 두 갈래를 갈라야 한다 (cross-family/codex F3 지목, 실측으로 **더 나쁜 형태** 확인):
|
|
217
|
+
# 초판은 `for _ctl in ${DLC_CONTROLS:-$(basename "$FH") session}` 였는데, 비인용 명령치환은
|
|
218
|
+
# 단어분리 **와 글로빙**을 둘 다 탄다 — 레포명이 `.*` 인 디렉터리에서 실측하니 컨트롤이
|
|
219
|
+
# `.git` 등 **4개로 글로브 확장**됐다. 게다가 컨트롤은 ERE 로 쓰이므로 레포명에 정규식
|
|
220
|
+
# 메타문자가 있으면 «아무 내용에나 매치»해 **계기 생존이 오탐**이 된다.
|
|
221
|
+
# ⇒ 유도 기본값은 **리터럴로 취급**(메타문자 이스케이프) · 사용자 지정값만 정규식으로 둔다.
|
|
222
|
+
local _ctl _label=1
|
|
223
|
+
if [ -n "${DLC_CONTROLS:-}" ]; then
|
|
224
|
+
for _ctl in $DLC_CONTROLS; do # 명시값은 «정규식» 계약 — 의도적 분리
|
|
225
|
+
printf 'CONTROL\t%s\t컨트롤%s: %s\n' "$_ctl" "$_label" "$_ctl"
|
|
226
|
+
_label=$((_label+1))
|
|
227
|
+
done
|
|
228
|
+
else
|
|
229
|
+
local _repo; _repo="$(basename "$FH")"
|
|
230
|
+
# ERE 메타문자 이스케이프 — 유도값은 «그 문자열이 실제로 있는가» 를 묻는 것이지 패턴이 아니다
|
|
231
|
+
_repo="$(printf '%s' "$_repo" | sed 's/[][\.^$*+?(){}|]/\\&/g')"
|
|
232
|
+
printf 'CONTROL\t%s\t컨트롤1: %s\n' "$_repo" "$_repo"
|
|
233
|
+
printf 'CONTROL\tsession\t컨트롤2: session\n'
|
|
234
|
+
fi
|
|
148
235
|
while IFS=$'\t' read -r id toks; do
|
|
149
236
|
[ -n "${id:-}" ] || continue
|
|
150
237
|
n=$((n+1))
|
|
@@ -168,7 +255,14 @@ do_check() {
|
|
|
168
255
|
[ -x "$CHECKER" ] || [ -f "$CHECKER" ] || {
|
|
169
256
|
echo "❌ 착지 검증기 부재: $CHECKER (이 스크립트는 검증을 자체 구현하지 않는다)" >&2; return 10; }
|
|
170
257
|
|
|
171
|
-
|
|
258
|
+
# 🟥 R7 (2026-08-19, **첫 실사용 발견**) — `set -u` + bash 3.2 에서 **빈 배열의 `${a[@]}` 는
|
|
259
|
+
# unbound 에러**다. 대상 레포에 digest 이후 커밋이 없으면 targets 가 비고, 그러면 아래
|
|
260
|
+
# 필터 루프가 터진다. 그 순간 스크립트는 판정을 인쇄하지 못한 채 **rc=1 로 끝나고**,
|
|
261
|
+
# 러너는 그 1 을 「SOME-UNLANDED」로 로그에 적는다 — 즉 **셸 에러가 착지율 시계열에
|
|
262
|
+
# 실측값으로 들어간다**([[feedback_not_found_is_not_zero_family]] 의 가장 조용한 얼굴).
|
|
263
|
+
# 실측: forge-wiki 대상 첫 런이 정확히 그렇게 «SOME-UNLANDED (rc=1)» 을 남겼고 본문은 없었다.
|
|
264
|
+
# 올바른 값은 rc=10(타깃 0건 = HARNESS-ERROR)이다. 빈 배열 확장을 전부 방어한다.
|
|
265
|
+
local -a targets=()
|
|
172
266
|
if [ "$#" -gt 0 ]; then targets=("$@")
|
|
173
267
|
else
|
|
174
268
|
# 기본 스코프 = 공개 FH 자산 + tracks. **비공개 store 제외**(운영자 결정).
|
|
@@ -176,7 +270,8 @@ do_check() {
|
|
|
176
270
|
# ⚠️ CLAUDE.md 도 **선후 필터를 거친다.** 초판은 무조건 추가했고, 그래서 이 파일 하나가
|
|
177
271
|
# 필터를 우회해 오탐 원천이 됐다 — 손검증에서 "M1 착지 1파일" 의 그 1파일이 CLAUDE.md
|
|
178
272
|
# 였고, 후보와 무관하게 원래 있던 토큰이었다. 예외 경로 하나가 필터 전체를 무력화한다.
|
|
179
|
-
|
|
273
|
+
case " $DLC_TRACKED_PATHS " in *" CLAUDE.md "*)
|
|
274
|
+
[ -f "$FH/CLAUDE.md" ] && [ "$FH/CLAUDE.md" -nt "$d" ] && targets+=("$FH/CLAUDE.md") ;; esac
|
|
180
275
|
# ★ **digest 보다 나중에 수정된 파일만** 본다. grep 은 "토큰이 있다" 만 재고
|
|
181
276
|
# "이 후보 때문에 생겼다" 는 못 잰다 — 이미 있던 문서가 착지로 잡힌다(실측: 후보 하나가
|
|
182
277
|
# 21파일 히트, 전부 무관한 기존 문서였다). 선후 필터가 인과를 주지는 않지만
|
|
@@ -211,11 +306,13 @@ do_check() {
|
|
|
211
306
|
[ -n "$f" ] || continue
|
|
212
307
|
case "$f" in *.md) ;; *) continue ;; esac
|
|
213
308
|
[ -f "$FH/$f" ] && targets+=("$FH/$f")
|
|
214
|
-
done < <(git -C "$FH" log --since="@$_dts" --name-only --pretty=format: --
|
|
215
|
-
|
|
216
|
-
|
|
309
|
+
done < <(git -C "$FH" log --since="@$_dts" --name-only --pretty=format: -- $DLC_TRACKED_PATHS 2>/dev/null | sort -u)
|
|
310
|
+
if [ -n "$DLC_IGNORED_DIR" ] && [ -d "$FH/$DLC_IGNORED_DIR" ]; then
|
|
311
|
+
while IFS= read -r f; do targets+=("$f"); done < <(
|
|
312
|
+
find "$FH/$DLC_IGNORED_DIR" -name '*.md' -type f -newer "$d" 2>/dev/null | head -400)
|
|
313
|
+
fi
|
|
217
314
|
# dirty tracked — 커밋 안 된 수정본엔 git 시각 증거가 없다. 초록으로 만들지 않고 셈에 남긴다.
|
|
218
|
-
DIRTY_TRACKED="$(git -C "$FH" diff --name-only --
|
|
315
|
+
DIRTY_TRACKED="$(git -C "$FH" diff --name-only -- $DLC_TRACKED_PATHS 2>/dev/null | grep -c '[.]md$')" || true
|
|
219
316
|
DIRTY_TRACKED="${DIRTY_TRACKED//[^0-9]/}"; DIRTY_TRACKED="${DIRTY_TRACKED:-0}"
|
|
220
317
|
fi
|
|
221
318
|
# ★ 자기참조 차단 — **이 계기의 첫 실물 실행에서 100% 오탐을 낸 원인**이다.
|
|
@@ -224,7 +321,7 @@ do_check() {
|
|
|
224
321
|
local _dbase; _dbase="$(basename "$d")"
|
|
225
322
|
local -a _filtered=()
|
|
226
323
|
local _t
|
|
227
|
-
for _t in "${targets[@]}"; do
|
|
324
|
+
for _t in ${targets[@]+"${targets[@]}"}; do
|
|
228
325
|
case "$_t" in
|
|
229
326
|
*"$_dbase") continue ;; # digest 자신
|
|
230
327
|
*/logs/*) continue ;; # 그 생성 로그
|
|
@@ -232,7 +329,7 @@ do_check() {
|
|
|
232
329
|
esac
|
|
233
330
|
_filtered+=("$_t")
|
|
234
331
|
done
|
|
235
|
-
targets=("${_filtered[@]}")
|
|
332
|
+
targets=(${_filtered[@]+"${_filtered[@]}"})
|
|
236
333
|
[ "${#targets[@]}" -gt 0 ] || { echo "❌ 타깃 0건" >&2; return 10; }
|
|
237
334
|
|
|
238
335
|
local probes; probes="$(mktemp -t dlc.XXXXXX)" || return 10
|
|
@@ -244,7 +341,7 @@ do_check() {
|
|
|
244
341
|
local unchk; unchk="$(grep -c '^# UNCHECKABLE' "$probes" 2>/dev/null)" || true
|
|
245
342
|
unchk="${unchk//[^0-9]/}"; unchk="${unchk:-0}"
|
|
246
343
|
echo "── digest 착지 검증: $(basename "$d") ──"
|
|
247
|
-
echo " 스코프:
|
|
344
|
+
echo " 스코프: git추적[${DLC_TRACKED_PATHS}] + ignored[${DLC_IGNORED_DIR:-없음}] (비공개 companion store 제외 — '조직 전파'가 아니라 '레포 내부 착지')"
|
|
248
345
|
echo " 타깃 파일 ${#targets[@]}건 (digest 이후 수정분만) · 판별불가 후보 ${unchk}건"
|
|
249
346
|
echo " 축: git추적=커밋시각 · gitignored=mtime · dirty tracked ${DIRTY_TRACKED:-0}건=UNMEASURED"
|
|
250
347
|
echo " ⚠️ 이 계기는 **선후**를 잰다. 인과가 아니다 — 히트는 열어서 확인하라"
|
|
@@ -331,8 +428,66 @@ EOF
|
|
|
331
428
|
touch knowledge/old.md
|
|
332
429
|
printf 'forge-harness session control\n' > tracks/_meta/card.md
|
|
333
430
|
) >/dev/null 2>&1
|
|
334
|
-
|
|
431
|
+
# 컨트롤은 픽스처가 실제로 담고 있는 토큰으로 고정한다(레포명이 mktemp 난수라서).
|
|
432
|
+
# 🟥 이 레인의 주장은 **git/mtime 두 축 분리**지 컨트롤 유도가 아니다 — 축을 섞지 않는다.
|
|
433
|
+
out="$( cd "$G" && CLAUDE_PROJECT_DIR="$G" DLC_CONTROLS="forge-harness session" \
|
|
434
|
+
bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" \
|
|
335
435
|
"$G/tracks/_meta/frontier_digest_2001_01_02.md" 2>&1 )"
|
|
436
|
+
# ── ★N-ctl (2026-08-18, R1-b) — 컨트롤이 **레포 이름을 따라간다** ────────────
|
|
437
|
+
# 초판은 `forge-harness`/`session` 하드코딩이라 **위성이 겨눈 레포에선 매번 컨트롤 사망**
|
|
438
|
+
# (= rc=10)이었다. R1 의 인자 버그를 고쳐도 계기는 구조적으로 죽은 채였을 것이다.
|
|
439
|
+
local out_ctl rc_ctl
|
|
440
|
+
out_ctl="$( cd "$G" && CLAUDE_PROJECT_DIR="$G" bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" \
|
|
441
|
+
"$G/tracks/_meta/frontier_digest_2001_01_02.md" 2>&1 )"; rc_ctl=$?
|
|
442
|
+
# 픽스처 레포 이름(mktemp 난수)은 본문에 없다 ⇒ 유도된 컨트롤이 죽어 rc=10 이 정상이다.
|
|
443
|
+
# 이것이 «유도가 실제로 돈다»의 증거다(하드코딩이면 여기서 rc=1 이 나온다).
|
|
444
|
+
_t "★N-ctl 컨트롤이 레포명을 따라간다(하드코딩 아님)" 10 "$rc_ctl"
|
|
445
|
+
# ── ★N-ctl-re (cross-family/codex F3) — 유도 컨트롤은 **리터럴**이지 정규식이 아니다 ──────
|
|
446
|
+
# 초판은 `for _ctl in ${DLC_CONTROLS:-$(basename "$FH") session}` 였다. 비인용 명령치환이라
|
|
447
|
+
# ⓐ 레포명이 ERE 메타문자면 «아무 내용에나 매치»해 계기 생존이 **오탐**이 되고
|
|
448
|
+
# ⓑ 실측하니 글로빙까지 타서 컨트롤이 `.git` 등으로 **확장**되기도 했다.
|
|
449
|
+
# 🟥 픽스처는 **판별하는 형태**여야 한다. 첫 판본은 레포명을 `.*` 로 잡았는데, 그건 글로빙에
|
|
450
|
+
# 걸려 죽은 컨트롤이 생기는 바람에 **버그 판본도 rc=10** 을 냈다 — 되돌림 프로브가 그
|
|
451
|
+
# 장식성을 잡았다([[feedback_fixture_must_use_the_breaking_spelling]]).
|
|
452
|
+
# 그래서 레포명을 `a.b` 로 쓴다: 글로브로는 아무것도 안 맞아 그대로 남고(확장 없음),
|
|
453
|
+
# ERE 로는 `axb` 에 매치된다. 대상에 `axb` 만 두면
|
|
454
|
+
# 버그 판본 → 컨트롤 **생존(오탐)** → rc≠10
|
|
455
|
+
# 수리 판본 → 리터럴 `a.b` 없음 → 컨트롤 사망 → rc=10
|
|
456
|
+
local RG; RG="$(mktemp -d -t 'dlc_re.XXXXXX')" || return 10
|
|
457
|
+
local RD="$RG/a.b"
|
|
458
|
+
(
|
|
459
|
+
mkdir -p "$RD/tracks/_meta" "$RD/knowledge" && cd "$RD" \
|
|
460
|
+
&& git init -q . && git config user.email t@t && git config user.name t
|
|
461
|
+
printf '# d\n## 📌 FH Immediate Application Candidates\n\n| # | 티어 | 내용 |\n|---|---|---|\n| **A1** | M | `zzz_absent_token` 후보 |\n\n## end\n' \
|
|
462
|
+
> tracks/_meta/frontier_digest_2001_01_01.md
|
|
463
|
+
touch -t 200101010000 tracks/_meta/frontier_digest_2001_01_01.md
|
|
464
|
+
# 리터럴 `a.b` 는 **없다**. `axb`(ERE 로만 매치) 와 `session` 만 있다.
|
|
465
|
+
printf 'axb 그리고 session\n' > tracks/_meta/card.md
|
|
466
|
+
) >/dev/null 2>&1
|
|
467
|
+
local rc_re
|
|
468
|
+
( cd "$RD" && CLAUDE_PROJECT_DIR="$RD" bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" \
|
|
469
|
+
"$RD/tracks/_meta/frontier_digest_2001_01_01.md" ) >/dev/null 2>&1; rc_re=$?
|
|
470
|
+
_t "★N-ctl-re 레포명이 정규식 메타문자여도 리터럴로 취급(컨트롤 오탐 없음)" 10 "$rc_re"
|
|
471
|
+
rm -rf "$RG"
|
|
472
|
+
|
|
473
|
+
# ── ★N-empty (2026-08-19, R7) — 타깃이 0건이면 **rc=10** 이지 rc=1 이 아니다 ────────────
|
|
474
|
+
# 🟥 `set -u` + bash 3.2 에서 빈 배열 확장이 터지면, 스크립트는 판정을 못 내고 **rc=1** 로
|
|
475
|
+
# 끝난다. 러너는 그 1 을 「SOME-UNLANDED」로 적는다 — 셸 에러가 착지율 시계열에 실측값으로
|
|
476
|
+
# 들어간다. 첫 실사용(forge-wiki 대상)에서 정확히 그 로그가 나왔고 본문은 비어 있었다.
|
|
477
|
+
local EG; EG="$(mktemp -d -t dlc_e.XXXXXX)" || return 10
|
|
478
|
+
(
|
|
479
|
+
mkdir -p "$EG/wiki" && cd "$EG" && git init -q . \
|
|
480
|
+
&& git config user.email t@t && git config user.name t
|
|
481
|
+
printf '# d\n## 📌 FH Immediate Application Candidates\n\n| # | 티어 | 내용 |\n|---|---|---|\n| **A1** | M | `zzz_tok` 후보 |\n\n## end\n' \
|
|
482
|
+
> wiki/frontier_digest_2001_01_01.md
|
|
483
|
+
) >/dev/null 2>&1
|
|
484
|
+
# digest 이후 커밋 0 · ignored 축 없음 ⇒ 타깃 0건
|
|
485
|
+
local rc_e
|
|
486
|
+
( cd "$EG" && CLAUDE_PROJECT_DIR="$EG" DLC_TRACKED_PATHS="wiki" DLC_IGNORED_DIR="" \
|
|
487
|
+
DLC_CONTROLS="zzz_tok" bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" \
|
|
488
|
+
"$EG/wiki/frontier_digest_2001_01_01.md" ) >/dev/null 2>&1; rc_e=$?
|
|
489
|
+
_t "★N-empty 타깃 0건은 HARNESS-ERROR(10)이지 미착지(1)가 아니다" 10 "$rc_e"
|
|
490
|
+
rm -rf "$EG"
|
|
336
491
|
# ⚠️ **줄 단위로** 본다. `case *"A1"*"미착지"*` 는 마지막 요약줄("N건 중 M건 미착지")까지
|
|
337
492
|
# 삼켜 오매칭한다 — 문자열 위치로 판정하는 prose-grep 함정([[feedback_typed_verdict_channel]]).
|
|
338
493
|
if printf '%s\n' "$out" | grep -qE '후보 A1[[:space:]]+미착지'; then
|
|
@@ -290,6 +290,77 @@ fi
|
|
|
290
290
|
( cd "$FH" && bash scripts/branch_claim.sh reap >/dev/null 2>&1 ) || true
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
# ── 매핑된 프로젝트의 capability 표면화 (2026-08-18) ───────────────────────────
|
|
294
|
+
# WHY HERE (branch-claim 과 같은 자리, early exit **위**): 이건 Mode D 전용이 아니다.
|
|
295
|
+
# companion store 가 없는 설치에서도 프로젝트는 매핑되고, 그때도 형제 하네스의 스킬은
|
|
296
|
+
# 안 보인다. 아래 early exit 밑에 두면 그 설치에서 통째로 죽는다 — 바로 위 branch-claim
|
|
297
|
+
# 블록이 같은 이유로 여기 있다.
|
|
298
|
+
# ⚠️ **정밀화(cross-family 적발)**: `tracks/` 는 npm 산출물에 **안 들어간다**(per-install 데이터).
|
|
299
|
+
# 그러니 «갓 설치한» 소비자에게는 이 블록이 아무것도 안 찍는다 — 값은 그 사람이 프로젝트를
|
|
300
|
+
# **매핑한 뒤부터** 생긴다. 「공개 사용자에게도 산다」는 그런 뜻이지 «설치 즉시 값이 있다» 가
|
|
301
|
+
# 아니다. (실측: 팩 산출물에 tracks 없음 → 침묵 · tracks/someproject 생성 후 → 발화)
|
|
302
|
+
#
|
|
303
|
+
# WHY AT ALL: `CLAUDE.md §Cross-Project Skill Bus` 는 registry 를 **온보딩 Step 1-c** 에서
|
|
304
|
+
# 읽는데, §Active Onboarding 의 Guards 가 «explicit task-entry → skip onboarding menu» 다.
|
|
305
|
+
# 첫 발화가 task 면 메뉴와 함께 **데이터 로드까지 같이 꺼진다.** 메뉴는 UI 고 registry 는
|
|
306
|
+
# 데이터인데 한 단계에 묶여 있어서 UI 를 끄면 데이터도 꺼진다.
|
|
307
|
+
# → Mode D companion-store 로드는 이 병을 이미 한 번 고쳤다(«이건 메뉴가 아니라 데이터
|
|
308
|
+
# 로드다» carve-out + 이 스크립트). **registry 엔 그 처방이 안 붙었다** = 반쪽-픽스
|
|
309
|
+
# 전파경계.
|
|
310
|
+
# 실측(2026-08-18): `tracks/gstack` 이 매핑돼 있고 SKILL.md 가 55개인데, 운영자가 이름을
|
|
311
|
+
# 댈 때까지 세션이 그 존재를 몰랐다. CATALOG·CLAUDE.md·sim-conductor 에 전부 등록돼 있었다.
|
|
312
|
+
#
|
|
313
|
+
# 비용 경계: 이름과 개수만. 목록 전량 로드는 안 한다(토큰). 실물이 없거나 스킬이 0인 트랙은
|
|
314
|
+
# 아예 안 찍는다 — 매 세션 13줄이 뜨면 그건 배경 소음이고 아무도 안 읽는다.
|
|
315
|
+
# 🟥 0 을 «없다» 로 렌더하지 않는다: 프로젝트 루트를 못 찾으면 그렇게 적고, 개수를 0 으로
|
|
316
|
+
# 적지 않는다(부재 ≠ 0).
|
|
317
|
+
_PROJ_ROOT="${FH_PROJECTS_ROOT:-$(dirname "$FH")}"
|
|
318
|
+
if [ -d "$FH/tracks" ]; then
|
|
319
|
+
_reg=""; _mapped=0; _resolved=0
|
|
320
|
+
for _t in "$FH"/tracks/*/; do
|
|
321
|
+
_n=$(basename "$_t")
|
|
322
|
+
case "$_n" in _*) continue ;; esac
|
|
323
|
+
[ "$_n" = "*" ] && continue
|
|
324
|
+
_mapped=$((_mapped + 1))
|
|
325
|
+
_d="$_PROJ_ROOT/$_n"
|
|
326
|
+
[ -d "$_d" ] || continue
|
|
327
|
+
_resolved=$((_resolved + 1))
|
|
328
|
+
# 무거운 디렉터리를 쳐낸다 — 비용 주장이 «내 머신의 내 프로젝트 집합» 위에 서 있으면
|
|
329
|
+
# 안 된다(cross-family 지적). node_modules/.venv/.git 이 depth 3 안에 있으면 선형으로 는다.
|
|
330
|
+
_sk=$(find "$_d" -maxdepth 3 \( -name node_modules -o -name .git -o -name .venv \
|
|
331
|
+
-o -name venv -o -name target -o -name dist \) -prune -o \
|
|
332
|
+
-name SKILL.md -print 2>/dev/null | wc -l | tr -d ' ')
|
|
333
|
+
[ "${_sk:-0}" -gt 0 ] || continue
|
|
334
|
+
_reg="${_reg} ▸ ${_n} — SKILL.md ${_sk}개 (${_d})
|
|
335
|
+
"
|
|
336
|
+
done
|
|
337
|
+
# 🟥 **부재 ≠ 0.** 판별자는 «루트 디렉터리가 있나»가 아니다 — `dirname "$FH"` 는 거의 항상
|
|
338
|
+
# 존재해서 그 분기는 발화 불가한 장식이었다(초판 결함, 같은 커밋에서 자력 적발).
|
|
339
|
+
# 실질 판별자는 **매핑된 트랙이 있는데 하나도 실물로 해소되지 않았다** 이고, 그때만
|
|
340
|
+
# 「못 쟀다」를 말한다. 소비자의 레포가 다른 경로에 있으면 정확히 이 상태가 된다.
|
|
341
|
+
# 🟥 **all-or-nothing 이면 안 된다** (cross-family 적발, 초판 결함): «전부 못 찾았을 때만»
|
|
342
|
+
# 말하면 13개 중 1개만 해소되고 12개가 없을 때 그 12개가 **완전히 침묵**한다 — 이 블록이
|
|
343
|
+
# 없애려는 «부재를 0으로 렌더» 를 블록 자신이 재현한다. 미해소가 하나라도 있으면 말한다.
|
|
344
|
+
_unres=$((_mapped - _resolved))
|
|
345
|
+
if [ "$_unres" -gt 0 ]; then
|
|
346
|
+
_reg="${_reg} ⚠️ 매핑된 트랙 ${_mapped}개 중 ${_unres}개는 «${_PROJ_ROOT}» 밑에서 실물을 못 찾았다 — 그 ${_unres}개는 «0» 이 아니라 **미측정**이다 (경로가 다르면 FH_PROJECTS_ROOT).
|
|
347
|
+
"
|
|
348
|
+
fi
|
|
349
|
+
if [ -n "$_reg" ]; then
|
|
350
|
+
if [ "$_resolved" -gt 0 ]; then
|
|
351
|
+
echo "🧩 [cross-project skill bus] 매핑된 프로젝트에 SKILL.md 가 있다 — 파일 개수만 센 것이지 «지금 세션에서 호출 가능하다» 는 확인이 아니다."
|
|
352
|
+
printf "%s" "$_reg"
|
|
353
|
+
# 목록이 실제로 있을 때만 낸다. 강등(못 찾음) 케이스에 이 안내가 붙으면 «읽을 것이 있다» 는
|
|
354
|
+
# 인상을 주는데 목록은 비어 있다 — 없는 것을 있다고 말하는 쪽이다.
|
|
355
|
+
echo " ⚠️ 이 줄은 «있다» 만 말한다. 쓰기 전에 그쪽 SKILL.md 를 읽어라 — 필드 하네스 용어를"
|
|
356
|
+
echo " 일반 개념으로 정규화하는 것이 이 레포의 상습 실패다."
|
|
357
|
+
else
|
|
358
|
+
echo "🧩 [cross-project skill bus] 매핑은 있는데 실물을 못 찾았다 — 아래는 **미측정**이다."
|
|
359
|
+
printf "%s" "$_reg"
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
|
|
293
364
|
# Non-Mode-D / no companion store → silent no-op (this is the majority path for public users).
|
|
294
365
|
[ -d "$BE/.git" ] || exit 0
|
|
295
366
|
|
package/scripts/relay_channel.sh
CHANGED
|
@@ -21,10 +21,20 @@
|
|
|
21
21
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
22
|
# 이 계기가 증명하지 않는 것 (과잉주장 금지 — 명명된 잔여)
|
|
23
23
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
# · **등록
|
|
25
|
-
# `scripts/capability_registry_check.sh`
|
|
26
|
-
#
|
|
27
|
-
#
|
|
24
|
+
# · ✅ **등록 시점 선언층은 2026-08-17 부터 여기서 막는다(배선됨).** `do_run` 이
|
|
25
|
+
# `_registry_gate` 로 `scripts/capability_registry_check.sh --declaration-only` 를 부르고,
|
|
26
|
+
# REJECTED 면 조합을 열지 않는다(COMPOSITION_VIOLATION).
|
|
27
|
+
# 🟥 **남은 잔여는 좁아졌을 뿐 사라지지 않았다**: 그 모드는 M4(known-pair 실행)·M6(선언
|
|
28
|
+
# 진위)를 **안 돌린다**. 즉 여기서 닫히는 것은 «선언이 형식에 맞나»(M1·M2·M3·M5 +
|
|
29
|
+
# 추가조항)이고, «선언이 **진실인지**»는 여전히 검사하지 않는다 — `writes: read-only`
|
|
30
|
+
# 의 진위는 등록 시점 M6 의 몫이다. 왜 전량 호출을 안 했는지는 `_registry_gate` 헤더에.
|
|
31
|
+
#
|
|
32
|
+
# · (이력) 🟥 **이 자리에 이렇게 적혀 있었다 — 그리고 그 문장이 잔여를 은폐했다.**
|
|
33
|
+
# 원문: *"`scripts/capability_registry_check.sh`(M1–M5 + M4 쌍 실행)는 **여전히 없다**."*
|
|
34
|
+
# 2026-08-17 실측 시점에 그 파일은 **42KB 로 실재했고 정상 작동했다.** 낡은 서술이
|
|
35
|
+
# «없으니 안 부르는 게 당연하다» 를 정당화하는 형태로 남아 있었고, 그 사이 relay 는
|
|
36
|
+
# `❌ REJECTED` 판정을 받는 capfile 을 그냥 실행해 CLEAN 을 체인에 병합했다.
|
|
37
|
+
# **stale 한 잔여 서술은 잔여를 기록하는 게 아니라 은폐한다.**
|
|
28
38
|
# · **호출 순간 자체엔 기계 floor 가 없다.** "지금 capability 를 조합하려 한다" 를
|
|
29
39
|
# 관측하는 훅은 존재하지 않는다(트리거가 의도다). 이 스크립트는 *호출되면* 계산을
|
|
30
40
|
# 강제하지만, **호출되도록 강제하지는 못한다.** 스펙 §1 이 이미 그렇게 적어놨고
|
|
@@ -507,6 +517,73 @@ _invoke_node() {
|
|
|
507
517
|
return 0
|
|
508
518
|
}
|
|
509
519
|
|
|
520
|
+
# ── 등록 시점 게이트 ──────────────────────────────────────────────────────────
|
|
521
|
+
# 호출 시점(여기)에서 **등록 시점 검사기를 그대로 부른다.** 선언 검사를 여기 새로 쓰지
|
|
522
|
+
# 않는 것이 요점이다 — 같은 스키마에 계기가 둘이 되면 관대함이 갈리고, 이 파일은 그
|
|
523
|
+
# divergent-normalizer 함정을 **이미 두 번 밟았다**(§META_KEYS 헤더: 2026-08-11 · 08-16,
|
|
524
|
+
# 둘 다 «등록되는데 부를 수 없는» 선언을 만들었다). 스키마의 단일 소스는 검사기 쪽이다.
|
|
525
|
+
#
|
|
526
|
+
# 🟥 **`--declaration-only` 로 부르는 이유 — 그리고 그 대가를 명시한다.**
|
|
527
|
+
# 전량 호출하면 M4(known-pair arm 실행)와 M6(effect probe 가 entry 를 실행)가 **매 relay
|
|
528
|
+
# 호출마다 재실행된다.** 검사기 자신이 «등록 거부될 capfile 의 진입점을 굳이 실행하는 것은
|
|
529
|
+
# 그 자체가 위험 노출» 이라는 가드를 갖고 있고, 그 판단은 호출 경로에서도 같다.
|
|
530
|
+
# ⇒ **대가**: 이 게이트는 «선언이 형식에 맞나» 를 보지 «선언이 사실인가» 는 **안 본다.**
|
|
531
|
+
# `writes: read-only` 의 진위는 여기서 닫히지 않는다(그건 등록 시점 M6 의 몫이다).
|
|
532
|
+
# 이 파일 헤더가 원래 적어둔 잔여 — «선언이 진실인지는 검사하지 않는다» — 는
|
|
533
|
+
# **좁아졌을 뿐 사라지지 않았다.** 좁아진 폭이 정확히 M1·M2·M3·M5 + 추가조항이다.
|
|
534
|
+
#
|
|
535
|
+
# **degrade 방향 = fail-closed.** 검사기 부재·실행 실패·알 수 없는 rc 는 전부 차단이다.
|
|
536
|
+
# 이건 조합을 여는 게이트이고, 게이트의 도구가 없을 때 통과시키는 것은 게이트가 아니다
|
|
537
|
+
# (CLAUDE.md §Surface-Class Degrade Invariant). 과차단 위험은 인정하되, 여기서 열면
|
|
538
|
+
# 배선의 목적 자체가 사라진다 — 안 부르던 상태와 같아지기 때문이다.
|
|
539
|
+
_registry_gate() {
|
|
540
|
+
# 심볼릭 링크 해석. `${0%/*}` 는 **호출에 쓰인 이름**의 디렉터리를 주므로, 이 파일로의
|
|
541
|
+
# 심링크가 있으면 링크 옆(아무것도 없는 곳)을 뒤지고 «검사기 부재» 로 fail-closed 한다.
|
|
542
|
+
# 자기 경로 버그를 남의 파일 탓으로 돌리는 게이트는 그냥 실패하는 것보다 나쁘다 —
|
|
543
|
+
# 읽는 사람을 틀린 수리로 보낸다(검사기 M6 이 2026-08-16 에 실제로 밟은 형태).
|
|
544
|
+
local _src="${BASH_SOURCE[0]}" _dir
|
|
545
|
+
while [ -L "$_src" ]; do
|
|
546
|
+
_dir=$(cd -P "$(dirname "$_src")" && pwd)
|
|
547
|
+
_src=$(readlink "$_src")
|
|
548
|
+
case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
|
|
549
|
+
done
|
|
550
|
+
_dir=$(cd -P "$(dirname "$_src")" && pwd)
|
|
551
|
+
local checker="$_dir/capability_registry_check.sh"
|
|
552
|
+
|
|
553
|
+
if [ ! -r "$checker" ]; then
|
|
554
|
+
_violation "등록 검사기에 도달할 수 없다: $checker — fail-closed(조합을 열지 않는다)"
|
|
555
|
+
return 1
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
local f out rc bad=0
|
|
559
|
+
GATE_SHA=() # 게이트가 «본» 바이트의 해시. 실행 직전에 대조한다(TOCTOU).
|
|
560
|
+
for f in "${CAPS[@]}"; do
|
|
561
|
+
# 🟥 rc 는 **직접** 취한다. 파이프로 읽으면 필터의 rc 가 잡혀 REJECTED 가 0 으로 읽힌다
|
|
562
|
+
# — 이 레포가 이름 붙인 결함이고 D6(pipefail) 과 같은 뿌리다.
|
|
563
|
+
out=$(bash "$checker" --declaration-only "$f" 2>&1); rc=$?
|
|
564
|
+
case "$rc" in
|
|
565
|
+
# 🟥 **성공 출력을 버리지 않는다** (cross-family A급 수리). 초판은 `0) ;;` 로 통째로
|
|
566
|
+
# 폐기했고, 그 결과 `relay run` 출력에는 `FH_STATUS: SUCCESS` 만 남아 **호출자가
|
|
567
|
+
# «전체 검사가 통과했다»로 읽었다.** 실제로는 M4(known-pair 실행)·M6(선언 진위)를
|
|
568
|
+
# 안 돌린 선언층 통과다. 침묵은 «다 봤다»로 읽힌다 — 안 본 축을 이름으로 찍는다.
|
|
569
|
+
0) printf 'FH_REGISTRY_GATE: DECLARATION_VALID %s\n' "$(basename "$f")"
|
|
570
|
+
printf 'FH_REGISTRY_GATE_SKIPPED: M4 M6 (%s — 선언 진위는 등록 시점 검사의 몫)\n' "$(basename "$f")"
|
|
571
|
+
# 🟥 **검사한 바이트를 실행할 바이트에 결박한다** (cross-family A급 — TOCTOU).
|
|
572
|
+
# 게이트는 capfile «경로» 를 검사하고, 그 뒤 `_merge_caps`·`_invoke_node` 가 같은
|
|
573
|
+
# 경로를 **다시 읽는다.** 그 사이 파일이 바뀌면 «검사된 선언» 과 «실행된 선언» 이
|
|
574
|
+
# 다르고, 초판엔 둘을 묶는 것이 없었다. 여기서 해시를 남기고 실행 직전에 대조한다.
|
|
575
|
+
GATE_SHA+=("$(_sha < "$f")") ;;
|
|
576
|
+
1) _violation "등록 시점 검사 REJECTED — 부를 수 없는 선언이다: $f"
|
|
577
|
+
printf '%s\n' "$out" | sed -n 's/^ ❌ / ↳ /p' >&2
|
|
578
|
+
bad=1 ;;
|
|
579
|
+
*) _violation "등록 검사기가 비정상 종료(rc=$rc): $f — fail-closed"
|
|
580
|
+
printf '%s\n' "$out" | tail -3 >&2
|
|
581
|
+
bad=1 ;;
|
|
582
|
+
esac
|
|
583
|
+
done
|
|
584
|
+
[ "$bad" -eq 0 ]
|
|
585
|
+
}
|
|
586
|
+
|
|
510
587
|
# ── run ───────────────────────────────────────────────────────────────────────
|
|
511
588
|
do_run() {
|
|
512
589
|
local task="" recdir="" ack=0 n=0 upstream="" rc final=$RC_PASS _ci
|
|
@@ -572,6 +649,41 @@ do_run() {
|
|
|
572
649
|
local ncap=${#CAPS[@]}
|
|
573
650
|
[ "$ncap" -ge 2 ] || _die "relay 는 노드 2개 이상을 요구한다(받은 수: $ncap) — 1노드는 단순 호출이지 경유가 아니다"
|
|
574
651
|
|
|
652
|
+
# ── 등록 시점 게이트 배선 (2026-08-17) ──────────────────────────────────────
|
|
653
|
+
# 🟥 **왜 여기가 필요했나 — 이 파일이 게이트를 갖고도 안 불렀다.**
|
|
654
|
+
# 2026-08-17 실측: `capability_registry_check.sh qasp_clean.cap` → `❌ REJECTED`
|
|
655
|
+
# (M2+ enum 에 「안 돌았다」 없음 · M5 requires_cwd 미선언 · M4 캘리브레이션 쌍 미선언).
|
|
656
|
+
# **그런데 relay 는 그 cap 을 그냥 실행하고 CLEAN 을 체인에 병합했다** — grep 결과
|
|
657
|
+
# `capability_registry_check` 히트 3건이 **전부 주석**이고 실행문은 0줄이었다.
|
|
658
|
+
# 게이트는 42KB 로 실재하고 정상 작동하는데 **아무도 부르지 않는** 상태였다
|
|
659
|
+
# ([[feedback_built_but_not_wired]] — 「호출부 미배선이면 산문이 실행기」).
|
|
660
|
+
# 그 결과가 §2-b 의 «relay 3노드 CLEAN 동의»이고, 그 동의는 허상이었다.
|
|
661
|
+
# 🟥 **우회 채널이 있고, 왜 있는지 적는다 — 이걸 숨기면 그게 더 나쁘다.**
|
|
662
|
+
# 이 게이트를 켜자 relay 자신의 **런타임 심층 방어 4종**이 do_run 경로에서 도달 불가가 됐다
|
|
663
|
+
# (실측: L26b «126 이상» · L31b «중복 선언» · L36b 실행 불가 · N8 requires_cwd 부재).
|
|
664
|
+
# 구조적으로 배타적이다 — entry 가 실행 불가여야 126 이 나는데 그건 M1 이 먼저 잡고,
|
|
665
|
+
# requires_cwd 가 없어야 NOT_CALLABLE 인데 그건 M5 가 먼저 잡는다.
|
|
666
|
+
# 그 방어들은 **죽은 코드가 아니라 심층 방어**다: 게이트를 안 거치는 진입점, 게이트가
|
|
667
|
+
# fail-closed 로 죽은 뒤의 경로에서 여전히 마지막 벽이다. 시험할 수 없으면 썩는다
|
|
668
|
+
# ([[feedback_built_but_not_wired]] 의 거울상 — 배선했더니 아래층이 안 보이게 된 형태).
|
|
669
|
+
# ⇒ 끄는 길을 **하나만**, **시끄럽게** 둔다. 조용한 우회가 실사용으로 새는 것이지
|
|
670
|
+
# 존재 자체가 새는 게 아니다. 이 줄이 실사용 로그에 보이면 그게 결함 신호다.
|
|
671
|
+
if [ "${FH_RELAY_REGISTRY_GATE:-on}" = "off" ]; then
|
|
672
|
+
# 🟥 **우회는 실행을 막지 않지만 clearing PASS 는 구조적으로 막는다** (cross-family A급 수리).
|
|
673
|
+
# 초판은 경고만 stderr 로 찍었는데, 재현 결과 **REJECT capfile 이 실제로 실행되고
|
|
674
|
+
# `FH_RELAY_VERDICT: PASS` 까지 갔다.** 경고는 텍스트일 뿐 게이트가 아니다.
|
|
675
|
+
# ⇒ `DEGRADED_SEEN` 과 같은 형태로 **verdict 를 강등**한다. 심층 방어 레인들은 rc=2/4 를
|
|
676
|
+
# 기대하므로 안 깨지고, 실사용자가 이 스위치를 켜도 **게이트를 열지는 못한다.**
|
|
677
|
+
# («이중 조건 env» 대신 이걸 고른 이유: 조건을 늘려도 둘 다 켜면 그만이고, 그건 여전히
|
|
678
|
+
# PASS 를 낸다. 여기서 막아야 하는 것은 진입이 아니라 **통과**다.)
|
|
679
|
+
GATE_BYPASSED=1
|
|
680
|
+
printf '⚠️ 등록 게이트 우회 중 (FH_RELAY_REGISTRY_GATE=off) — 심층 방어 시험용 경로다.\n' >&2
|
|
681
|
+
printf '⚠️ 실사용에서 이 줄이 보이면 결함이다: 부를 수 없는 선언이 조합에 들어온다.\n' >&2
|
|
682
|
+
printf '⚠️ 이 런은 clearing PASS 를 낼 수 없다(강등 고정).\n' >&2
|
|
683
|
+
else
|
|
684
|
+
_registry_gate || { printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: COMPOSITION_VIOLATION\n'; return "$RC_VIOLATION"; }
|
|
685
|
+
fi
|
|
686
|
+
|
|
575
687
|
_merge_caps || { printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: COMPOSITION_VIOLATION\n'; return "$RC_VIOLATION"; }
|
|
576
688
|
_run_checks || { printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: COMPOSITION_VIOLATION\n'; return "$RC_VIOLATION"; }
|
|
577
689
|
|
|
@@ -600,6 +712,21 @@ do_run() {
|
|
|
600
712
|
: > "$recdir/CHAIN.txt"
|
|
601
713
|
printf 'task_sha: %s\n' "$(printf '%s' "$task" | _sha)" >> "$recdir/CHAIN.txt"
|
|
602
714
|
|
|
715
|
+
# 🟥 **TOCTOU 재확인** — 게이트가 검사한 바이트와 지금 실행할 바이트가 같은지 본다.
|
|
716
|
+
# 게이트를 우회한 런(GATE_SHA 비어 있음)은 대조할 기준이 없으므로 건너뛴다 — 그 런은
|
|
717
|
+
# 이미 clearing PASS 를 못 내도록 강등돼 있다.
|
|
718
|
+
if [ "${#GATE_SHA[@]}" -gt 0 ]; then
|
|
719
|
+
local _i=0 _now
|
|
720
|
+
for f in "${CAPS[@]}"; do
|
|
721
|
+
_now=$(_sha < "$f")
|
|
722
|
+
if [ "$_now" != "${GATE_SHA[$_i]}" ]; then
|
|
723
|
+
printf 'FH_STATUS: ERROR\nFH_RELAY_VERDICT: HARNESS_ERROR\n'
|
|
724
|
+
_die "capfile 이 검사 후 변경됐다: $f — 검사한 선언과 실행할 선언이 다르다(TOCTOU)"
|
|
725
|
+
fi
|
|
726
|
+
_i=$((_i+1))
|
|
727
|
+
done
|
|
728
|
+
fi
|
|
729
|
+
|
|
603
730
|
for f in "${CAPS[@]}"; do
|
|
604
731
|
n=$((n+1))
|
|
605
732
|
_invoke_node "$f" "$task" "$upstream" "$recdir" "$n" "${CAPARGS[$((n-1))]:-}"
|
|
@@ -679,7 +806,14 @@ do_run() {
|
|
|
679
806
|
# D2 — 체인에 judge:model 이 있으면 PASS 는 **단독으로 게이트를 열 수 없다.**
|
|
680
807
|
# D4 연장 — advisory 로 넘긴 HARNESS_ERROR 도 clearing PASS 를 영구히 막는다.
|
|
681
808
|
# "안 재고 통과" 를 초록으로 렌더하지 않는다(미측정≠PASS).
|
|
682
|
-
if [ "${
|
|
809
|
+
if [ "${GATE_BYPASSED:-0}" -eq 1 ]; then
|
|
810
|
+
# 🟥 등록 게이트를 우회한 런은 **어떤 경우에도 clearing PASS 가 아니다.**
|
|
811
|
+
# 선언이 부를 수 있는 것인지 확인하지 않았으므로 «안 재고 통과» 이고, 이 파일의 D4 가
|
|
812
|
+
# 이미 그것을 PASS 로 렌더하지 않기로 정했다(미측정 ≠ PASS).
|
|
813
|
+
printf 'FH_RELAY_VERDICT: PENDING\n'
|
|
814
|
+
printf 'FH_RELAY_CLEARING: NON_CLEARING (등록 게이트 우회 — 선언 유효성을 안 쟀다. 미측정은 PASS 가 아니다)\n'
|
|
815
|
+
final=$RC_PENDING
|
|
816
|
+
elif [ "${DEGRADED_SEEN:-0}" -eq 1 ]; then
|
|
683
817
|
printf 'FH_RELAY_VERDICT: PENDING\n'
|
|
684
818
|
printf 'FH_RELAY_CLEARING: NON_CLEARING (HARNESS_ERROR 를 advisory 로 통과시켰다 — 미측정은 PASS 가 아니다)\n'
|
|
685
819
|
final=$RC_PENDING
|
|
@@ -702,7 +836,18 @@ do_merge_only() {
|
|
|
702
836
|
[ "${#CAPS[@]}" -gt 0 ] || _die "usage: merge --cap <capfile> [--cap ...]"
|
|
703
837
|
_merge_caps || return "$RC_VIOLATION"
|
|
704
838
|
_run_checks || return "$RC_VIOLATION"
|
|
705
|
-
|
|
839
|
+
# 🟥 **`merge` 는 등록 게이트를 부르지 않는다 — 그리고 그 사실을 출력이 말한다**
|
|
840
|
+
# (cross-family A급 수리). 재현: 같은 capfile 이 `capability_registry_check --declaration-only`
|
|
841
|
+
# 에서 rc=1 인데 `relay merge` 에서는 rc=0 `FH_STATUS: SUCCESS` 였다. 실행은 아니지만
|
|
842
|
+
# **preflight/조합 계산 표면으로 쓰이면 fail-open** 이다.
|
|
843
|
+
# ⇒ 게이트를 강제하지 **않는** 이유: `merge` 는 «병합만 계산» 하는 조회 경로라 등록 전
|
|
844
|
+
# 선언을 실험하는 정당한 용도가 있다. 막을 곳은 진입이 아니라 **오독**이다.
|
|
845
|
+
# ⇒ 그래서 `SUCCESS` 뒤에 **무엇을 안 봤는지**를 기계 필드로 찍는다. 침묵하면 호출자가
|
|
846
|
+
# «조합이 유효하다»로 읽는다 — 이 파일이 D4 에서 이미 정한 «미측정 ≠ PASS» 와 같은 규율.
|
|
847
|
+
printf 'FH_STATUS: SUCCESS\n'
|
|
848
|
+
printf 'FH_MERGE_SCOPE: SCHEMA_ONLY (병합 계산만 — 등록 시점 검사 미실행)\n'
|
|
849
|
+
printf 'FH_REGISTRY_GATE: NOT_RUN (이 경로는 capability_registry_check 를 부르지 않는다. `run` 에서만 검사한다)\n'
|
|
850
|
+
_print_merge
|
|
706
851
|
}
|
|
707
852
|
|
|
708
853
|
case "${1:-}" in
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -527,6 +527,9 @@ for _pair in \
|
|
|
527
527
|
"scripts/knowledge_seam_check.sh|scripts/test_knowledge_seam_lanes.sh" \
|
|
528
528
|
"templates/.git-hooks/pre-commit|scripts/test_marker_crossfamily_lanes.sh" \
|
|
529
529
|
"templates/.git-hooks/pre-commit|scripts/test_marker_floor_lanes.sh" \
|
|
530
|
+
".github/workflows/regression-guard.yml|scripts/test_regression_guard_ci_lanes.sh" \
|
|
531
|
+
"templates/.git-hooks/pre-commit|scripts/test_marker_standpoint_lanes.sh" \
|
|
532
|
+
"templates/.git-hooks/pre-commit|scripts/test_marker_thirdparty_lanes.sh" \
|
|
530
533
|
"scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
|
|
531
534
|
"scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
|
|
532
535
|
"scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
|
|
@@ -700,6 +703,35 @@ else
|
|
|
700
703
|
fail=1
|
|
701
704
|
fi
|
|
702
705
|
|
|
706
|
+
# test_satellite_publish_gate_lanes — 위성 공개표면 게이트(2026-08-18, 원정 2차).
|
|
707
|
+
# 🟥 이 배선이 없어서 CI 가 `lane-runner: lane suite(s) with no runner and no declaration` 로
|
|
708
|
+
# 빨갰다. 스위트를 신설하고 아무 데서도 안 돌린 것 — 검사기의 표현대로
|
|
709
|
+
# *"A suite nothing executes is prose."* 그 검사기가 내 신설 레인을 잡았다.
|
|
710
|
+
if [ ! -f scripts/test_satellite_publish_gate_lanes.sh ]; then
|
|
711
|
+
echo "FAIL test_satellite_publish_gate_lanes.sh: missing — publish gate has no anchor"
|
|
712
|
+
fail=1
|
|
713
|
+
elif _out=$(bash scripts/test_satellite_publish_gate_lanes.sh < /dev/null 2>&1); then
|
|
714
|
+
echo "PASS test_satellite_publish_gate_lanes.sh ($(printf '%s\n' "$_out" | grep -oE '[0-9]+ passed, [0-9]+ failed' | tail -1))"
|
|
715
|
+
else
|
|
716
|
+
echo "FAIL test_satellite_publish_gate_lanes.sh: publish gate lanes failed"
|
|
717
|
+
_show_failure "$_out"
|
|
718
|
+
fail=1
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
# test_evidence_root_psa_lanes — 워크트리에서 기밀성 오버라이드를 찾는가 (R4, 2026-08-18).
|
|
722
|
+
# 🟥 `EVIDENCE_ROOT` 에는 레인이 **하나도 없었다** — 그래서 `tracks/` 만 옮기고 gitignored
|
|
723
|
+
# 패턴 파일은 안 옮긴 반쪽-픽스가 그대로 출하됐다. 되돌림 프로브로 E1 만 적색 확인.
|
|
724
|
+
if [ ! -f scripts/test_evidence_root_psa_lanes.sh ]; then
|
|
725
|
+
echo "FAIL test_evidence_root_psa_lanes.sh: missing — EVIDENCE_ROOT 기밀성 경로에 앵커 없음"
|
|
726
|
+
fail=1
|
|
727
|
+
elif _out=$(bash scripts/test_evidence_root_psa_lanes.sh < /dev/null 2>&1); then
|
|
728
|
+
echo "PASS test_evidence_root_psa_lanes.sh ($(printf '%s\n' "$_out" | grep -oE '[0-9]+ passed, [0-9]+ failed' | tail -1))"
|
|
729
|
+
else
|
|
730
|
+
echo "FAIL test_evidence_root_psa_lanes.sh: evidence-root psa lanes failed"
|
|
731
|
+
_show_failure "$_out"
|
|
732
|
+
fail=1
|
|
733
|
+
fi
|
|
734
|
+
|
|
703
735
|
# cluster_capability_scan — wired 2026-08-16 (정체성 ①-(a) cluster-wizard).
|
|
704
736
|
# 종단 판정줄이 `… 캘리브레이션 통과/실패: N PASS / M FAIL` 이라 위 `_subj` 루프의
|
|
705
737
|
# 캘리브레이션 게이트를 **진짜 판정줄로** 만족한다 — 테스트 케이스 제목이 아니다.
|
|
@@ -958,6 +990,21 @@ else
|
|
|
958
990
|
fail=1
|
|
959
991
|
fi
|
|
960
992
|
|
|
993
|
+
# target_freeze — 같은 형태(shipped subject + 자기 앵커). 🟥 이 블록이 없어서 `lane-runner`
|
|
994
|
+
# 검사가 «돌리는 게 없는 스위트는 산문이다» 로 정확히 걸었다. 지은 사람이 자기 레인을 안
|
|
995
|
+
# 배선한 것이고, 오늘 이 세션이 남의 코드에서 네 번 잡은 그 얼굴이다.
|
|
996
|
+
if [ ! -f scripts/target_freeze.sh ]; then
|
|
997
|
+
echo "FAIL scripts/target_freeze.sh is in package.json files[] but absent — a deleted subject, not a skip"
|
|
998
|
+
fail=1
|
|
999
|
+
elif [ -f scripts/test_target_freeze_lanes.sh ]; then
|
|
1000
|
+
if ! bash scripts/test_target_freeze_lanes.sh; then
|
|
1001
|
+
fail=1
|
|
1002
|
+
fi
|
|
1003
|
+
else
|
|
1004
|
+
echo "FAIL test_target_freeze_lanes.sh: target_freeze.sh present but its anchor is missing"
|
|
1005
|
+
fail=1
|
|
1006
|
+
fi
|
|
1007
|
+
|
|
961
1008
|
# And the ablation calibrator, for the third instance of the same reason. Its whole job is telling
|
|
962
1009
|
# apart states that look identical from outside — "the arm could not answer" vs "the runner is dead"
|
|
963
1010
|
# vs "the runner read the answer off disk" — and round 2 of its own adversarial review found that
|