@chrono-meta/fh-gate 2.6.0 → 2.7.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 +26 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +24 -3
- package/CLAUDE.md +39 -12
- package/README.ja.md +32 -8
- package/README.ko.md +32 -8
- package/README.md +112 -15
- package/README.zh.md +28 -8
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +19 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +90 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/package.json +13 -2
- 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 +111 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -0
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
- package/scripts/adapters/peer_resolve.sh +58 -6
- package/scripts/cluster_capability_scan.sh +42 -13
- package/scripts/digest_landing_check.sh +142 -2
- package/scripts/fh_hub_identity.sh +83 -0
- package/scripts/fh_session_load.sh +53 -5
- package/scripts/fh_track_resolve.sh +114 -0
- package/scripts/field_canon_preload.sh +50 -5
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/prior_art_prompt.sh +168 -0
- package/scripts/psa_scan_lib.sh +201 -15
- package/scripts/residency_admission_check.sh +204 -0
- package/scripts/selfcheck.sh +88 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -0
- package/scripts/test_marker_defense_lanes.sh +152 -0
- package/scripts/test_marker_soul_check_lanes.sh +211 -0
- package/scripts/test_prior_art_prompt_lanes.sh +128 -0
- package/scripts/test_psa_singlefile_lanes.sh +351 -1
- package/scripts/test_residency_admission_lanes.sh +60 -0
- package/scripts/test_track_resolve_lanes.sh +158 -0
- package/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- package/templates/settings.PriorArt.snippet.json +15 -0
|
@@ -100,6 +100,9 @@ CHECKER="$SELF_DIR/utterance_landing_check.sh"
|
|
|
100
100
|
# 다중 경로 지원과의 트레이드오프라 오늘은 **문서화만** 했고 기계 검사는 없다.
|
|
101
101
|
DLC_TRACKED_PATHS="${DLC_TRACKED_PATHS:-knowledge CLAUDE.md}" # git 추적축 (커밋시각)
|
|
102
102
|
DLC_IGNORED_DIR="${DLC_IGNORED_DIR-tracks/_meta}" # gitignored 축 (mtime). 빈 값 = 그 축 없음
|
|
103
|
+
# digest 의 «입력» 파일들(레포 상대경로, 공백 구분). 이 파일들은 digest 생성 시 프롬프트에
|
|
104
|
+
# 주입됐으므로 어휘 겹침이 보장된다 ⇒ 착지를 가를 수 없다. 아래 자기참조 차단 2 참조.
|
|
105
|
+
DLC_EXCLUDE_TARGETS="${DLC_EXCLUDE_TARGETS:-}" # 미설정 = 제외 없음 (종전 동작)
|
|
103
106
|
# 🟥 2026-08-19 — 이 정규식은 **FH 자기 어휘**였고 오버라이드가 없었다. 위성 산출의 절 제목은
|
|
104
107
|
# 대상 프로필이 정한다(`## <대상> Application Candidates`) — 즉 계기가 **대상 레포에서는 구조적으로
|
|
105
108
|
# 후보를 못 찾는다.** 실측: forge-wiki·the-bible 둘 다 rc=10. R1(디렉터리를 타깃 파일로 넘김)과
|
|
@@ -323,19 +326,86 @@ do_check() {
|
|
|
323
326
|
# ★ 자기참조 차단 — **이 계기의 첫 실물 실행에서 100% 오탐을 낸 원인**이다.
|
|
324
327
|
# digest 자신(과 그 로그)이 타깃에 있으면 모든 후보 토큰이 거기서 발견되므로
|
|
325
328
|
# **전건이 자동으로 "착지"** 가 된다. 손검증 전까지 3/3 초록이었고 실제 착지는 0이었다.
|
|
329
|
+
# ★ 자기참조 차단 2 — **digest 의 «입력»도 타깃이 될 수 없다** (2026-08-20, 실측 발견).
|
|
330
|
+
# 위 필터는 digest 자신·그 로그·타 digest 를 뺀다. 그런데 위성 런은 `FD_PROFILE`(대상 하네스가
|
|
331
|
+
# 자기를 설명하는 파일)을 **프롬프트에 주입해서** digest 를 만든다 ⇒ 그 프로필과 digest 는
|
|
332
|
+
# 어휘를 공유하는 것이 **보장돼 있다.** 프로필이 타깃 집합에 들어오면 후보 토큰이 거기서
|
|
333
|
+
# 발견되고, 그건 «채택» 이 아니라 **«내가 준 것을 내가 되읽은 것»** 이다.
|
|
334
|
+
#
|
|
335
|
+
# 실측 2026-08-20 (forge-wiki, 무인 런) — 이 필터가 없어서 판정이 뒤집혔다:
|
|
336
|
+
# digest mtime 이후 커밋된 .md = `wiki/.satellite-profile.md` **한 건**(= 그날의 타깃 전부)
|
|
337
|
+
# ⇒ 5 후보 중 4 «착지» · 1 «미착지» → rc=1 SOME-UNLANDED 로 렌더됐다.
|
|
338
|
+
# 손검증: 유일한 «미착지» 후보를 열었더니 레포 전체 0히트이고, `git log --since=<digest>` 도
|
|
339
|
+
# 공집합이었다. **실제 착지는 0이다.** 즉 계기는 0 을 4 로 보고했다.
|
|
340
|
+
# 올바른 값은 rc=10(타깃 0건 = 못 쟀다)이고, 이 필터가 그 값을 되돌려준다.
|
|
341
|
+
#
|
|
342
|
+
# ⚠️ 명명된 잔여 — **프로필에 진짜로 착지한 건은 이제 구조적으로 안 보인다.**
|
|
343
|
+
# (예: digest 가 "프로필에 egress sink 를 선언해라" 라고 했고 운영자가 그렇게 고친 경우.)
|
|
344
|
+
# 위 `*frontier_digest_*` 제외가 「후보가 내일 digest 에 재등장하는 것」을 못 보는 것과 **같은
|
|
345
|
+
# 급의 트레이드오프**이고, 같은 이유로 받는다: 보장된 겹침을 착지로 렌더하는 쪽이 **거짓
|
|
346
|
+
# 양성**이라 더 나쁘다. 미측정은 0 으로 렌더되지 않고 rc=10 으로 나간다.
|
|
347
|
+
#
|
|
348
|
+
# 🟥 기본값 무변경: `DLC_EXCLUDE_TARGETS` 미설정이면 아래 루프는 한 건도 안 뺀다 —
|
|
349
|
+
# FH 자체 런(프로필 없음)은 **바이트 동일**하게 돈다.
|
|
326
350
|
local _dbase; _dbase="$(basename "$d")"
|
|
327
351
|
local -a _filtered=()
|
|
328
|
-
local _t
|
|
352
|
+
local _t _x
|
|
353
|
+
local _n_input_excl=0
|
|
329
354
|
for _t in ${targets[@]+"${targets[@]}"}; do
|
|
330
355
|
case "$_t" in
|
|
331
356
|
*"$_dbase") continue ;; # digest 자신
|
|
332
357
|
*/logs/*) continue ;; # 그 생성 로그
|
|
333
358
|
*frontier_digest_*) continue ;; # 다른 날짜 digest (후보가 이월되며 반복 등장한다)
|
|
334
359
|
esac
|
|
360
|
+
# digest 의 선언된 입력(프로필 등). 레포 상대경로로 받아 절대경로로 맞춘다.
|
|
361
|
+
# 🟥 두 방어는 cross-family(codex/gpt-5.6-terra, 2026-08-20) 가 **fail-open** 으로 지목해서 붙었다.
|
|
362
|
+
# ⓐ 비인용 확장이라 값이 **글로빙**된다 — `wiki/*.md` 가 CWD 기준으로 퍼져 진짜 착지를 과잉
|
|
363
|
+
# 제외할 수 있다. `set -f` 로 그 루프에서만 끈다(뒤에서 원복).
|
|
364
|
+
# ⓑ 문자열 동등비교라 `./wiki/x.md` 가 git 경로 `wiki/x.md` 와 **안 맞는다.** `FD_PROFILE` 은
|
|
365
|
+
# 러너의 `-f` 게이트를 통과하므로(그건 실재만 본다) 제외만 조용히 실패한다 = 거짓 양성 재발.
|
|
366
|
+
# ⇒ 양쪽에서 `./` 접두와 후행 `/` 를 벗겨 비교한다.
|
|
367
|
+
# ⚠️ 남은 잔여(명명): **심볼릭 링크**는 여전히 안 맞는다. `profile.md -> source.md` 이고 git 이
|
|
368
|
+
# `source.md` 를 타깃으로 주면 제외가 빗나간다. 실경로 해석은 bash 3.2 에서 이식성 있게
|
|
369
|
+
# 하기 어렵고, 이 레포의 위성 둘 다 링크를 안 쓴다(실측) ⇒ 안 닫고 이름으로 남긴다.
|
|
370
|
+
local _is_input=0
|
|
371
|
+
local _tn="${_t#./}"; _tn="${_tn%/}"
|
|
372
|
+
set -f
|
|
373
|
+
for _x in ${DLC_EXCLUDE_TARGETS:-}; do
|
|
374
|
+
[ -n "$_x" ] || continue
|
|
375
|
+
_x="${_x#./}"; _x="${_x%/}"
|
|
376
|
+
if [ "$_tn" = "$FH/$_x" ] || [ "$_tn" = "$_x" ]; then _is_input=1; break; fi
|
|
377
|
+
done
|
|
378
|
+
set +f
|
|
379
|
+
if [ "$_is_input" -eq 1 ]; then _n_input_excl=$((_n_input_excl+1)); continue; fi
|
|
335
380
|
_filtered+=("$_t")
|
|
336
381
|
done
|
|
337
382
|
targets=(${_filtered[@]+"${_filtered[@]}"})
|
|
338
|
-
|
|
383
|
+
# 🟥 공백이 든 경로는 단어분리로 조각난다(codex HIGH #1). 조각은 실재 파일이 아니므로, 선언된
|
|
384
|
+
# 제외 항목 중 **레포에 실재하지 않는 것**을 크게 경고한다 — 조용한 fail-open 대신 시끄러운 오류.
|
|
385
|
+
# (이 검사는 «제외가 걸렸나» 가 아니라 «지정이 말이 되나» 를 본다. 프로필이 그날 안 바뀐 정상
|
|
386
|
+
# 케이스에서는 실재하므로 안 뜬다.)
|
|
387
|
+
set -f
|
|
388
|
+
local _bad=""
|
|
389
|
+
for _x in ${DLC_EXCLUDE_TARGETS:-}; do
|
|
390
|
+
[ -n "$_x" ] || continue
|
|
391
|
+
_x="${_x#./}"; _x="${_x%/}"
|
|
392
|
+
[ -e "$FH/$_x" ] || [ -e "$_x" ] || _bad="$_bad $_x"
|
|
393
|
+
done
|
|
394
|
+
set +f
|
|
395
|
+
[ -z "$_bad" ] || \
|
|
396
|
+
echo " ⚠️ DLC_EXCLUDE_TARGETS 에 실재하지 않는 항목:$_bad — 경로에 공백이 있으면 단어분리로 조각난다. 제외가 빗나가 입력이 타깃에 남을 수 있다(fail-open)" >&2
|
|
397
|
+
[ "$_n_input_excl" -eq 0 ] || \
|
|
398
|
+
echo " ⓘ digest 입력 ${_n_input_excl}건을 타깃에서 제외했다 (어휘 겹침이 보장돼 착지를 못 가른다): ${DLC_EXCLUDE_TARGETS}" >&2
|
|
399
|
+
# rc=10 은 둘 다 «못 쟀다» 지만 **원인이 다르고 처방도 다르다** — 하나로 뭉개면 운영자가
|
|
400
|
+
# 「아무도 안 고쳤다」와 「내가 겨냥을 잘못했다」를 구분 못 한다.
|
|
401
|
+
if [ "${#targets[@]}" -eq 0 ]; then
|
|
402
|
+
if [ "$_n_input_excl" -gt 0 ]; then
|
|
403
|
+
echo "❌ 타깃 0건 — 변경분이 digest 입력뿐이었다(제외 ${_n_input_excl}건). 착지 0 이 아니라 **미측정**이다" >&2
|
|
404
|
+
else
|
|
405
|
+
echo "❌ 타깃 0건 — digest 이후 커밋된 대상 파일이 없다. 착지 0 이 아니라 **미측정**이다" >&2
|
|
406
|
+
fi
|
|
407
|
+
return 10
|
|
408
|
+
fi
|
|
339
409
|
|
|
340
410
|
local probes; probes="$(mktemp -t dlc.XXXXXX)" || return 10
|
|
341
411
|
if ! do_extract "$d" > "$probes"; then rm -f "$probes"; return 10; fi
|
|
@@ -504,6 +574,76 @@ EOF
|
|
|
504
574
|
DLC_CONTROLS="zzz_tok" bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" \
|
|
505
575
|
"$EG/wiki/frontier_digest_2001_01_01.md" ) >/dev/null 2>&1; rc_e=$?
|
|
506
576
|
_t "★N-empty 타깃 0건은 HARNESS-ERROR(10)이지 미착지(1)가 아니다" 10 "$rc_e"
|
|
577
|
+
|
|
578
|
+
# ── ★N-input (2026-08-20) — digest 의 «입력»(프로필)은 타깃이 될 수 없다 ────────────────
|
|
579
|
+
# 🟥 이 픽스처는 **오늘 실제로 일어난 런을 그대로 재현한다**(forge-wiki 무인 런, 10:03:38).
|
|
580
|
+
# digest 이후 커밋된 유일한 .md 가 프로필 자신이었고, 프로필은 digest 를 만들 때 프롬프트에
|
|
581
|
+
# 주입되므로 어휘 겹침이 **보장**돼 있다 ⇒ 계기가 자기 입력을 읽고 「착지」라고 보고했다.
|
|
582
|
+
# 실제 착지는 0 이었는데 rc=1(SOME-UNLANDED, 4/5 착지)로 렌더됐다.
|
|
583
|
+
# 알려진 짝이다 — 두 팔이 **같은 레포·같은 digest·같은 커밋 수**이고 다른 것은 «커밋된 파일이
|
|
584
|
+
# 입력이냐 아니냐» **하나뿐**이다. 한 팔만 있으면 「제외가 다 죽인다」와 구분이 안 된다.
|
|
585
|
+
local IG; IG="$(mktemp -d -t dlc_i.XXXXXX)" || return 10
|
|
586
|
+
local rc_ia rc_ib
|
|
587
|
+
# 팔 A(known-POSITIVE, 버그의 형상): 커밋된 것이 프로필뿐 → 타깃 0건이어야 한다(10)
|
|
588
|
+
(
|
|
589
|
+
mkdir -p "$IG/wiki" && cd "$IG" && git init -q . \
|
|
590
|
+
&& git config user.email t@t && git config user.name t
|
|
591
|
+
printf '# d\n## 📌 FH Immediate Application Candidates\n\n| # | 티어 | 내용 |\n|---|---|---|\n| **A1** | M | `zzz_tok` 후보 |\n\n## end\n' \
|
|
592
|
+
> wiki/digest.md
|
|
593
|
+
touch -t 200101010000 wiki/digest.md
|
|
594
|
+
# 프로필은 digest 어휘를 그대로 갖는다 — 주입된 입력이니 당연하다
|
|
595
|
+
printf 'zzz_tok 을 다루는 위성 프로필 · zzz_ctl\n' > wiki/.satellite-profile.md
|
|
596
|
+
git add -A && git commit -qm p
|
|
597
|
+
) >/dev/null 2>&1
|
|
598
|
+
( cd "$IG" && CLAUDE_PROJECT_DIR="$IG" DLC_TRACKED_PATHS="wiki" DLC_IGNORED_DIR="" \
|
|
599
|
+
DLC_CONTROLS="zzz_ctl" DLC_EXCLUDE_TARGETS="wiki/.satellite-profile.md" \
|
|
600
|
+
bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$IG/wiki/digest.md" ) >/dev/null 2>&1; rc_ia=$?
|
|
601
|
+
_t "★N-input a) 변경분이 digest 입력(프로필)뿐이면 미측정(10)이지 착지가 아니다" 10 "$rc_ia"
|
|
602
|
+
# 팔 B(known-NEGATIVE, 제외가 과잉이 아님): 진짜 착지면이 함께 커밋되면 계기는 **살아야** 한다
|
|
603
|
+
(
|
|
604
|
+
cd "$IG" && printf 'zzz_tok 을 실제로 반영한 문서 · zzz_ctl\n' > wiki/adopted.md
|
|
605
|
+
git add -A && git commit -qm a
|
|
606
|
+
) >/dev/null 2>&1
|
|
607
|
+
( cd "$IG" && CLAUDE_PROJECT_DIR="$IG" DLC_TRACKED_PATHS="wiki" DLC_IGNORED_DIR="" \
|
|
608
|
+
DLC_CONTROLS="zzz_ctl" DLC_EXCLUDE_TARGETS="wiki/.satellite-profile.md" \
|
|
609
|
+
bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$IG/wiki/digest.md" ) >/dev/null 2>&1; rc_ib=$?
|
|
610
|
+
# 🟥 초판은 `not10` 만 요구했다 — cross-family 지적: 그건 «하네스가 안 죽었다» 지 «진짜 착지를
|
|
611
|
+
# 옳게 잡았다» 가 아니다. 다른 이유로 rc=1 이 나도 통과한다. 이제 **rc=0(전건 착지)** 을 요구한다.
|
|
612
|
+
_t "★N-input b) 진짜 착지면이 있으면 제외가 그걸 안 죽인다(전건 착지=0)" 0 "$rc_ib"
|
|
613
|
+
# 팔 C(컨트롤 — 제외를 «끄면» 옛 거짓 양성이 되살아난다). 이게 없으면 팔 A 가 제외 덕인지
|
|
614
|
+
# 다른 이유로 10 이 난 건지 안 갈린다.
|
|
615
|
+
local rc_ic
|
|
616
|
+
( cd "$IG" && git rm -q --cached wiki/adopted.md >/dev/null 2>&1; rm -f wiki/adopted.md; git commit -qm r ) >/dev/null 2>&1
|
|
617
|
+
( cd "$IG" && CLAUDE_PROJECT_DIR="$IG" DLC_TRACKED_PATHS="wiki" DLC_IGNORED_DIR="" \
|
|
618
|
+
DLC_CONTROLS="zzz_ctl" \
|
|
619
|
+
bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$IG/wiki/digest.md" ) >/dev/null 2>&1; rc_ic=$?
|
|
620
|
+
_t "★N-input c) CONTROL — 제외를 끄면 프로필이 타깃이 되어 10 이 아니게 된다(버그 재현)" "not10" "$([ "$rc_ic" -eq 10 ] && echo is10 || echo not10)"
|
|
621
|
+
# ── 아래 둘은 cross-family(codex, 2026-08-20) 가 낸 **fail-open 2건의 회귀 앵커**다.
|
|
622
|
+
# 둘 다 「제외가 조용히 빗나가 입력이 타깃에 남는다」= 옛 거짓 양성 재발이 증상이다.
|
|
623
|
+
local rc_id rc_ie
|
|
624
|
+
# d) `./` 접두로 지정해도 제외돼야 한다. FD_PROFILE 은 러너의 `-f` 게이트를 통과하므로
|
|
625
|
+
# (그건 실재만 본다) 이게 안 맞으면 **아무 신호 없이** 버그가 돌아온다.
|
|
626
|
+
( cd "$IG" && CLAUDE_PROJECT_DIR="$IG" DLC_TRACKED_PATHS="wiki" DLC_IGNORED_DIR="" \
|
|
627
|
+
DLC_CONTROLS="zzz_ctl" DLC_EXCLUDE_TARGETS="./wiki/.satellite-profile.md" \
|
|
628
|
+
bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$IG/wiki/digest.md" ) >/dev/null 2>&1; rc_id=$?
|
|
629
|
+
_t "★N-input d) './' 접두 지정도 제외된다(경로 정규화, fail-open 앵커)" 10 "$rc_id"
|
|
630
|
+
# e) 글롭이 확장되면 안 된다. 확장되면 CWD 의 다른 .md 까지 제외돼 **진짜 착지가 사라진다**.
|
|
631
|
+
# 여기선 실착지면(adopted.md)을 되살려 놓고, 글롭 지정이 그걸 안 먹는지 본다.
|
|
632
|
+
( cd "$IG" && printf 'zzz_tok 을 실제로 반영한 문서 · zzz_ctl\n' > wiki/adopted.md \
|
|
633
|
+
&& git add -A && git commit -qm a2 ) >/dev/null 2>&1
|
|
634
|
+
# 🟥 **CWD 가 레포 루트여야 이 레인이 산다.** 초판은 `$IG/wiki` 에서 돌렸는데 거기서는
|
|
635
|
+
# `wiki/*.md` 가 매치할 게 없어 확장이 아예 안 일어났다 ⇒ **죽은 레인**(뮤턴트로 자기적발).
|
|
636
|
+
# 2차 시도(루트에서 `wiki/*.md`)도 죽어 있었다 — 글롭이 퍼져도 프로필은 **닷파일이라 `*` 에
|
|
637
|
+
# 안 걸려** 타깃이 안 비고 rc 가 양쪽 다 0 이었다. 판별되려면 **글롭이 걸릴 때만 타깃이
|
|
638
|
+
# 0 이 되어야** 한다:
|
|
639
|
+
# noglob : `wiki/a*.md` 는 리터럴이라 아무것도 안 맞음 → 프로필만 제외 → 타깃=[adopted] → 0
|
|
640
|
+
# glob : `wiki/a*.md`→`wiki/adopted.md` → 둘 다 제외 → 타깃 0건 → 10
|
|
641
|
+
# 두 번 죽은 레인을 두 번 다 뮤턴트가 잡았다. 컨트롤 없이 「추가했다」로 끝냈으면 장식이었다.
|
|
642
|
+
( cd "$IG" && CLAUDE_PROJECT_DIR="$IG" DLC_TRACKED_PATHS="wiki" DLC_IGNORED_DIR="" \
|
|
643
|
+
DLC_CONTROLS="zzz_ctl" DLC_EXCLUDE_TARGETS='wiki/a*.md wiki/.satellite-profile.md' \
|
|
644
|
+
bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$IG/wiki/digest.md" ) >/dev/null 2>&1; rc_ie=$?
|
|
645
|
+
_t "★N-input e) 글롭이 확장되지 않는다(진짜 착지면을 과잉 제외 안 함)" 0 "$rc_ie"
|
|
646
|
+
rm -rf "$IG"
|
|
507
647
|
rm -rf "$EG"
|
|
508
648
|
# ⚠️ **줄 단위로** 본다. `case *"A1"*"미착지"*` 는 마지막 요약줄("N건 중 M건 미착지")까지
|
|
509
649
|
# 삼켜 오매칭한다 — 문자열 위치로 판정하는 prose-grep 함정([[feedback_typed_verdict_channel]]).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fh_hub_identity.sh — shared hub-identity + companion-store namespace resolution.
|
|
3
|
+
#
|
|
4
|
+
# Sourced by sync-to-be.sh, sync-from-be.sh, and fh_session_load.sh — the write path, the return
|
|
5
|
+
# path, and the SessionStart reader all touch the SAME companion-store directories, and all three
|
|
6
|
+
# must agree on which namespace ("tracks-meta/" vs "tracks-meta-pmh/", etc.) belongs to THIS hub.
|
|
7
|
+
#
|
|
8
|
+
# Origin (pmh-dev#68 PR #368 review, 2026-08-14): sync-to-be.sh shipped the write-side namespace
|
|
9
|
+
# split, but sync-from-be.sh and fh_session_load.sh kept reading/writing the UNSUFFIXED paths
|
|
10
|
+
# unconditionally. A sibling hub (PMH) that successfully wrote to `tracks-meta-pmh/` would, on its
|
|
11
|
+
# very next SessionStart or `sync-from-be.sh` pull, read FH's own unsuffixed `tracks-meta/` back as
|
|
12
|
+
# if it were its own — and worse, the two machine-scoped legs (`tracks-meta/manifests/$MID.yaml`,
|
|
13
|
+
# `memory/_index/$MID.md`) are keyed by MACHINE id, not hub id, so FH and a sibling hub running on
|
|
14
|
+
# the SAME machine would round-trip each other's manifest/memory-index through the identical
|
|
15
|
+
# filename. Factoring identity resolution into one sourced file (found→extend on the existing
|
|
16
|
+
# hook_source_lib.sh pattern already used by fh_session_load.sh) means all three callers can never
|
|
17
|
+
# drift out of sync on what "this hub's namespace" means — a job three separate hand-copies of the
|
|
18
|
+
# same case-statement could not guarantee.
|
|
19
|
+
#
|
|
20
|
+
# CALLER CONTRACT: set $FH before sourcing this file, then call `fh_resolve_hub_identity`.
|
|
21
|
+
# Success (recognized hub) → sets HUB_SUFFIX / HUB_NAME / TM / TA / TCH / TR / MMD / HO, returns 0.
|
|
22
|
+
# Failure (unrecognized hub) → returns 1, leaves those variables UNSET. The caller decides how to
|
|
23
|
+
# fail (sync-to-be.sh / sync-from-be.sh both refuse with exit 10 — "not my hub" only, never
|
|
24
|
+
# reused for a real error; see either script's own comment on that code).
|
|
25
|
+
#
|
|
26
|
+
# A sibling hub's identifying text is NOT hardcoded here — this file ships in the PUBLIC
|
|
27
|
+
# forge-harness repo, and a sibling hub's name/header is an operator-private token in that context
|
|
28
|
+
# (the confidentiality gate blocked the first draft of sync-to-be.sh's fix for hardcoding one
|
|
29
|
+
# directly; `git grep` showed zero prior occurrences anywhere in this repo, i.e. a genuinely new
|
|
30
|
+
# leak, not an already-accepted pattern). Sibling identity instead comes from a LOCAL, gitignored
|
|
31
|
+
# config file (`.git/info/exclude`, same convention as the sibling's own local tracks config) — `key=value` lines,
|
|
32
|
+
# read as DATA (never sourced as shell, to avoid handing arbitrary code execution to a config file):
|
|
33
|
+
# HUB_HEADER_MATCH (a prefix of the sibling's CLAUDE.md first line), HUB_SUFFIX, HUB_NAME.
|
|
34
|
+
|
|
35
|
+
fh_resolve_hub_identity() {
|
|
36
|
+
local hub1
|
|
37
|
+
hub1="$(head -1 "$FH/CLAUDE.md" 2>/dev/null)"
|
|
38
|
+
case "$hub1" in
|
|
39
|
+
"# forge-harness — Persistent Knowledge Hub"*)
|
|
40
|
+
HUB_SUFFIX=""; HUB_NAME="forge-harness"
|
|
41
|
+
;;
|
|
42
|
+
*)
|
|
43
|
+
local idfile match
|
|
44
|
+
idfile="${FH_HUB_IDENTITY_FILE:-$FH/.fh-hub-identity.local}"
|
|
45
|
+
# Always exits 0 (return via $? of the LAST command only) — under `set -eo pipefail` in a
|
|
46
|
+
# caller, an unguarded `var="$(fn)"` assignment aborts the whole script if `fn`'s pipeline
|
|
47
|
+
# exits nonzero, and both "no local identity file" AND "file exists but key absent" (grep
|
|
48
|
+
# finds 0 matches → pipefail propagates grep's 1) must fall through to the caller's own
|
|
49
|
+
# refuse path, not crash here (self-caught in testing when this lived inline in
|
|
50
|
+
# sync-to-be.sh — a `return 0` on its own trailing line does NOT suffice, `set -e` aborts on
|
|
51
|
+
# the failing pipeline before that line is ever reached; the `|| true` on the pipeline itself
|
|
52
|
+
# is required). $2=1 additionally strips ALL whitespace (for value fields with no legitimate
|
|
53
|
+
# internal spaces — HUB_SUFFIX/HUB_NAME become directory-name fragments, so a stray trailing
|
|
54
|
+
# CR from a CRLF-authored file silently mints a second namespace, reproduced in review); the
|
|
55
|
+
# header-match text keeps internal spaces (it legitimately contains them) and only a trailing
|
|
56
|
+
# CR is stripped, matching the sibling's local-tracks-config reader's own trim (line ~338 of
|
|
57
|
+
# sync-to-be.sh) applied to the field that actually needs it.
|
|
58
|
+
_fh_hub_id_get() {
|
|
59
|
+
[ -f "$idfile" ] || return 0
|
|
60
|
+
local v
|
|
61
|
+
v="$(grep "^$1=" "$idfile" 2>/dev/null | head -1 | cut -d= -f2- || true)"
|
|
62
|
+
v="${v%$'\r'}"
|
|
63
|
+
[ "${2:-}" = "1" ] && v="$(printf '%s' "$v" | tr -d '[:space:]')"
|
|
64
|
+
printf '%s' "$v"
|
|
65
|
+
}
|
|
66
|
+
match="$(_fh_hub_id_get HUB_HEADER_MATCH)"
|
|
67
|
+
if [ -n "$match" ] && [ "${hub1#"$match"}" != "$hub1" ]; then
|
|
68
|
+
HUB_SUFFIX="$(_fh_hub_id_get HUB_SUFFIX 1)"
|
|
69
|
+
HUB_NAME="$(_fh_hub_id_get HUB_NAME 1)"
|
|
70
|
+
fi
|
|
71
|
+
[ -n "${HUB_SUFFIX:-}" ] && [ -n "${HUB_NAME:-}" ] || return 1
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
TM="tracks-meta$HUB_SUFFIX"; TA="tracks-audit$HUB_SUFFIX"; TCH="tracks-chamber$HUB_SUFFIX"
|
|
75
|
+
TR="tracks$HUB_SUFFIX"; MMD="memory$HUB_SUFFIX"
|
|
76
|
+
# NOTE: the operator-private area's directory name is deliberately NOT set here.
|
|
77
|
+
# This file ships in the PUBLIC npm package (files[]), and that name is an operator-private
|
|
78
|
+
# token in that context — the same reason the sibling hub's name is read from local config
|
|
79
|
+
# rather than hardcoded (see the header). The only consumer is sync-to-be.sh, which does NOT
|
|
80
|
+
# ship, so it defines the name itself. Measured 2026-08-21: the pre-publish confidentiality
|
|
81
|
+
# scan blocked `npm publish` on this exact literal.
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
@@ -315,15 +315,51 @@ fi
|
|
|
315
315
|
# 🟥 0 을 «없다» 로 렌더하지 않는다: 프로젝트 루트를 못 찾으면 그렇게 적고, 개수를 0 으로
|
|
316
316
|
# 적지 않는다(부재 ≠ 0).
|
|
317
317
|
_PROJ_ROOT="${FH_PROJECTS_ROOT:-$(dirname "$FH")}"
|
|
318
|
+
# ── track→repo 해석: 단일 소스 = scripts/fh_track_resolve.sh ──────────────────
|
|
319
|
+
# 🟥 강등 블록은 세 소비자(fh_session_load · field_canon_preload · cluster_capability_scan)에
|
|
320
|
+
# **문자 그대로 동일**해야 한다. 2026-08-21 적대검증 HIGH-2: 초판은 파일마다 별칭을
|
|
321
|
+
# 1종/2종/3종으로 다르게 봤고, 그건 «강등 경로가 F-1 결함(갈라진 정규화기)의 완전한
|
|
322
|
+
# 복제본» 이라는 뜻이었다. 이제 강등은 **별칭 0종 + 큰 소리**다 — 덜 유용하지만
|
|
323
|
+
# 세 파일이 같은 답을 내고, 무엇보다 **조용하지 않다**. skipped 를 passed 로 렌더하지
|
|
324
|
+
# 않는다는 이 저장소 규율 그대로다. [[feedback_not_found_is_not_zero_family]]
|
|
325
|
+
_FH_TRLIB="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/fh_track_resolve.sh"
|
|
326
|
+
# shellcheck source=scripts/fh_track_resolve.sh
|
|
327
|
+
[ -f "$_FH_TRLIB" ] && . "$_FH_TRLIB"
|
|
328
|
+
# 🟥 `type` 는 **존재**만 재고 **정합**은 못 잰다(잘린 파일·구버전·환경에서 export -f 된 동명
|
|
329
|
+
# 함수는 전부 통과한다 — 적대검증 LOW-3). 그래서 라이브러리가 API 버전을 선언하고
|
|
330
|
+
# 소비자가 그걸 확인한다. 채널을 타입으로 만드는 것이지 성실성에 기대는 게 아니다.
|
|
331
|
+
if ! type fh_resolve_track_root >/dev/null 2>&1 || [ "${FH_TRACK_RESOLVE_API:-}" != "1" ]; then
|
|
332
|
+
printf '⚠️ [track-resolve] DEGRADED — fh_track_resolve.sh 부재 또는 API 불일치. 별칭 해석 없음(밑줄→하이픈·-dev 접미 미적용). 이건 «해당 없음»이 아니라 **미해석**이다.\n'
|
|
333
|
+
fh_resolve_track_root() {
|
|
334
|
+
case "${1-}" in '' |*/*|*'|'*|*'..'* ) printf '|ARGS:bad-name'; return 3 ;; esac
|
|
335
|
+
[ -n "${2-}" ] || { printf '|ARGS:empty-root'; return 3; }
|
|
336
|
+
case "${3:-dir}" in
|
|
337
|
+
git) [ -d "$2/$1/.git" ] && { printf '%s|' "$2/$1"; return 0; } ;;
|
|
338
|
+
dir) [ -d "$2/$1" ] && { printf '%s|' "$2/$1"; return 0; } ;;
|
|
339
|
+
*) printf '|ARGS:bad-pred'; return 3 ;;
|
|
340
|
+
esac
|
|
341
|
+
printf '%s|UNRESOLVED' "$2/$1"
|
|
342
|
+
return 1
|
|
343
|
+
}
|
|
344
|
+
fi
|
|
318
345
|
if [ -d "$FH/tracks" ]; then
|
|
319
|
-
_reg=""; _mapped=0; _resolved=0
|
|
346
|
+
_reg=""; _mapped=0; _resolved=0; _ambig=0
|
|
320
347
|
for _t in "$FH"/tracks/*/; do
|
|
321
348
|
_n=$(basename "$_t")
|
|
322
349
|
case "$_n" in _*) continue ;; esac
|
|
323
350
|
[ "$_n" = "*" ] && continue
|
|
324
351
|
_mapped=$((_mapped + 1))
|
|
325
|
-
|
|
326
|
-
|
|
352
|
+
_rr=$(fh_resolve_track_root "$_n" "$_PROJ_ROOT" dir)
|
|
353
|
+
_d="${_rr%|*}"; _note="${_rr##*|}"
|
|
354
|
+
# 🟥 모호는 «못 찾음» 이 아니다 — 여럿이 맞은 것이다. 조용히 하나를 고르지 않고 따로 센다.
|
|
355
|
+
case "$_note" in
|
|
356
|
+
AMBIGUOUS:*) _ambig=$((_ambig + 1)); continue ;;
|
|
357
|
+
UNRESOLVED) continue ;;
|
|
358
|
+
# 🟥 ARGS = 전제 파손(빈 이름·경로 탈출·알 수 없는 술어)이지 «못 찾음» 이 아니다.
|
|
359
|
+
# 미해소로 접으면 그것도 미측정을 0 으로 렌더하는 형태다 — 따로 말한다.
|
|
360
|
+
ARGS:*) printf '%s' "⚠️ [track-resolve] 트랙 이름 «$_n» 이 거부됐다(${_note#ARGS:}) — 이건 «레포 없음» 이 아니라 **전제 파손**이다.
|
|
361
|
+
"; continue ;;
|
|
362
|
+
esac
|
|
327
363
|
_resolved=$((_resolved + 1))
|
|
328
364
|
# 무거운 디렉터리를 쳐낸다 — 비용 주장이 «내 머신의 내 프로젝트 집합» 위에 서 있으면
|
|
329
365
|
# 안 된다(cross-family 지적). node_modules/.venv/.git 이 depth 3 안에 있으면 선형으로 는다.
|
|
@@ -341,13 +377,25 @@ if [ -d "$FH/tracks" ]; then
|
|
|
341
377
|
# 🟥 **all-or-nothing 이면 안 된다** (cross-family 적발, 초판 결함): «전부 못 찾았을 때만»
|
|
342
378
|
# 말하면 13개 중 1개만 해소되고 12개가 없을 때 그 12개가 **완전히 침묵**한다 — 이 블록이
|
|
343
379
|
# 없애려는 «부재를 0으로 렌더» 를 블록 자신이 재현한다. 미해소가 하나라도 있으면 말한다.
|
|
344
|
-
_unres=$((_mapped - _resolved))
|
|
380
|
+
_unres=$((_mapped - _resolved - _ambig))
|
|
381
|
+
if [ "$_ambig" -gt 0 ]; then
|
|
382
|
+
_reg="${_reg} ⚠️ 트랙 ${_ambig}개는 «${_PROJ_ROOT}» 밑에서 **레포 여럿이 동시에 맞았다** — 고르지 않았다. 매핑을 정리해야 한다.
|
|
383
|
+
"
|
|
384
|
+
fi
|
|
345
385
|
if [ "$_unres" -gt 0 ]; then
|
|
346
386
|
_reg="${_reg} ⚠️ 매핑된 트랙 ${_mapped}개 중 ${_unres}개는 «${_PROJ_ROOT}» 밑에서 실물을 못 찾았다 — 그 ${_unres}개는 «0» 이 아니라 **미측정**이다 (경로가 다르면 FH_PROJECTS_ROOT).
|
|
347
387
|
"
|
|
348
388
|
fi
|
|
349
389
|
if [ -n "$_reg" ]; then
|
|
350
|
-
|
|
390
|
+
# 🟥 판별자는 `_resolved` 가 아니라 **▸ 행이 실제로 있는가** 다 (적대검증 MED-2).
|
|
391
|
+
# `_resolved` 는 SKILL.md 를 세기 **전에** 증가하고, `_sk == 0` 이면 ▸ 행이 안 들어간다.
|
|
392
|
+
# 그래서 해소된 트랙이 전부 SKILL.md 0개면 «있다» 배너가 **▸ 행 0개 위에** 붙는다 —
|
|
393
|
+
# 바로 아래 주석이 금지한 그 형태다. 이 수리는 별칭 해소를 늘리므로 그 도달성을 높인다.
|
|
394
|
+
case "$_reg" in
|
|
395
|
+
*"▸"*) _FH_HAS_ROWS=1 ;;
|
|
396
|
+
*) _FH_HAS_ROWS=0 ;;
|
|
397
|
+
esac
|
|
398
|
+
if [ "$_FH_HAS_ROWS" -eq 1 ]; then
|
|
351
399
|
echo "🧩 [cross-project skill bus] 매핑된 프로젝트에 SKILL.md 가 있다 — 파일 개수만 센 것이지 «지금 세션에서 호출 가능하다» 는 확인이 아니다."
|
|
352
400
|
printf "%s" "$_reg"
|
|
353
401
|
# 목록이 실제로 있을 때만 낸다. 강등(못 찾음) 케이스에 이 안내가 붙으면 «읽을 것이 있다» 는
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fh_track_resolve.sh — track 이름 → 레포 루트. **sourced 라이브러리다. 실행하지 마라.**
|
|
3
|
+
# (선례: scripts/fh_hub_identity.sh — 같은 형태로 훅/스크립트가 `.` 로 읽는다)
|
|
4
|
+
#
|
|
5
|
+
# 왜 있는가 (2026-08-21 harness-doctor 진단 F-1, 라이브 실측):
|
|
6
|
+
# `tracks/` 이름과 레포 이름은 1:1 이 아니다 — `tracks/qasp` ↔ `~/projects/qasp-dev` ·
|
|
7
|
+
# `tracks/the_bible` ↔ `~/projects/the-bible`. 그런데 이 해석을 **세 소비자가 각각 다르게**
|
|
8
|
+
# 구현하고 있었다:
|
|
9
|
+
# scripts/cluster_capability_scan.sh 닫힌 별칭 3종 + 모호거부 → qasp ✅ the_bible ✅
|
|
10
|
+
# scripts/field_canon_preload.sh `-dev` 접미만 → qasp ✅ the_bible ❌ 무음 continue
|
|
11
|
+
# scripts/fh_session_load.sh 별칭 없음 → qasp ❌ the_bible ❌
|
|
12
|
+
# 한 세션에서 동시에 관측됐다: 시작 배너가 «11개 중 2개 못 찾음» 을 내는 동안, 같은 세션의
|
|
13
|
+
# field-canon 훅은 qasp 는 풀고 the-bible 만 빠뜨린 10개를 출력했다. 정규화기가 갈라지면
|
|
14
|
+
# **한쪽만 통과하는 입력이 무음 드롭된다** ([[feedback_divergent_leniency_duplicate_normalizers]]).
|
|
15
|
+
#
|
|
16
|
+
# 🟥 **덮는 범위를 정직하게 적는다 — 「단일 소스」는 아직 부분 참이다**(적대검증 HIGH-3).
|
|
17
|
+
# 2026-08-21 호출 그래프 실측 기준 track→repo 정규화기는 최소 다섯 벌이었다:
|
|
18
|
+
# ✅ fh_session_load.sh · field_canon_preload.sh · cluster_capability_scan.sh — 이 파일로 배선됨
|
|
19
|
+
# ⏳ scripts/adapters/peer_resolve.sh — 같은 별칭 3종을 독립 구현. 배선 진행 중
|
|
20
|
+
# 🟥 scripts/fh_env_delta_scan.sh — **역방향**(레포명 → 트랙명), 별칭 0종. 이 파일이 안 덮는다
|
|
21
|
+
# (실측: `tracks/qasp-dev`·`tracks/the-bible` 부재 → 실제 매핑된 레포 2건이 «미매핑 형제»로
|
|
22
|
+
# 계상되고, 지금은 `~/.cc_sentinels/*_mapping_skipped` 가 그 나그를 덮고 있다)
|
|
23
|
+
# 이 목록이 닫히기 전에는 "모든 해석은 여기서 한다" 고 쓰지 마라.
|
|
24
|
+
#
|
|
25
|
+
# 🟥 이 파일은 «새 기계» 가 아니다. cluster_capability_scan.sh 가 이미 정답 형태를 갖고 있었고,
|
|
26
|
+
# 그걸 단일 소스로 뽑아 나머지 둘이 같이 쓰게 하는 것이다. 별칭 목록은 **닫혀 있고**,
|
|
27
|
+
# 맞은 별칭은 호출자에게 **표면화**되며, 여럿이 동시에 맞으면 **고르지 않고 모호로 낸다** —
|
|
28
|
+
# 둘 중 하나를 조용히 고르는 것이 이 파일이 막으려는 접힘 그 자체다.
|
|
29
|
+
#
|
|
30
|
+
# 🟥 존재 술어는 통일하지 않는다 — 소비자마다 다른 것이 **의도**다.
|
|
31
|
+
# cluster 는 «디렉토리가 있나»(`dir`)를, field-canon 은 «git 레포인가»(`git`)를 묻는다.
|
|
32
|
+
# 갈라진 것은 별칭 목록이지 술어가 아니었으므로, 술어를 통일하면 한쪽을 조용히 느슨하게
|
|
33
|
+
# (또는 빡빡하게) 만든다. 그건 이 수리가 고치려는 결함과 같은 종류의 결함이다.
|
|
34
|
+
#
|
|
35
|
+
# ⚠️ 다만 술어가 가르는 것은 **엄격도만이 아니다 — 판정 «클래스» 도 갈린다**(적대검증 MED-5).
|
|
36
|
+
# 예: `~/projects/foo`(git 아님) + `~/projects/foo-dev`(git) 가 동시에 있으면
|
|
37
|
+
# pred=dir → 후보 2개 적중 → **AMBIGUOUS**(「매핑을 정리해라」)
|
|
38
|
+
# pred=git → 후보 1개 → **alias 해소**(정상 동작)
|
|
39
|
+
# 즉 같은 트랙에 대해 두 소비자가 사람에게 **서로 모순되는 말**을 할 수 있다. 초판 헤더는
|
|
40
|
+
# 이 가능성을 안 적었다. 이 머신에서 실제로 갈리는 트랙은 (부분 확인) **못 찾았다** —
|
|
41
|
+
# 부재 확인이지 부재 증명이 아니다. [[feedback_rule_misdescribes_its_own_machine]]
|
|
42
|
+
|
|
43
|
+
# fh_resolve_track_root <track-name> <projects-root> [predicate]
|
|
44
|
+
# predicate: dir (기본, 디렉토리 존재) | git (`.git` 이 있는 레포만)
|
|
45
|
+
# stdout: "<root>|<note>"
|
|
46
|
+
# note = "" 별칭 없이 이름 그대로 맞음
|
|
47
|
+
# | "alias:<cand>" 별칭으로 맞음 — 호출자가 표면화해야 한다
|
|
48
|
+
# | "AMBIGUOUS:<a,b,...>" 여럿이 맞음 — 고르지 않았다. 사람이 매핑을 정리해야 한다
|
|
49
|
+
# | "UNRESOLVED" 아무것도 안 맞음. 🟥 «0» 이 아니라 **미측정**이다
|
|
50
|
+
# | "ARGS:<why>" 전제 파손(빈 이름/빈 루트/경로탈출/알 수 없는 술어)
|
|
51
|
+
# 반환값 = 0 해소 · 1 UNRESOLVED · 2 AMBIGUOUS · 3 ARGS. note 와 **둘 다** 판정을 싣는다
|
|
52
|
+
# (note 만 실었더니 종료코드만 읽는 호출자에게 셋이 전부 «성공» 이었다 — cross-family 지적).
|
|
53
|
+
fh_resolve_track_root() {
|
|
54
|
+
local n="${1-}" root="${2-}" pred="${3:-dir}"
|
|
55
|
+
local c hits="" first="" nh=0 list=""
|
|
56
|
+
|
|
57
|
+
# ── 입력 검증. 🟥 2026-08-21 cross-family(codex/gpt-5.6-sol)가 **실행해서** 반증한 축들이고,
|
|
58
|
+
# 전부 fail-open 방향이었다. 자력 적발 0. 각각이 왜 위험한지 이름으로 남긴다:
|
|
59
|
+
# ⓐ n="" 이면 첫 후보가 "" 라 `[ -d "$root/" ]` = `[ -d "/" ]` 가 **참**이 된다 → 후보를
|
|
60
|
+
# 세어 놓고 first 는 비어서 UNRESOLVED 로 떨어지는데, **종료코드만 읽는 호출자에겐 통과**다.
|
|
61
|
+
# ⓑ root="" 이면 `$root/$n` = `/foo` — **의도한 루트가 아닌 파일시스템 루트**를 검사한다.
|
|
62
|
+
# ⓒ n 에 `/` 나 `..` 가 있으면 루트 **바깥으로 탈출**한 경로도 `-d` 만 참이면 해소된다.
|
|
63
|
+
# 그리고 dedup 이 `/` 를 구분자로 쓰므로 그 불변식도 n 에 `/` 가 없어야 성립한다
|
|
64
|
+
# (구분자 자체는 codex 가 길이 9 전수 + 랜덤 300만으로 공격해 **거짓 중복을 못 찾았다** —
|
|
65
|
+
# 여기서 막는 것은 구분자가 아니라 경로 탈출이다).
|
|
66
|
+
# ⓓ n 에 `|` 가 있으면 `"<root>|<note>"` 직렬화가 깨져 **손상된 결과가 정상 성공으로 승격**된다.
|
|
67
|
+
# ⓔ pred 오타(`gti`)가 `*)` 로 떨어져 조용히 `dir` 모드가 됐다 — **오타가 fail-open** 이었다.
|
|
68
|
+
case "$n" in
|
|
69
|
+
'' ) printf '|ARGS:empty-name'; return 3 ;;
|
|
70
|
+
*/*|*'|'*|.|..|*/..|../*|*/../* ) printf '|ARGS:bad-name'; return 3 ;;
|
|
71
|
+
esac
|
|
72
|
+
case "$n" in *'..'* ) printf '|ARGS:bad-name'; return 3 ;; esac
|
|
73
|
+
[ -n "$root" ] || { printf '|ARGS:empty-root'; return 3; }
|
|
74
|
+
case "$pred" in dir|git) ;; *) printf '|ARGS:bad-pred'; return 3 ;; esac
|
|
75
|
+
|
|
76
|
+
for c in "$n" "${n}-dev" "$(printf '%s' "$n" | tr '_' '-')"; do
|
|
77
|
+
case "$pred" in
|
|
78
|
+
git) [ -d "$root/$c/.git" ] || continue ;;
|
|
79
|
+
dir) [ -d "$root/$c" ] || continue ;;
|
|
80
|
+
esac
|
|
81
|
+
# 중복 후보 제거. 🟥 구분자로 **공백을 쓰지 마라** — 이름에 공백이 있으면 한 후보가 둘로
|
|
82
|
+
# 세어져 **거짓 AMBIGUOUS** 가 난다(실측: 실재하는 `a b` → `AMBIGUOUS:a,b`). `/` 는
|
|
83
|
+
# 위 입력 검증이 n 에서 배제하므로 후보 안에도 없다 = 안전한 구분자다.
|
|
84
|
+
case "$hits" in *"/$c/"*) continue ;; esac
|
|
85
|
+
hits="$hits/$c/"
|
|
86
|
+
nh=$((nh + 1))
|
|
87
|
+
if [ -n "$list" ]; then list="$list,$c"; else list="$c"; fi
|
|
88
|
+
[ -n "$first" ] || first="$c"
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
# 🟥 «빈 출력 = 깨끗함» 이 되면 안 된다. 그리고 **종료코드도 판정을 실어야 한다** —
|
|
92
|
+
# 초판은 «항상 0, 판정은 note 로» 였는데 cross-family 가 지목했다: 종료코드만 읽는 호출자에게
|
|
93
|
+
# AMBIGUOUS 와 UNRESOLVED 가 **둘 다 성공**으로 보였다. note 는 그대로 두되 rc 를 타입화한다.
|
|
94
|
+
# 0 = 해소 · 1 = UNRESOLVED · 2 = AMBIGUOUS · 3 = ARGS(전제 파손, «peer 없음» 이 아니다)
|
|
95
|
+
if [ "$nh" -gt 1 ]; then
|
|
96
|
+
printf '%s|AMBIGUOUS:%s' "$root/$n" "$list"
|
|
97
|
+
return 2
|
|
98
|
+
fi
|
|
99
|
+
if [ "$nh" -eq 0 ] || [ -z "$first" ]; then
|
|
100
|
+
printf '%s|UNRESOLVED' "$root/$n"
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
if [ "$first" != "$n" ]; then
|
|
104
|
+
printf '%s|alias:%s' "$root/$first" "$first"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
printf '%s|' "$root/$n"
|
|
108
|
+
return 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# 🟥 API 버전 선언 — 소비자의 `type` 가드는 **존재**만 재고 **정합**은 못 잰다(잘린 파일·
|
|
112
|
+
# 구버전·환경에서 `export -f` 된 동명 함수가 전부 통과한다). 소비자는 이 값을 확인하고,
|
|
113
|
+
# 안 맞으면 강등 + 큰 소리로 알린다. 계약이 바뀌면 이 숫자를 올려라.
|
|
114
|
+
FH_TRACK_RESOLVE_API=1
|
|
@@ -21,6 +21,34 @@
|
|
|
21
21
|
#
|
|
22
22
|
# 종료: 항상 0 · 보고는 stdout. (비영 종료는 stdout 이 폐기되고 stderr 도 안 전달된다 —
|
|
23
23
|
# `[[feedback_hook_nonzero_exit_is_silent]]`.)
|
|
24
|
+
# ── track→repo 해석: 단일 소스 = scripts/fh_track_resolve.sh ──────────────────
|
|
25
|
+
# 🟥 강등 블록은 세 소비자(fh_session_load · field_canon_preload · cluster_capability_scan)에
|
|
26
|
+
# **문자 그대로 동일**해야 한다. 2026-08-21 적대검증 HIGH-2: 초판은 파일마다 별칭을
|
|
27
|
+
# 1종/2종/3종으로 다르게 봤고, 그건 «강등 경로가 F-1 결함(갈라진 정규화기)의 완전한
|
|
28
|
+
# 복제본» 이라는 뜻이었다. 이제 강등은 **별칭 0종 + 큰 소리**다 — 덜 유용하지만
|
|
29
|
+
# 세 파일이 같은 답을 내고, 무엇보다 **조용하지 않다**. skipped 를 passed 로 렌더하지
|
|
30
|
+
# 않는다는 이 저장소 규율 그대로다. [[feedback_not_found_is_not_zero_family]]
|
|
31
|
+
_FH_TRLIB="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/fh_track_resolve.sh"
|
|
32
|
+
# shellcheck source=scripts/fh_track_resolve.sh
|
|
33
|
+
[ -f "$_FH_TRLIB" ] && . "$_FH_TRLIB"
|
|
34
|
+
# 🟥 `type` 는 **존재**만 재고 **정합**은 못 잰다(잘린 파일·구버전·환경에서 export -f 된 동명
|
|
35
|
+
# 함수는 전부 통과한다 — 적대검증 LOW-3). 그래서 라이브러리가 API 버전을 선언하고
|
|
36
|
+
# 소비자가 그걸 확인한다. 채널을 타입으로 만드는 것이지 성실성에 기대는 게 아니다.
|
|
37
|
+
if ! type fh_resolve_track_root >/dev/null 2>&1 || [ "${FH_TRACK_RESOLVE_API:-}" != "1" ]; then
|
|
38
|
+
printf '⚠️ [track-resolve] DEGRADED — fh_track_resolve.sh 부재 또는 API 불일치. 별칭 해석 없음(밑줄→하이픈·-dev 접미 미적용). 이건 «해당 없음»이 아니라 **미해석**이다.\n'
|
|
39
|
+
fh_resolve_track_root() {
|
|
40
|
+
case "${1-}" in '' |*/*|*'|'*|*'..'* ) printf '|ARGS:bad-name'; return 3 ;; esac
|
|
41
|
+
[ -n "${2-}" ] || { printf '|ARGS:empty-root'; return 3; }
|
|
42
|
+
case "${3:-dir}" in
|
|
43
|
+
git) [ -d "$2/$1/.git" ] && { printf '%s|' "$2/$1"; return 0; } ;;
|
|
44
|
+
dir) [ -d "$2/$1" ] && { printf '%s|' "$2/$1"; return 0; } ;;
|
|
45
|
+
*) printf '|ARGS:bad-pred'; return 3 ;;
|
|
46
|
+
esac
|
|
47
|
+
printf '%s|UNRESOLVED' "$2/$1"
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
fi
|
|
51
|
+
|
|
24
52
|
set -uo pipefail
|
|
25
53
|
|
|
26
54
|
HUB="${CLAUDE_PROJECT_DIR:-${HOME:-}/projects/forge-harness}"
|
|
@@ -94,11 +122,28 @@ for name in $mapped; do
|
|
|
94
122
|
printf '%s' "$PROMPT" | grep -qiF -- "$name" || continue # -F: 이름의 regex 문자 오탐/미탐 방지
|
|
95
123
|
[ -n "$SENT_DIR" ] && [ -e "$SENT_DIR/$name" ] && continue # 이 세션에서 이미 띄웠다
|
|
96
124
|
|
|
97
|
-
# 레포
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
# 레포 해석은 scripts/fh_track_resolve.sh 가 단일 소스다 (2026-08-21 F-1).
|
|
126
|
+
# 여기 있던 «이름 그대로 → -dev 접미» 2종은 `tracks/the_bible`(→the-bible)을 **무음으로
|
|
127
|
+
# 버렸다** — 밑줄→하이픈 별칭이 없었기 때문이다. 같은 세션에서 cluster_capability_scan.sh 는
|
|
128
|
+
# 그걸 풀고 있었으므로, 갈라진 정규화기가 «한쪽만 통과하는 입력» 을 만든 형태였다.
|
|
129
|
+
# 술어는 `git` 을 유지한다 — 이 훅은 정본 파일을 읽히므로 실제 레포여야 한다.
|
|
130
|
+
rr=$(fh_resolve_track_root "$name" "$PROJ_ROOT" git)
|
|
131
|
+
repo="${rr%|*}"; rnote="${rr##*|}"
|
|
132
|
+
case "$rnote" in
|
|
133
|
+
UNRESOLVED) continue ;;
|
|
134
|
+
ARGS:*)
|
|
135
|
+
printf '⚠️ [field-canon] 트랙 이름 «%s» 거부(%s) — «레포 없음» 이 아니라 **전제 파손**이다.\n' \
|
|
136
|
+
"$name" "${rnote#ARGS:}"
|
|
137
|
+
continue ;;
|
|
138
|
+
# 🟥 여럿이 맞으면 고르지 않는다 — 어느 레포의 정본을 실었는지 모르게 되는 게 더 나쁘다.
|
|
139
|
+
# 그러나 **고르지 않는 것과 말하지 않는 것은 다르다** (적대검증 MED-3). 라이브러리가
|
|
140
|
+
# 이 값을 «사람이 매핑을 정리해야 한다» 로 정의했는데 그 판정을 사람에게 안 전달하면
|
|
141
|
+
# 판정이 없는 것과 같다 — 이 훅의 계약은 「항상 exit 0 · stdout 보고」라 말할 자리가 있다.
|
|
142
|
+
AMBIGUOUS:*)
|
|
143
|
+
printf '⚠️ [field-canon] 트랙 «%s» 에 레포가 여럿 맞는다(%s) — 고르지 않았다. 정본을 안 실었으니 «없음»이 아니라 **미해석**이다. 매핑을 정리해라.\n' \
|
|
144
|
+
"$name" "${rnote#AMBIGUOUS:}"
|
|
145
|
+
continue ;;
|
|
146
|
+
esac
|
|
102
147
|
[ -n "$repo" ] || continue
|
|
103
148
|
|
|
104
149
|
# **실재하는 것만** 싣는다. 없는 파일을 가리키면 다음 사람이 그 지시를 못 믿게 된다.
|
|
@@ -39,6 +39,14 @@ cd "$REPO_ROOT" || exit 1
|
|
|
39
39
|
# scripts/sync_to_be_lanes.sh — forward-path lane suite for sync-to-be.sh, itself
|
|
40
40
|
# ACCEPTED_ABSENT above; added 2026-08-14, pmh-dev#69.
|
|
41
41
|
ACCEPTED_ABSENT=(
|
|
42
|
+
# 🟥 나가는 질의 위생 린트와 그 레인 — **일부러 출하하지 않는다.**
|
|
43
|
+
# 이 가드는 운영자 내부 패턴층(`.claude/rules/.public-surface-patterns`, gitignored)이 없으면
|
|
44
|
+
# fail-closed 로 막는다. 그 층은 소비자에게 안 나가므로, 출하하면 **신선한 설치는 100% 차단**된다
|
|
45
|
+
# — CLAUDE.md 가 «모든 새 install 을 막는 게이트는 엄격한 게이트가 아니라 우회 훈련기» 라 못박은
|
|
46
|
+
# 그 형태다. 소비자용으로 열려면 override 부재 시의 degrade 를 따로 설계해야 하고, 안 했다.
|
|
47
|
+
# `selfcheck.sh` 는 대상 부재 시 `_absent_subject_verdict` 로 빠지므로 패키지 모드에서 옳게 degrade 한다.
|
|
48
|
+
"scripts/outbound_query_guard.sh"
|
|
49
|
+
"scripts/test_outbound_query_lanes.sh"
|
|
42
50
|
".claude/registry/LOCAL_SKILL_REGISTRY.md"
|
|
43
51
|
# An INSTALL DESTINATION the user creates (`cp templates/local_fh_context.md
|
|
44
52
|
# .claude/rules/local_fh_context.md`), not a file FH ships. Shipping it would overwrite the
|