@chrono-meta/fh-gate 2.0.0 → 2.1.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/capabilities/adapters/gstack-content-safety.cap +110 -0
- package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
- package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
- package/.claude/capabilities/degrade-direction-scan.cap +24 -0
- package/.claude/capabilities/public-surface-scan.cap +24 -0
- package/.claude/rules/fh_4axis_gate.md +77 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +25 -3
- package/CHEATSHEET.md +17 -1
- package/CLAUDE.md +241 -5
- package/README.ja.md +55 -20
- package/README.ko.md +86 -20
- package/README.md +57 -20
- package/README.zh.md +49 -19
- package/knowledge/shared/harness-core/capability_composition_contract.md +68 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +122 -5
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +115 -3
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +83 -1
- package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +255 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +234 -11
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +128 -0
- package/knowledge/shared/rules/sister_asset_protocol.md +12 -0
- package/package.json +12 -3
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/agents/quench-challenger.md +6 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +60 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +14 -2
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +13 -0
- package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +34 -1
- package/scripts/adapters/gstack_content_safety.sh +186 -0
- package/scripts/adapters/mate_agent_boundary.sh +120 -0
- package/scripts/adapters/peer_resolve.sh +99 -0
- package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
- package/scripts/branch_claim.sh +14 -0
- package/scripts/capability_effect_probe.sh +340 -20
- package/scripts/capability_registry_check.sh +271 -20
- package/scripts/chamber_run.sh +24 -3
- package/scripts/chamber_witness.sh +25 -0
- package/scripts/cluster_capability_scan.sh +629 -0
- package/scripts/fh-gate.sh +16 -1
- package/scripts/fh_session_load.sh +70 -0
- package/scripts/package_coverage_check.sh +10 -0
- package/scripts/portability_lint.sh +257 -0
- package/scripts/prepublish_scope_note.sh +139 -0
- package/scripts/relay_channel.sh +70 -7
- package/scripts/selfcheck.sh +167 -5
- package/scripts/test_adapter_lanes.sh +296 -0
- package/scripts/test_capability_entrypoint_shipping.sh +8 -1
- package/scripts/test_fh_gate_regressions.sh +21 -9
- package/scripts/test_marker_axes_run_lanes.sh +87 -3
- package/scripts/test_marker_crossfamily_lanes.sh +31 -1
- package/scripts/test_relay_channel_lanes.sh +76 -0
- package/templates/.git-hooks/pre-commit +239 -12
- package/templates/subagent-tally-hook.json +2 -2
|
@@ -578,6 +578,82 @@ A_N10=$(cap a_n10.cap "id: demo:a" "entry: bash $T/n_key_pass.sh" "verdict_chann
|
|
|
578
578
|
rc=0; bash "$RELAY" run --cap "$A_N10" --cap "$B" --task t > "$T/on10" 2>&1 || rc=$?
|
|
579
579
|
_t "N10 both 채널의 **일치** 정상 경로가 실제로 돈다(과차단 아님)" 0 "$rc"
|
|
580
580
|
|
|
581
|
+
# ── N11–N15 노드별 호출 인자 채널 (`--cap-args`) ─────────────────────────────
|
|
582
|
+
# 🟥 이 채널이 없던 동안 **호출 시점 인자를 요구하는 능력은 relay 를 통과할 수 없었다**
|
|
583
|
+
# (실측 2026-08-16, pmh-dev:merge-noop-check → 항상 ARGS→HARNESS_ERROR→BLOCKED).
|
|
584
|
+
# fail-closed 라 방향은 옳았지만 신호가 0 이었고, 그건 «호출 가능» 을 구조적으로 못 만든다.
|
|
585
|
+
# 레인은 네 가지를 따로 단언한다 — 도달 · 결박 대상 · fail-closed · **비주입 컨트롤**.
|
|
586
|
+
mk n_echo_args.sh '#!/usr/bin/env bash
|
|
587
|
+
# 받은 위치 인자를 그대로 찍고, 첫 인자가 "GO" 일 때만 통과한다.
|
|
588
|
+
echo "ARGS=[$*]"
|
|
589
|
+
[ "${1:-}" = "GO" ] && exit 0
|
|
590
|
+
exit 1'
|
|
591
|
+
ARGSCAP() { cap "$1" "id: demo:$2" "entry: bash $T/n_echo_args.sh" "verdict_channel: exit" \
|
|
592
|
+
"verdict_enum: 0=PASS 1=FAIL" "approval: auto" "reversibility: reversible" "residency: public" \
|
|
593
|
+
"degrade: fail-closed" "tier_floor: none" "writes: read-only" "judge: mechanical" \
|
|
594
|
+
"verdict_binding: FAIL"; }
|
|
595
|
+
|
|
596
|
+
# N11 인자가 실제로 진입점에 **도달한다** — 없으면 FAIL 이어야 할 노드가 통과한다
|
|
597
|
+
A_N11=$(ARGSCAP a_n11.cap a)
|
|
598
|
+
rc=0; bash "$RELAY" run --cap "$A_N11" --cap-args "GO" --cap "$B" --task t \
|
|
599
|
+
--record "$T/rec11" > "$T/on11" 2>&1 || rc=$?
|
|
600
|
+
_t "N11 --cap-args 가 진입점에 실제로 도달한다" 0 "$rc"
|
|
601
|
+
# ★ 단언 대상은 relay 의 stdout 이 아니라 **노드 자신의 출력**이다. relay 는 노드 stdout 을
|
|
602
|
+
# 기록 디렉터리로 보내므로, relay 출력에 대고 grep 하면 늘 «없음» 이 나와 거짓 적색이 된다
|
|
603
|
+
# (초판이 그렇게 틀렸다 — 계기가 아니라 단언 위치가 틀린 경우).
|
|
604
|
+
_tout "N11b 받은 인자를 진입점이 그대로 본다" "ARGS=\[GO\]" "$T/rec11/node1.out"
|
|
605
|
+
|
|
606
|
+
# N12 🟥 **비주입 컨트롤** — 같은 capfile 에 인자만 빼면 FAIL 이어야 한다.
|
|
607
|
+
# 이게 없으면 N11 은 «인자와 무관하게 늘 통과하는 계기» 와 구분되지 않는다.
|
|
608
|
+
rc=0; bash "$RELAY" run --cap "$A_N11" --cap "$B" --task t \
|
|
609
|
+
--record "$T/rec12" > "$T/on12" 2>&1 || rc=$?
|
|
610
|
+
_t "N12 인자를 안 주면 같은 노드가 막힌다(컨트롤 — 채널이 실재한다는 증거)" 2 "$rc"
|
|
611
|
+
_tout "N12b 인자 없이 부르면 진입점이 빈 인자를 본다" "ARGS=\[\]" "$T/rec12/node1.out"
|
|
612
|
+
|
|
613
|
+
# N13 결박 대상은 **가장 최근 --cap** 이다 — 위치 인덱스로 짝지으면 조용히 다른 노드에 붙는다
|
|
614
|
+
A_N13=$(ARGSCAP a_n13.cap a)
|
|
615
|
+
rc=0; bash "$RELAY" run --cap "$B" --cap "$A_N13" --cap-args "GO" --task t > "$T/on13" 2>&1 || rc=$?
|
|
616
|
+
_t "N13 인자가 «가장 최근 --cap» 에 결박된다(첫 노드로 새지 않는다)" 0 "$rc"
|
|
617
|
+
|
|
618
|
+
# N14 `--cap` 없이 먼저 오면 fail-closed — 어디 붙일지 모르는 인자를 조용히 버리지 않는다
|
|
619
|
+
rc=0; bash "$RELAY" run --cap-args "GO" --cap "$A_N11" --cap "$B" --task t > "$T/on14" 2>&1 || rc=$?
|
|
620
|
+
_t "N14 --cap 앞의 --cap-args 는 fail-closed(무음 폐기 아님)" 10 "$rc"
|
|
621
|
+
|
|
622
|
+
# N15 셸 메타문자 거부 — 진입점이 단어분해로 실행되므로 프로브와 같은 규칙을 쓴다
|
|
623
|
+
rc=0; bash "$RELAY" run --cap "$A_N11" --cap-args 'GO; rm -rf /tmp/x' --cap "$B" --task t > "$T/on15" 2>&1 || rc=$?
|
|
624
|
+
_t "N15 --cap-args 의 셸 메타문자는 거부된다" 10 "$rc"
|
|
625
|
+
|
|
626
|
+
# ── N16–N18 cross-family 3건(2026-08-16) — 전부 손 재현 후 앵커 ────────────────
|
|
627
|
+
# 셋 다 「조용히 잘못되는」 형태다: 값이 사라지거나, 결박이 죽거나, 인자 경계가 바뀐다.
|
|
628
|
+
A_N16=$(ARGSCAP a_n16.cap a)
|
|
629
|
+
rc=0; bash "$RELAY" run --cap "$A_N16" --cap-args "FIRST" --cap-args "GO" --cap "$B" --task t > "$T/on16" 2>&1 || rc=$?
|
|
630
|
+
_t "N16 같은 --cap 에 --cap-args 두 번은 fail-closed(앞 값 무음 폐기 금지)" 10 "$rc"
|
|
631
|
+
|
|
632
|
+
# N17 결박 플래그와의 충돌 — `--cap-args --` 로 `--upstream-verdict` 를 위치인자로 밀어낼 수 있었다
|
|
633
|
+
A_N17=$(cap a_n17.cap "id: demo:a" "entry: bash $T/n_echo_args.sh" "upstream_argv: yes" \
|
|
634
|
+
"verdict_channel: exit" "verdict_enum: 0=PASS 1=FAIL" \
|
|
635
|
+
"approval: auto" "reversibility: reversible" "residency: public" "degrade: fail-closed" \
|
|
636
|
+
"tier_floor: none" "writes: read-only" "judge: mechanical" "verdict_binding: FAIL")
|
|
637
|
+
rc=0; bash "$RELAY" run --cap "$A_N17" --cap-args "GO" --cap "$B" --task t > "$T/on17" 2>&1 || rc=$?
|
|
638
|
+
_t "N17 upstream_argv 노드에 --cap-args 를 붙이면 fail-closed(결박이 조용히 죽지 않는다)" 10 "$rc"
|
|
639
|
+
# N17b 컨트롤 — upstream_argv 를 **선언하지 않은** 노드는 종전대로 통과해야 한다(과차단 방지)
|
|
640
|
+
rc=0; bash "$RELAY" run --cap "$A_N16" --cap-args "GO" --cap "$B" --task t > "$T/on17b" 2>&1 || rc=$?
|
|
641
|
+
_t "N17b 컨트롤 — upstream_argv 미선언 노드는 --cap-args 를 정상 수용" 0 "$rc"
|
|
642
|
+
|
|
643
|
+
# N18 개행 — 초판 가드를 통과해 **한 인자가 두 인자로 쪼개졌다**($'GO\nSECOND' → ARGC=2)
|
|
644
|
+
rc=0; bash "$RELAY" run --cap "$A_N16" --cap-args "$(printf 'GO\nSECOND')" --cap "$B" --task t > "$T/on18" 2>&1 || rc=$?
|
|
645
|
+
_t "N18 --cap-args 의 개행은 거부된다(인자 경계 무음 변형 차단)" 10 "$rc"
|
|
646
|
+
# N18b 컨트롤 — **정상 다중 인자**(공백 구분)는 계속 통과해야 한다. 안 그러면 N18 은
|
|
647
|
+
# 「인자를 여러 개 못 주는 계기」와 구분되지 않는다.
|
|
648
|
+
A_N18=$(cap a_n18.cap "id: demo:a" "entry: bash $T/n_echo_args.sh" "verdict_channel: exit" \
|
|
649
|
+
"verdict_enum: 0=PASS 1=FAIL" "approval: auto" "reversibility: reversible" "residency: public" \
|
|
650
|
+
"degrade: fail-closed" "tier_floor: none" "writes: read-only" "judge: mechanical" \
|
|
651
|
+
"verdict_binding: FAIL")
|
|
652
|
+
rc=0; bash "$RELAY" run --cap "$A_N18" --cap-args "GO SECOND THIRD" --cap "$B" --task t \
|
|
653
|
+
--record "$T/rec18" > "$T/on18b" 2>&1 || rc=$?
|
|
654
|
+
_t "N18b 컨트롤 — 공백 구분 다중 인자는 정상 통과" 0 "$rc"
|
|
655
|
+
_tout "N18b2 세 인자가 그대로 도달한다" "ARGS=\[GO SECOND THIRD\]" "$T/rec18/node1.out"
|
|
656
|
+
|
|
581
657
|
printf '\n── relay_channel lanes: %d PASS / %d FAIL ──\n' "$pass" "$fail"
|
|
582
658
|
[ "$fail" -eq 0 ] || exit 1
|
|
583
659
|
exit 0
|
|
@@ -284,8 +284,20 @@ else
|
|
|
284
284
|
psa_load "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" \
|
|
285
285
|
"${PSA_PATTERNS:-$REPO_ROOT/.claude/rules/.public-surface-patterns}"
|
|
286
286
|
if [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
# 2026-08-16 weekly-audit residual — operator decision: LOUDER WARNING, not fail-closed.
|
|
288
|
+
# A hard block here would fail every fresh clone's / CI's / worktree's first commit (the
|
|
289
|
+
# override is gitignored by construction), training PUBLIC_SURFACE_OK into a reflex — the same
|
|
290
|
+
# override-habituation this repo has measured and warned against repeatedly. So the degrade
|
|
291
|
+
# direction stays PASS; what changed is that the old two-line notice was easy to scroll past in
|
|
292
|
+
# a green hook run. This is now impossible to miss, still non-blocking.
|
|
293
|
+
echo ""
|
|
294
|
+
echo " 🟧🟧🟧 CONFIDENTIALITY COVERAGE REDUCED — READ BEFORE TRUSTING THIS PASS 🟧🟧🟧"
|
|
295
|
+
echo " ⚠️ operator pattern override ABSENT — only committed defaults are active."
|
|
296
|
+
echo " Company/employer-name and real-username token classes are UNSCANNED this run."
|
|
297
|
+
echo " A clean PASS below reflects the defaults-only pattern set, not the full one."
|
|
298
|
+
echo " Fix: populate .claude/rules/.public-surface-patterns (gitignored, one severity<TAB>regex per line)."
|
|
299
|
+
echo " 🟧🟧🟧 ────────────────────────────────────────────────────────────── 🟧🟧🟧"
|
|
300
|
+
echo ""
|
|
289
301
|
fi
|
|
290
302
|
if [ "$PSA_DEFAULTS_OK" -eq 0 ] || [ "$PSA_BAD_ROWS" -gt 0 ] \
|
|
291
303
|
|| [ -z "$(printf '%s' "$PSA_STREAM" | grep -vE '^[[:space:]]*(#|$)' || true)" ]; then
|
|
@@ -557,7 +569,61 @@ validate_crossfamily_leg() { # $1 = marker path
|
|
|
557
569
|
|
|
558
570
|
case "$val" in
|
|
559
571
|
declined)
|
|
560
|
-
|
|
572
|
+
# `declined` means ONE thing: the OPERATOR declined sidecars (UAP), a chosen floor. It was
|
|
573
|
+
# the only value in this enum with no grounds requirement, and that made it the enum's soft
|
|
574
|
+
# spot: an author who judged decorrelation unnecessary reached for the nearest permissive
|
|
575
|
+
# token and it passed clean. Measured 2026-08-16, in this repo, by the session that had
|
|
576
|
+
# CORRECTED a different marker's crossfamily value earlier the same day and had the correct
|
|
577
|
+
# semantics printed to it in this hook's own error text — i.e. neither ignorance of the enum
|
|
578
|
+
# nor of the file. Form was satisfied, so the question stopped.
|
|
579
|
+
#
|
|
580
|
+
# A closed enum stops free prose from collapsing distinct states; it does NOT stop a wrong
|
|
581
|
+
# token from being a valid one. This lane is the difference. The grounds bar is deliberately
|
|
582
|
+
# the same shape as the degrade branch below (substance + a token naming WHO/WHERE), because
|
|
583
|
+
# the claim being made is factual and attributable: an operator decision has a record.
|
|
584
|
+
# 🟥 THE TEST IS A RESOLVABLE RECORD, NOT VOCABULARY. The first version of this lane grepped
|
|
585
|
+
# for words (operator|uap|consent|…). A cross-family review (codex/gpt-5.6-terra) attacked it
|
|
586
|
+
# and all three findings reproduced on the spot:
|
|
587
|
+
# · SELF-VALIDATING `declined — declined because I judged it unnecessary` PASSED, because
|
|
588
|
+
# `declin` was in the keyword list and the author had echoed the value.
|
|
589
|
+
# · VACUOUS PASS `declined — operator documentation says authors may choose` PASSED
|
|
590
|
+
# while describing no declination at all.
|
|
591
|
+
# · OVER-BLOCK `declined — the person who runs this harness asked us not to use
|
|
592
|
+
# external reviewers here` was BLOCKED — a real operator declination,
|
|
593
|
+
# in natural prose, carrying none of the listed tokens.
|
|
594
|
+
# Adding patterns is the Grep-Collision Treadmill (P10): each new word relocates the evasion.
|
|
595
|
+
# So the check changed KIND. Per CLAUDE.md §Mechanization Boundary a channel check asserts a
|
|
596
|
+
# property OF THE RECORD, never a conclusion — and the honest record property here is:
|
|
597
|
+
# **the claim names a file that exists.** Vocabulary cannot be stuffed into it, a fabricated
|
|
598
|
+
# path fails, and a legitimate declination just cites where it lives.
|
|
599
|
+
# HONEST RESIDUAL, stated rather than implied: this proves a cited record EXISTS, not that it
|
|
600
|
+
# SAYS what is claimed. That is the marker's own declared scope (form + non-vacuity +
|
|
601
|
+
# auditability, NOT provenance) — identical ceiling, not a new gap.
|
|
602
|
+
_cf_cited=""
|
|
603
|
+
for _tok in $reason; do
|
|
604
|
+
_tok=${_tok%%,}; _tok=${_tok%%\)}; _tok=${_tok##\(}
|
|
605
|
+
case "$_tok" in
|
|
606
|
+
*/*|*.md|*.yaml|*.yml|*.json)
|
|
607
|
+
if [ -e "$REPO_ROOT/$_tok" ] || [ -e "$_tok" ] || [ -e "$HOME/$_tok" ]; then
|
|
608
|
+
_cf_cited="$_tok"; break
|
|
609
|
+
fi ;;
|
|
610
|
+
esac
|
|
611
|
+
done
|
|
612
|
+
if [ "${#reason}" -lt 20 ] || [ -z "$_cf_cited" ]; then
|
|
613
|
+
echo " ❌ FAIL — 'declined' must cite a record that EXISTS on disk."
|
|
614
|
+
echo " declined = the OPERATOR declined sidecars, a chosen floor. It does NOT mean"
|
|
615
|
+
echo " 'I judged decorrelation unnecessary here' — that is a CHOICE the author made"
|
|
616
|
+
echo " with a panel reachable, and its honest value is DEGRADED_PANEL_UNUSED."
|
|
617
|
+
echo " An operator decision has a record. Name its path on this same line:"
|
|
618
|
+
echo " crossfamily: declined — operator declined sidecars, tracks/_meta/user_adaptation_profile.md"
|
|
619
|
+
echo " crossfamily: declined — chosen floor per CLAUDE.local.md §sidecar"
|
|
620
|
+
echo " No such record → you are not describing an operator declination:"
|
|
621
|
+
echo " crossfamily: DEGRADED_PANEL_UNUSED — codex+agy reachable, not recruited (<why>)"
|
|
622
|
+
[ "${#reason}" -ge 20 ] && echo " (grounds were substantive but named no resolvable path)"
|
|
623
|
+
return 1
|
|
624
|
+
fi
|
|
625
|
+
echo " ✅ cross-family leg: declined (operator-chosen floor, first-class — not a degrade)"
|
|
626
|
+
echo " $(printf '%s' "$reason" | cut -c1-80)" ;;
|
|
561
627
|
panel*)
|
|
562
628
|
if ! printf '%s' "$val" | grep -qE '^panel\([^)]+\)$'; then
|
|
563
629
|
echo " ❌ FAIL — panel claim without a family list: '$val'"
|
|
@@ -665,33 +731,143 @@ validate_crossfamily_leg() { # $1 = marker path
|
|
|
665
731
|
# 브랜치가 전부 막히고, 그 과차단이 `--no-verify` 를 습관화시켜 같은 훅 안의 Destructive-Op
|
|
666
732
|
# 게이트까지 무장해제한다(오늘 실측된 트레이드오프).
|
|
667
733
|
AXES_RUN_GRACE_DATE="2026-08-10"
|
|
734
|
+
# ── 6축 확장 (2026-08-17) ────────────────────────────────────────────────────────
|
|
735
|
+
# 산문 정본은 6축(`fh_three_layer_canon.md` §1-a-2)인데 이 훅은 네 글자만 봤다. 확장하면서
|
|
736
|
+
# **키 표기를 산문과 1:1 로 정렬**한다 — 그 이유가 「예쁘게」가 아니라 아래 실측이다.
|
|
737
|
+
#
|
|
738
|
+
# 🟥 옛 배열과 새 배열은 **같은 글자가 다른 축을 가리킨다**:
|
|
739
|
+
# 문자 §1-a (옛 4축, 마커가 쓰던 것) §1-a-2 (현 6축, 산문이 쓰는 것)
|
|
740
|
+
# ⓐ 다른 계열 다른 계열 (동일)
|
|
741
|
+
# ⓑ 첫 실사용 **입장** ← 옛 ⓑ 는 현 ⓔ 로 밀렸다
|
|
742
|
+
# ⓒ 기록 그라운딩 격리 그라운딩 (대응)
|
|
743
|
+
# ⓓ 되돌림 실측 **3자 대면** ← 옛 ⓓ 는 현 ⓕ 로 밀렸다
|
|
744
|
+
# 그래서 `b=`/`d=` 를 그대로 쓰면서 의미만 바꾸면 **훅이 조용히 다른 축으로 읽으며
|
|
745
|
+
# 오류를 내지 않는다.** 무음 재해석은 이 레포가 `not found ≠ 0` 라 부르는 것의 형제다.
|
|
746
|
+
#
|
|
747
|
+
# 실측(2026-08-17): 디스크 마커 190개 중 유예일 이후 51개. 훅은 «오늘·이 브랜치» 마커
|
|
748
|
+
# **한 개만** 검증하므로(아래 호출부) 확장으로 **커밋이 막히는 기존 마커는 0건**이다.
|
|
749
|
+
# 실제 비용은 그 51건이 «옛 배열인지 새 배열인지 읽을 수 없다» 는 것 — 파손이 아니라
|
|
750
|
+
# **미측정**이다. 그래서 처방이 「고친다」가 아니라 **「어느 배열인지 말하게 만든다」**:
|
|
751
|
+
# 마커 파일명 날짜 < 2026-08-17 → 옛 4축 배열 (ASCII 키)
|
|
752
|
+
# 마커 파일명 날짜 >= 2026-08-17 → 신 6축 배열 (기호 키)
|
|
753
|
+
#
|
|
754
|
+
# 🟥 **판별자는 날짜지 표기법이 아니다 — 초판이 그 반대로 적었고 코퍼스가 반증했다.**
|
|
755
|
+
# 초판 주석: «표기법 자체가 배열을 선언하므로 감사자가 grep 한 방으로 판별한다».
|
|
756
|
+
# ⓓ3자대면 축이 디스크 마커를 손으로 세어 반례를 냈다 — axes-run 보유 53건 중
|
|
757
|
+
# 기호 키 4 · 혼용 1 이고, 기호 4건 중 **2건이 2026-08-10 자이면서 옛 4축 의미**다:
|
|
758
|
+
# …fix_digest-collection-spec-reality_2026-08-10.marker
|
|
759
|
+
# → `ⓐ 미실행 · ⓑ 첫실사용 … · ⓓ 미실행` (ⓑ·ⓓ 가 현 배열에선 ⓔ·ⓕ)
|
|
760
|
+
# …fix_entrypoint-drift-and-close-protocol_2026-08-10.marker
|
|
761
|
+
# → `ⓐ 적대검증 · ⓑ 첫실사용 … · ⓓ 되돌림`
|
|
762
|
+
# 훅은 이 셋을 절대 안 읽으므로 커밋을 막지 않는다. 틀리는 건 **감사자**다.
|
|
763
|
+
# ⇒ 표기 정렬은 «앞으로 쓸 때 안 헷갈리게» 하는 값은 있으나 **소급 판별자로 쓰면 안 된다**.
|
|
764
|
+
#
|
|
765
|
+
# ⚠️ **그리고 이 날짜 비교는 프로덕션에서 도달 불가 분기다 — 숨기지 않고 적는다.**
|
|
766
|
+
# 아래 호출부가 마커 경로를 `${TODAY}` 로 **구성**하므로 `mdate` 는 항상 오늘이다. 즉
|
|
767
|
+
# 오늘 이후 `six=0` 분기를 타는 실제 커밋은 없고, 살아있는 소비자는 픽스처뿐이다.
|
|
768
|
+
# **기존 마커를 지킨 것은 이 상수가 아니라 그 경로 구성이다.** 상수는 픽스처가 경계를
|
|
769
|
+
# 고정하는 값 + 훗날 호출부가 임의 마커를 검증하게 될 때의 대비로 남긴다.
|
|
770
|
+
# (선례: 8일 전 `crossfamily:` 를 free prose → closed enum 으로 바꿀 때는 컷오프 없이
|
|
771
|
+
# 그냥 강제했고 문제가 없었다 — 이유가 정확히 같다.)
|
|
772
|
+
# 부수 노출: 시계가 과거로 틀어진 노드는 파일명도 과거 날짜라 `six=0` 으로 새 나간다
|
|
773
|
+
# (fail-open, 저위험). 닫으려면 상수를 지우고 무조건 6축을 요구하면 된다 — 어차피
|
|
774
|
+
# 도달 불가 분기이므로 **지우는 것이 곧 닫는 것**이다. 지금은 안 지웠다(픽스처 경계값).
|
|
775
|
+
#
|
|
776
|
+
# 이식성: bash 3.2 + BSD grep, LC_ALL={C, POSIX, en_US.UTF-8, ko_KR.UTF-8} 4개 arm 에서
|
|
777
|
+
# 6/6 HIT · known-negative(ⓖ) 0건으로 실측했다. ⚠️ Linux/GNU grep 은 **미측정** — CI(ubuntu)
|
|
778
|
+
# 가 첫 측정이고, 깨지면 아래 픽스처 레인이 빨개진다(무음 아님).
|
|
779
|
+
#
|
|
780
|
+
# ⓑ입장은 값을 여기 적지 않는다 — 이미 `standpoint:` 자기 필드가 정본이라 이중 기록이 된다.
|
|
781
|
+
# 대신 `ⓑ=→standpoint` 포인터를 요구하고, 그 경우 `standpoint:` 줄의 **존재**를 확인한다.
|
|
782
|
+
# (`standpoint:` 값 자체의 enum 검증은 별건이고 정본이 명시적으로 유보해 뒀다.)
|
|
783
|
+
SIX_AXES_GRACE_DATE="2026-08-17"
|
|
668
784
|
validate_marker_axes_run() { # $1 = marker path
|
|
669
|
-
local m="$1" mdate line ax miss=""
|
|
785
|
+
local m="$1" mdate line ax miss="" nlines axkeys six=0
|
|
670
786
|
mdate=$(basename "$m" | sed -nE 's/.*_([0-9]{4}-[0-9]{2}-[0-9]{2})\.marker$/\1/p')
|
|
671
787
|
# 날짜를 못 읽으면 요구하지 않는다 — 파일명 규약 밖의 마커를 이 검사가 판정할 근거가 없다.
|
|
672
788
|
[ -n "$mdate" ] || return 0
|
|
673
789
|
[ "$mdate" \< "$AXES_RUN_GRACE_DATE" ] && return 0
|
|
790
|
+
[ "$mdate" \< "$SIX_AXES_GRACE_DATE" ] || six=1
|
|
791
|
+
|
|
792
|
+
# 다중 `axes-run:` 줄 차단. `crossfamily:` 는 예전부터 이걸 막는데 `axes-run` 은 안 막아서,
|
|
793
|
+
# 둘째 줄이 **없다고 판정되는 게 아니라 안 보인다**(아래 grep -m1). 실측: 디스크에 2줄짜리
|
|
794
|
+
# 마커 1건 존재(2026-08-15). 같은 얼굴이므로 같은 자리에서 닫는다.
|
|
795
|
+
nlines=$(grep -c -E '^[[:space:]]*axes-run:' "$m" 2>/dev/null)
|
|
796
|
+
if [ "${nlines:-0}" -gt 1 ]; then
|
|
797
|
+
echo " ❌ MARKER FORMAT — 'axes-run:' 줄이 ${nlines}개다 (하나여야 한다)"
|
|
798
|
+
echo " 이 검사는 첫 줄만 읽는다. 둘째 줄은 «없다고 판정»되는 게 아니라 **안 보인다** —"
|
|
799
|
+
echo " 즉 거기 적은 축은 검사를 통째로 우회한다. 한 줄로 합쳐라."
|
|
800
|
+
return 1
|
|
801
|
+
fi
|
|
674
802
|
|
|
675
803
|
line=$(grep -m1 -E '^[[:space:]]*axes-run:' "$m" 2>/dev/null)
|
|
676
804
|
if [ -z "$line" ]; then
|
|
677
|
-
echo " ❌ MARKER FORMAT — missing 'axes-run:' line (
|
|
805
|
+
echo " ❌ MARKER FORMAT — missing 'axes-run:' line (축 자기 대조)"
|
|
678
806
|
echo " CLAUDE.md §3층 자기 대조: **돌린 축과 안 돌린 축을 각각 이름으로** 적는다."
|
|
679
807
|
echo " 안 돈 축은 침묵이 아니라 'none' 이다 — 미측정을 0으로 렌더하지 않는다."
|
|
680
|
-
|
|
681
|
-
|
|
808
|
+
if [ "$six" -eq 1 ]; then
|
|
809
|
+
echo " axes-run: ⓐ=codex(4건) ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=CI배선 ⓕ=되돌림(국소성)"
|
|
810
|
+
echo " (ⓐ다른계열 · ⓑ입장 · ⓒ격리그라운딩 · ⓓ3자대면 · ⓔ첫실사용 · ⓕ되돌림실측)"
|
|
811
|
+
else
|
|
812
|
+
echo " axes-run: a=codex(4건) b=CI배선 c=none d=되돌림(국소성 확인)"
|
|
813
|
+
echo " (a=다른계열 · b=첫실사용 · c=기록그라운딩 · d=되돌림)"
|
|
814
|
+
fi
|
|
682
815
|
return 1
|
|
683
816
|
fi
|
|
684
|
-
|
|
817
|
+
if [ "$six" -eq 1 ]; then axkeys="ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ"; else axkeys="a b c d"; fi
|
|
818
|
+
for ax in $axkeys; do
|
|
685
819
|
echo "$line" | grep -qE "(^|[[:space:]])${ax}=[^[:space:]]" || miss="$miss $ax"
|
|
686
820
|
done
|
|
687
821
|
if [ -n "$miss" ]; then
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
822
|
+
if [ "$six" -eq 1 ]; then
|
|
823
|
+
echo " ❌ MARKER — 'axes-run:' 에 빠진 축:$miss"
|
|
824
|
+
echo " 여섯 축을 **전부** 적는다. 안 돌린 축은 'none' 으로 명시한다 — 빠뜨리는 것과"
|
|
825
|
+
echo " 안 돌렸다고 적는 것은 다른 명제이고, 빠뜨리면 읽는 쪽이 전자를 후자로 읽는다."
|
|
826
|
+
echo " axes-run: ⓐ=codex(4건) ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=CI배선 ⓕ=되돌림(국소성)"
|
|
827
|
+
echo " (ⓐ다른계열 · ⓑ입장 · ⓒ격리그라운딩 · ⓓ3자대면 · ⓔ첫실사용 · ⓕ되돌림실측)"
|
|
828
|
+
echo " 🟥 옛 ASCII 배열(a=…d=)과 **글자 의미가 다르다** — 옛 b=첫실사용은 지금 ⓔ,"
|
|
829
|
+
echo " 옛 d=되돌림은 지금 ⓕ 다. 옛 표기를 그대로 옮기면 축이 뒤바뀐다."
|
|
830
|
+
else
|
|
831
|
+
echo " ❌ MARKER — 'axes-run:' 에 빠진 축:$miss"
|
|
832
|
+
echo " 네 축을 **전부** 적는다. 안 돌린 축은 'none' 으로 명시한다 — 빠뜨리는 것과"
|
|
833
|
+
echo " 안 돌렸다고 적는 것은 다른 명제이고, 빠뜨리면 읽는 쪽이 전자를 후자로 읽는다."
|
|
834
|
+
echo " axes-run: a=codex(4건) b=CI배선 c=none d=되돌림(국소성 확인)"
|
|
835
|
+
fi
|
|
692
836
|
return 1
|
|
693
837
|
fi
|
|
694
838
|
|
|
839
|
+
if [ "$six" -eq 1 ]; then
|
|
840
|
+
# ── 혼용 가드는 **짓지 않는다**. 지었다가 실측으로 지웠다 ────────────────────
|
|
841
|
+
# 초판은 `grep -qE "(^|[[:space:]])[a-f]=[^[:space:]]"` 로 옛 ASCII 키 잔존을 차단했다.
|
|
842
|
+
# 자기 프로브에서 두 가지가 드러났다:
|
|
843
|
+
# ⓐ **오탐이 실재한다** — 값이 자유 산문이라 `ⓔ=실사용 ⓕ=되돌림 f=0.9 상승` 이나
|
|
844
|
+
# `ⓔ=arm a=제어 b=처리 비교` 가 «혼용» 으로 차단됐다. 과차단은 `--no-verify` 를
|
|
845
|
+
# 훈련시키고, 그건 같은 훅 안의 Destructive-Op 게이트까지 무장해제한다.
|
|
846
|
+
# ⓑ **그리고 애초에 불필요하다** — 「어느 배열인가」는 **여섯 기호가 다 있는가**로
|
|
847
|
+
# 이미 판별된다. 옛 ASCII 키가 기호 키를 *대신하는* 경우는 그 기호가 빠졌다는
|
|
848
|
+
# 뜻이므로 바로 위 missing-key 검사가 이미 차단한다. 여섯이 다 있는 줄에 남은
|
|
849
|
+
# ASCII 조각은 값의 일부일 뿐 배열 선언을 흐리지 않는다.
|
|
850
|
+
# ⇒ 오탐만 만들고 아무것도 더 막지 않는 필터였다. **삭제가 수리보다 낫다**
|
|
851
|
+
# ([[feedback_deletion_beats_repair_dead_filter]]). 픽스처는 남긴다 — 그 케이스는
|
|
852
|
+
# 여전히 차단돼야 하고, 이제 *다른 사유로* 차단된다는 사실 자체가 고정할 값어치가 있다.
|
|
853
|
+
|
|
854
|
+
# ⓑ입장은 값을 여기 적지 않는다 — `standpoint:` 자기 필드가 정본이라 이중 기록이 된다.
|
|
855
|
+
# 포인터를 쓴 경우 그 필드의 **존재**만 확인한다(값 enum 검증은 별건, 정본이 유보).
|
|
856
|
+
# 🟥 화살표를 필수로 보면 **fail-open** 이다 — 자기 프로브 실측: `ⓑ=standpoint` (화살표
|
|
857
|
+
# 없음)는 검사를 통째로 비껴가서 죽은 포인터가 그대로 통과했다. 그래서 «ⓑ 값이
|
|
858
|
+
# standpoint 를 참조하는가» 로 넓힌다. **명시 잔여**: `ⓑ=입장리뷰 standpoint 참조`
|
|
859
|
+
# 처럼 참조어가 **다른 토큰**에 있으면 여전히 안 잡힌다(값에 공백이 허용되므로
|
|
860
|
+
# 토큰 경계를 알 수 없다). 넓히려면 값 문법을 조여야 하고 그건 별건이다.
|
|
861
|
+
if echo "$line" | grep -qE "(^|[[:space:]])ⓑ=[^[:space:]]*standpoint" \
|
|
862
|
+
&& ! grep -qE '^[[:space:]]*standpoint:[[:space:]]*[^[:space:]]' "$m" 2>/dev/null; then
|
|
863
|
+
echo " ❌ MARKER — 'ⓑ=→standpoint' 라고 적었는데 'standpoint:' 줄이 없다"
|
|
864
|
+
echo " 포인터가 가리키는 곳이 비어 있으면 그건 기록이 아니라 죽은 포인터다."
|
|
865
|
+
echo " standpoint: tier1 (내용만 — 남의 하네스 정본을 안 읽었다)"
|
|
866
|
+
echo " standpoint: not-applicable"
|
|
867
|
+
return 1
|
|
868
|
+
fi
|
|
869
|
+
fi
|
|
870
|
+
|
|
695
871
|
line=$(grep -m1 -E '^[[:space:]]*controls:' "$m" 2>/dev/null \
|
|
696
872
|
| sed -E 's/^[[:space:]]*controls:[[:space:]]*//')
|
|
697
873
|
if [ -z "$line" ]; then
|
|
@@ -1164,6 +1340,57 @@ if [ -f "$JC_REG" ] && [ -f "$JC_LINT" ]; then
|
|
|
1164
1340
|
fi
|
|
1165
1341
|
fi
|
|
1166
1342
|
|
|
1343
|
+
# ── Portability lint (advisory — the SHELL-layer sibling of the two lints above) ──────────────
|
|
1344
|
+
# 🟥 WHY THIS IS HERE AND NOT DEFERRED. `scripts/portability_lint.sh` shipped in the immediately
|
|
1345
|
+
# preceding commit with its self-test wired and NO real-run caller, and `fh_4axis_gate.md:232` says
|
|
1346
|
+
# exactly what that is:
|
|
1347
|
+
# "If you build a checker and defer its caller, you shipped prose. The gate below trims what
|
|
1348
|
+
# you *attached*; it never licenses shipping something unreachable."
|
|
1349
|
+
# The standpoint review (2026-08-16, isolated agent reading the canon COLD before the diff) caught
|
|
1350
|
+
# it — and caught that the same commit CITED that section as its bundling justification while
|
|
1351
|
+
# violating it in the other half. Two other adversarial legs and a cross-family leg had all missed
|
|
1352
|
+
# this, because all three were handed the author's framing; the standpoint leg was handed the canon.
|
|
1353
|
+
# That is the axis earning its place, and this block is the repair.
|
|
1354
|
+
#
|
|
1355
|
+
# Placement is also not free choice: weekly_audit_2026-08-16 🟧-3 prescribed "pre-commit 에 패턴
|
|
1356
|
+
# denylist 한 장 … 새 러너가 아니라 degrade_direction_scan 옆 한 레인" — the runner already exists,
|
|
1357
|
+
# so this lane is the half that was missing from the prescription, not a second mechanism.
|
|
1358
|
+
#
|
|
1359
|
+
# STAGED BLOBS, not working-tree paths — same reason the two lints above use _stage_blob: measuring
|
|
1360
|
+
# the disk after `git add` reports clean while a different blob commits (optimistic fail-open).
|
|
1361
|
+
# ADVISORY by construction: FP-tolerant pre-screen, never blocks. Over-blocking here would train
|
|
1362
|
+
# `--no-verify`, which disarms the irreversible Destructive-Op gate living in the sibling hook.
|
|
1363
|
+
PL_LINT="$REPO_ROOT/scripts/portability_lint.sh"
|
|
1364
|
+
if [ -f "$PL_LINT" ]; then
|
|
1365
|
+
_pl_targets=()
|
|
1366
|
+
while IFS= read -r _pl_line; do
|
|
1367
|
+
[ -n "$_pl_line" ] || continue
|
|
1368
|
+
# 확장자 없는 셸 파일도 대상이다(.git-hooks/pre-commit·pre-push, bin/fh-*) — 이 레포에서
|
|
1369
|
+
# BSD/GNU 결함이 가장 비싼 파일들이 바로 그 형태이고, 초판 스캔이 그래서 놓쳤다.
|
|
1370
|
+
case "$_pl_line" in
|
|
1371
|
+
*.sh) : ;;
|
|
1372
|
+
templates/.git-hooks/*|bin/*) : ;;
|
|
1373
|
+
*) continue ;;
|
|
1374
|
+
esac
|
|
1375
|
+
_b="$(_stage_blob "$_pl_line")" || continue
|
|
1376
|
+
# 비-셸(bin/*.js 등)은 shebang 으로 걸러낸다. 확장자로 거르면 확장자 없는 훅이 다시 빠진다.
|
|
1377
|
+
case "$_pl_line" in
|
|
1378
|
+
*.sh) : ;;
|
|
1379
|
+
*) head -1 "$_b" 2>/dev/null | grep -qE '^#!.*\b(ba)?sh' || { rm -f "$_b"; continue; } ;;
|
|
1380
|
+
esac
|
|
1381
|
+
_pl_targets+=("$_b"); _STAGE_TMPS+=("$_b"); _STAGE_MAP+=("$_b=$_pl_line")
|
|
1382
|
+
done < <(printf '%s\n' "$STAGED")
|
|
1383
|
+
if [ "${#_pl_targets[@]}" -gt 0 ]; then
|
|
1384
|
+
echo "[Gate] shell file staged — portability lint (advisory)..."
|
|
1385
|
+
_pl_out="$(bash "$PL_LINT" "${_pl_targets[@]}" 2>&1)"; _pl_rc=$?
|
|
1386
|
+
printf '%s\n' "$(_unmap "$_pl_out")" | sed 's/^/ /'
|
|
1387
|
+
# rc=10 은 이 스캐너 자신의 HARNESS-ERROR 다. PASS 도 FAIL 도 아니며, 삼키면 죽은 계기가
|
|
1388
|
+
# 깨끗한 실행으로 렌더된다 — 이 커밋 전체가 고치고 있는 그 클래스다.
|
|
1389
|
+
[ "$_pl_rc" = "10" ] && echo " 🟥 portability lint HARNESS-ERROR (rc=10) — instrument did not measure"
|
|
1390
|
+
echo " (advisory — does not block. 알고 쓰는 줄엔 \`# portability-noqa: <사유>\`.)"
|
|
1391
|
+
fi
|
|
1392
|
+
fi
|
|
1393
|
+
|
|
1167
1394
|
# ── Novelty-claim ledger (advisory) ───────────────────────────────────────────
|
|
1168
1395
|
# "Ask the world before asserting novelty" is an INTENT trigger — un-hookable, like the
|
|
1169
1396
|
# repo-go-public surface. So this does not force the asking; it makes NOT having asked visible
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_readme": "FH dispatch-tally hook. Merge the `hooks.SubagentStop` block below into your .claude/settings.json (that file is gitignored, which is why this template exists). It feeds session_close_check.sh ④-e, which blocks a close push when sub-agents were dispatched and the day's invocation-log entries are zero. The hook TALLIES ONLY — it never writes a log entry, because a fabricated outcome/evidence field would poison the 60/40 promotion gate worse than a missing one.",
|
|
2
|
+
"_readme": "FH dispatch-tally hook. Merge the `hooks.SubagentStop` block below into your .claude/settings.json (that file is gitignored, which is why this template exists). It feeds session_close_check.sh ④-e, which blocks a close push when sub-agents were dispatched and the day's invocation-log entries are zero. The hook TALLIES ONLY — it never writes a log entry, because a fabricated outcome/evidence field would poison the 60/40 promotion gate worse than a missing one. Root resolution is 3-tier (CLAUDE_PROJECT_DIR -> git rev-parse --show-toplevel -> hardcoded path), matching the SAME convention fh_node_check.sh already uses in its own SessionStart hook -- a 2026-08-16 standpoint review on a sibling harness (pmh-dev) found this template's earlier 2-tier version get copy-pasted without picking up that convention, silently diverging from session_close_check.sh's own git-toplevel-first resolution in a nested/worktree/subagent context. If you adapt this template for another repo, keep the 3-tier form.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SubagentStop": [
|
|
5
5
|
{
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "bash -c 'HUB=\"${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}\"; T=\"$HUB/tracks/_meta/.subagent_dispatch_tally\"; mkdir -p \"$(dirname \"$T\")\" 2>/dev/null; printf \"%s\\n\" \"$(date +%Y-%m-%d)\" >> \"$T\" 2>/dev/null; exit 0'",
|
|
10
|
+
"command": "bash -c 'HUB=\"${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo \"$HOME/projects/forge-harness\")}\"; T=\"$HUB/tracks/_meta/.subagent_dispatch_tally\"; mkdir -p \"$(dirname \"$T\")\" 2>/dev/null; printf \"%s\\n\" \"$(date +%Y-%m-%d)\" >> \"$T\" 2>/dev/null; exit 0'",
|
|
11
11
|
"timeout": 5
|
|
12
12
|
}
|
|
13
13
|
]
|