@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.
Files changed (39) hide show
  1. package/.claude/rules/fh_4axis_gate.md +25 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CLAUDE.md +107 -7
  4. package/README.ja.md +14 -2
  5. package/README.ko.md +14 -1
  6. package/README.md +16 -2
  7. package/README.zh.md +12 -1
  8. package/knowledge/shared/harness-core/fh_three_layer_canon.md +18 -6
  9. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +262 -7
  10. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
  11. package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
  12. package/knowledge/shared/harness-core/sonnet_floor_doctrine.md +16 -0
  13. package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -0
  14. package/knowledge/shared/rules/multi_session_close_protocol.md +124 -2
  15. package/package.json +8 -1
  16. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  17. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  18. package/plugins/fh-meta/CHANGELOG.md +105 -0
  19. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
  20. package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
  21. package/scripts/adapters/mate_agent_boundary.sh +27 -7
  22. package/scripts/capability_effect_probe.sh +173 -5
  23. package/scripts/capability_registry_check.sh +69 -3
  24. package/scripts/digest_landing_check.sh +168 -13
  25. package/scripts/fh_session_load.sh +71 -0
  26. package/scripts/relay_channel.sh +151 -6
  27. package/scripts/selfcheck.sh +47 -0
  28. package/scripts/target_freeze.sh +239 -0
  29. package/scripts/test_adapter_lanes.sh +17 -0
  30. package/scripts/test_evidence_root_psa_lanes.sh +78 -0
  31. package/scripts/test_marker_standpoint_lanes.sh +83 -0
  32. package/scripts/test_marker_thirdparty_lanes.sh +73 -0
  33. package/scripts/test_regression_guard_ci_lanes.sh +170 -0
  34. package/scripts/test_relay_channel_lanes.sh +146 -17
  35. package/scripts/test_satellite_publish_gate_lanes.sh +339 -0
  36. package/scripts/test_target_freeze_lanes.sh +203 -0
  37. package/templates/.git-hooks/pre-commit +306 -6
  38. package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
  39. package/templates/regression_guard.sh +32 -4
@@ -18,6 +18,42 @@
18
18
  set -uo pipefail
19
19
 
20
20
  REPO_ROOT=$(git rev-parse --show-toplevel)
21
+ # ── EVIDENCE_ROOT — 증거(tracks/)가 사는 곳. **소스 경로와 다르다.** ──────────────
22
+ # 🟥 WHY: 워크트리에서 `--show-toplevel` 은 **그 워크트리**를 준다. `tracks/` 는 gitignored 라
23
+ # 워크트리에 안 따라오므로, 마커와 매니페스트를 거기서 찾으면 **항상 부재**다. 그래서 게이트가
24
+ # 스스로 권하는 «세션당 worktree» 를 게이트 자신이 봉쇄하고 있었다(2026-08-18 실측: 워크트리
25
+ # FH-자산 커밋 → Axis 2+3 「NOT CONFIRMED」 + Axis 4 「edit_manifest.yaml not found」).
26
+ # 「구조적으로 불가능」이 아니라 **경로 해석 한 줄**이었다.
27
+ # `--git-common-dir` 은 워크트리에서도 **메인 트리의 .git** 을 준다. 메인 트리에서는 `.git` 이라
28
+ # 같은 값이 나온다 ⇒ **하위호환**(3트리 대조 실측: 메인/워크트리/FH 전부 기대대로).
29
+ # ⚠️ 상대 경로로 나올 수 있으므로 절대화한다. 실패하면 REPO_ROOT 로 **degrade** — 증거를 못 찾는
30
+ # 쪽이지 통과시키는 쪽이 아니다(fail-closed 방향 유지).
31
+ #
32
+ # 🟥 **명시된 잔여 — 이 수리가 «두 번째 위험»을 처음 도달 가능하게 만든다** (2026-08-18,
33
+ # 타계열 리뷰 지적, 실측으로 확인):
34
+ # ⓐ 증거 부재 = 이 블록이 닫는 것
35
+ # ⓑ 훅 자기무력화 = **안 닫힌다.** `core.hooksPath` 가 **상대 경로**(`templates/.git-hooks`
36
+ # — FH 문서가 지시하는 바로 그 형태)면 워크트리는 **자기 사본**의 훅을 돌린다.
37
+ # 그 사본을 워크트리 안에서 고치면 그 워크트리의 게이트가 죽고,
38
+ # 메인 트리의 `git status` 에는 안 보인다.
39
+ # 종전엔 ⓐ 가 **사실상의 장벽**이라 ⓑ 가 도달 불가였다. ⓐ 를 없애면 ⓑ 가 **처음으로 실재**한다.
40
+ # ⇒ 워크트리를 쓸 거면 `core.hooksPath` 를 **절대 경로**로 설정해라:
41
+ # git config core.hooksPath "$(git rev-parse --show-toplevel)/templates/.git-hooks"
42
+ # 그러면 모든 워크트리가 **메인 트리의 훅 한 벌**을 돈다.
43
+ # 🟥 정정 (2026-08-18) — 초판은 여기에 «그러면 자기무력화가 **구조적으로 막힌다**» 라고
44
+ # 적었고 그건 **과대주장**이다. 절대 경로가 막는 것은 **훅 파일 자체의 교체**뿐이다:
45
+ # 이 훅은 `$REPO_ROOT/scripts/*` 를 10곳 넘게 **source** 하므로(psa_scan_lib ·
46
+ # branch_claim · regression_guard …), 워크트리에서 그 헬퍼 하나를 고치면 게이트는
47
+ # 그대로 초록으로 통과한다. 「막힌다」가 아니라 **「한 겹 좁아진다」**가 정확하다.
48
+ # 🟥 이 훅은 그것을 **강제하지 않는다** — 자기 자신의 설치 형태를 자기가 검사하는 것은
49
+ # 이미 무력화된 뒤엔 안 돌기 때문이다(검사기가 검사 대상). 문서·설치 안내가 그 층이다.
50
+ _gcd=$(git rev-parse --git-common-dir 2>/dev/null || echo "")
51
+ case "$_gcd" in
52
+ "") EVIDENCE_ROOT="$REPO_ROOT" ;;
53
+ /*) EVIDENCE_ROOT=$(dirname "$_gcd") ;;
54
+ *) EVIDENCE_ROOT=$(dirname "$REPO_ROOT/$_gcd") ;;
55
+ esac
56
+ [ -d "$EVIDENCE_ROOT" ] || EVIDENCE_ROOT="$REPO_ROOT"
21
57
  BRANCH=$(git rev-parse --abbrev-ref HEAD)
22
58
  TODAY=$(date +%Y-%m-%d)
23
59
  BRANCH_SLUG="${BRANCH//\//_}"
@@ -281,8 +317,17 @@ if [ ! -r "$PSA_LIB" ]; then
281
317
  FAILED=1
282
318
  else
283
319
  . "$PSA_LIB"
320
+ # ── R4 (2026-08-18) · 오버라이드는 **EVIDENCE_ROOT** 에서 찾는다 ──────────────
321
+ # `tracks/` 는 `--git-common-dir` 로 옮겼는데 **똑같이 gitignored 인** 이 패턴 파일은
322
+ # `$REPO_ROOT` 에 남아 있었다. 그래서 워크트리 커밋이 매번 **defaults-only** 로 기밀성
323
+ # 스캔을 돌았다(회사명·실명 클래스가 통째로 UNSCANNED, 게다가 비차단 경고라 조용하다).
324
+ # 반쪽-픽스의 전형이다([[feedback_half_fix_propagation_boundary]]).
325
+ # 🟥 `defaults` 는 **tracked** 라 `REPO_ROOT` 가 맞다 — 같이 옮기면 안 된다. 옮기는 것은
326
+ # gitignored 인 오버라이드 쪽 하나뿐이고, 없으면 REPO_ROOT 로 폴백한다.
327
+ PSA_OVR="$EVIDENCE_ROOT/.claude/rules/.public-surface-patterns"
328
+ [ -r "$PSA_OVR" ] || PSA_OVR="$REPO_ROOT/.claude/rules/.public-surface-patterns"
284
329
  psa_load "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" \
285
- "${PSA_PATTERNS:-$REPO_ROOT/.claude/rules/.public-surface-patterns}"
330
+ "${PSA_PATTERNS:-$PSA_OVR}"
286
331
  if [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
287
332
  # 2026-08-16 weekly-audit residual — operator decision: LOUDER WARNING, not fail-closed.
288
333
  # A hard block here would fail every fresh clone's / CI's / worktree's first commit (the
@@ -305,7 +350,7 @@ else
305
350
  if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
306
351
  echo " ⚠️ proceeding with an incomplete gate by PUBLIC_SURFACE_OK=1 (conscious)"
307
352
  echo "$(date +%Y-%m-%dT%H:%M:%S) PUBLIC_SURFACE_OK override — branch $BRANCH — gate inactive/incomplete" \
308
- >> "$REPO_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
353
+ >> "$EVIDENCE_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
309
354
  else
310
355
  FAILED=1
311
356
  fi
@@ -342,7 +387,7 @@ else
342
387
  if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
343
388
  echo " ⚠️ public-surface hit(s) allowed by PUBLIC_SURFACE_OK=1 (conscious, reviewed intent)"
344
389
  echo "$(date +%Y-%m-%dT%H:%M:%S) PUBLIC_SURFACE_OK override — branch $BRANCH — review suppressed hit(s) above" \
345
- >> "$REPO_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
390
+ >> "$EVIDENCE_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
346
391
  else
347
392
  echo " Operator-private token reached the public surface. Generalize it (companion-store name"
348
393
  echo " → 'a private companion store'; corp-context → 'restricted/corp env'; absolute home path"
@@ -717,6 +762,217 @@ validate_crossfamily_leg() { # $1 = marker path
717
762
  return 0
718
763
  }
719
764
 
765
+ # ── standpoint: typed value lane (2026-08-17) ────────────────────────────────
766
+ # Grammar: standpoint: <VALUE>[ — <reason>]
767
+ #
768
+ # WHY NOW. This field shipped 2026-08-14 with its value DELIBERATELY unvalidated — the doctrine
769
+ # said "mechanize on the first recorded false value, not before". That value is now on record:
770
+ # a `release_2.3.0` marker wrote `not-applicable` on a delta whose OWN grounds line concedes
771
+ # "소비자 install 의 게이트 수용은 바뀐다 (BREAKING 2건)", and two 2026-08-14 deltas that changed
772
+ # shipped gate scripts / a shipped SKILL.md carried no line at all. The threshold this field set
773
+ # for itself is met.
774
+ #
775
+ # SCOPE — channel, not judgment (CLAUDE.md §Mechanization Boundary). This asserts properties of the
776
+ # RECORD: present · single · a member of the closed enum · non-vacuous · not wearing the OTHER
777
+ # axis's tokens. It never asserts the value is CORRECT — that stays with the reader, by design.
778
+ #
779
+ # 🟥 NAMED RESIDUAL — the EXECUTION claim warns, it does not block. tier2/tier2b/tier3 assert that
780
+ # something was RUN in the target, which §7 calls "the load-bearing half". Blocking on that needs a
781
+ # vocabulary grep, and on first contact with the real corpus that grep over-blocked a legitimate
782
+ # marker whose grounds read "그 레포에서 실제로 호출해 양·음 arm 을 확인했다" — it did not know 「호출」.
783
+ # Over-blocking trains `--no-verify`, which would disarm the Destructive-Op gate in this same hook.
784
+ # So the cost is stated instead of hidden: a fabricated tier2 with a fluent reason PASSES this lane.
785
+ # Closing that is §4-b's job (cross-family reads the marker), not this lane's.
786
+ #
787
+ # 🟥 DOGFOOD RESULT, STATED SO NOBODY READS THIS AS MORE THAN IT IS — the very marker whose false
788
+ # value MET this field's mechanization threshold (`release_2.3.0`, which wrote `not-applicable` on a
789
+ # release delta) **PASSES this lane**, because its grounds sentence is long and specific. That is not
790
+ # a bug in the lane; it is the Mechanization Boundary holding: the channel is checked, the judgment
791
+ # is not. Anyone reporting that this wiring "closed the self-report axis" is wrong. What closed is
792
+ # the shape of the record; what remains open is whether the recorded answer is true.
793
+ #
794
+ # Calibration before wiring (2026-08-17): known-pair 12/12 both directions; run against all 24
795
+ # corpus markers carrying the field → 7 blocked, every one a real defect (6 bare `not-applicable`,
796
+ # independently matching a hand count, + 1 marker carrying TWO contradictory standpoint lines),
797
+ # and 0 over-blocks. Anchor: scripts/test_marker_standpoint_lanes.sh
798
+ validate_standpoint_leg() { # $1 = marker path
799
+ local m="$1" line val reason n quoted=""
800
+ n=$(grep -cE '^[[:space:]]*standpoint:' "$m" 2>/dev/null); n=${n:-0}
801
+ if [ "$n" -eq 0 ]; then
802
+ echo " ❌ FAIL — load-bearing file staged with no 'standpoint:' line in the Axes 2-3 marker."
803
+ echo " State the answer, whatever it is — the gate blocks silence, not a modest value."
804
+ echo " Settle the TARGET CLASS first (§7 Q0), then the tier:"
805
+ echo " standpoint: not-applicable — <what you checked to conclude no target exists>"
806
+ echo " standpoint: tier1b(pmh-dev) — read the target's own files, executed nothing"
807
+ echo " standpoint: tier2(qasp-dev) — ran <command> there, saw <output>"
808
+ echo " standpoint: DEGRADED_NOT_RUN — target reachable, not run (<why>)"
809
+ echo " Append one line to: $m"
810
+ return 1
811
+ fi
812
+ # Duplicate guard — same defect class the crossfamily lane closes: a marker is append-only in
813
+ # practice and readers take the FIRST match, so an appended correction is silently shadowed.
814
+ # Measured in this corpus: one marker carries `tier1` at :32 and `not-applicable` at :89.
815
+ if [ "$n" -gt 1 ]; then
816
+ echo " ❌ FAIL — marker carries MORE THAN ONE 'standpoint:' line."
817
+ grep -nE '^[[:space:]]*standpoint:' "$m" | sed 's/^/ /'
818
+ echo " A later line does not supersede an earlier one — the reader takes the first."
819
+ echo " Delete the stale line and leave exactly one."
820
+ return 1
821
+ fi
822
+ line=$(grep -m1 -E '^[[:space:]]*standpoint:' "$m" | sed -E 's/^[[:space:]]*standpoint:[[:space:]]*//')
823
+ case "$line" in '"'*|"'"*) quoted=" [format: value wrapped in quotes — an auditor's grep for
824
+ 'standpoint: tier2' misses it; drop the quotes next time]";; esac
825
+ line=$(printf '%s' "$line" | sed -E 's/^["'"'"']//; s/["'"'"']$//')
826
+ val=$(printf '%s' "$line" | sed -E 's/[[:space:]]*(—|--).*$//' | sed -E 's/[[:space:]]+$//')
827
+ reason=$(printf '%s' "$line" | sed -E 's/^[^—-]*((—|--)[[:space:]]*)?//')
828
+ # Cross-axis contamination. `crossfamily:` and `standpoint:` each carry a three-way degrade
829
+ # triad and the doctrine says the literals are DISTINCT ON PURPOSE. Borrowing the other axis's
830
+ # token records the wrong axis while looking well-formed.
831
+ case "$val" in
832
+ DEGRADED_SINGLE_FAMILY*|DEGRADED_PANEL_UNUSED*|panel\(*|declined)
833
+ echo " ❌ FAIL — '$val' belongs to the crossfamily: axis, not standpoint:."
834
+ echo " crossfamily = WHICH MODEL FAMILY reviewed. standpoint = WHOSE REPO was ground truth."
835
+ echo " standpoint's own triad: DEGRADED_NO_TARGET_ACCESS (could not) /"
836
+ echo " DEGRADED_NOT_RUN (did not) / UNKNOWN (did not look)."
837
+ return 1 ;;
838
+ esac
839
+ case "$val" in
840
+ tier1)
841
+ echo " ✅ standpoint leg: tier1 (content-only — a scoping fact, not a failure)$quoted" ;;
842
+ tier1b\(*\)|tier2\(*\)|tier2b\(*\)|tier3\(*\))
843
+ case "$val" in
844
+ tier2\(*\)|tier2b\(*\)|tier3\(*\))
845
+ if [ "${#reason}" -lt 20 ] || ! printf '%s' "$reason" \
846
+ | grep -qiE 'ran |run |실행|돌렸|호출|invok|call|rc=|exit |output|출력|suite|스위트|test|bash |grep |npm |pytest|command|커맨드|arm'; then
847
+ echo " ⚠️ standpoint leg: $val — recorded, but the grounds do not NAME an execution."
848
+ echo " ${val%%(*} asserts code RAN in the target. The discriminator is mechanical:"
849
+ echo " name the command you ran and the output you saw. If you only read files,"
850
+ echo " the honest rung is tier1b — weaker on purpose, so the owed arm stays visible."
851
+ echo " Advisory by design (see the named residual above this function)."
852
+ else
853
+ echo " ✅ standpoint leg: $val$quoted"
854
+ fi ;;
855
+ *) echo " ✅ standpoint leg: $val$quoted" ;;
856
+ esac ;;
857
+ not-applicable)
858
+ # §7: asserting non-applicability without naming what was checked is indistinguishable from
859
+ # UNKNOWN wearing a permissive label. Measured: 6 bare instances in this corpus.
860
+ if [ "${#reason}" -lt 20 ]; then
861
+ echo " ❌ FAIL — bare 'not-applicable' with no grounds on the same line."
862
+ echo " That is UNKNOWN wearing a permissive label. Name what you checked:"
863
+ echo " no named peer carries this surface AND it changes no consumer-visible behavior."
864
+ return 1
865
+ fi
866
+ echo " ✅ standpoint leg: not-applicable$quoted" ;;
867
+ DEGRADED_NO_TARGET_ACCESS*|DEGRADED_NOT_RUN*|UNKNOWN*)
868
+ if [ "${#reason}" -lt 20 ]; then
869
+ echo " ❌ FAIL — $val without substantive grounds on the same line."
870
+ echo " could-not / did-not / did-not-look are separate values on purpose; collapsing"
871
+ echo " them renders an unrun probe as a zero finding. Name what you probed."
872
+ return 1
873
+ fi
874
+ echo " ⚠️ standpoint leg: $val — recorded, not silent" ;;
875
+ *)
876
+ echo " ❌ FAIL — '$val' is not a member of the standpoint: enum."
877
+ echo " Closed enum: tier1 · tier1b(<h>) · tier2(<h>) · tier2b(<h>) · tier3(<h>) ·"
878
+ echo " not-applicable · DEGRADED_NO_TARGET_ACCESS · DEGRADED_NOT_RUN · UNKNOWN"
879
+ echo " Default-deny: an unrecognised value is UNDECIDABLE, never assumed benign."
880
+ return 1 ;;
881
+ esac
882
+ return 0
883
+ }
884
+
885
+ # ── thirdparty: typed value lane (2026-08-17) ────────────────────────────────
886
+ # Grammar: thirdparty: <VALUE>(<grounds>) · UNKNOWN takes no parenthetical
887
+ #
888
+ # WHAT THIS AXIS IS — and it has TWO halves, which is what made getting it wrong so easy.
889
+ # The 6-axis canon (fh_three_layer_canon.md:243) defines ⓓ by what it RECEIVES: «문제 + 남의
890
+ # 코드베이스», and it asks two questions:
891
+ # ① prior art — has this claimed-new thing already been solved outside? checked/none-found
892
+ # ② harness-level — how does my change look from ANOTHER HARNESS's repo and peer-review
893
+ # adversarial persona? (operator: put Sonnet in gstack, wear the gstack
894
+ # persona, review the qasp change — a cross-FAMILY, harness-scoped
895
+ # adversarial review. FH is the GOVERNOR: it creates the situation,
896
+ # observes, and judges. What the 4-axis gate verifies is the OPINION
897
+ # that third harness produced, judged LOCALLY — it does not put a gate
898
+ # on someone else's repo.)
899
+ # It is NOT "I talked to another session". The canonical spec
900
+ # (`.claude/rules/fh_4axis_gate.md`) has said so since the field shipped, and the evidence that
901
+ # created it was 6 presentation claims that turned out to have prior art (mutation testing,
902
+ # promptfoo/DeepEval, an official cross-family plugin, …).
903
+ #
904
+ # WHY NOW — the first recorded false values exist, and they are the author's own. Measured across
905
+ # the marker corpus 2026-08-17: 4 markers used the enum correctly; **2 wrote free prose about
906
+ # peer-session contact** and matched no enum member at all. That is the field-canon failure this
907
+ # repo already names — normalizing a harness term into a general concept ("third party" → "another
908
+ # session"). Presence was checked; the value was not; nothing caught it.
909
+ #
910
+ # SCOPE — channel, not judgment (CLAUDE.md §Mechanization Boundary). This asserts the RECORD is a
911
+ # member of the closed enum and names non-vacuous grounds. It never asserts the prior-art search
912
+ # was thorough, nor that what a third party told you is TRUE — 🟥 that second one is deliberate and
913
+ # load-bearing: the axis exists to surface information you could not predict, and the truth of what
914
+ # comes back is adjudicated by the OTHER axes (adversarial · grounding · first-real-use), which is
915
+ # what the 4-axis gate is for. A wrong answer from a third party is that party's defect, possibly
916
+ # one to go fix; it is not something this field should be asked to certify.
917
+ #
918
+ # TRIGGER — validate IF PRESENT. Presence stays pointer-driven (`ⓓ=→thirdparty`), deliberately:
919
+ # requiring the field on every load-bearing change would force a prior-art search per commit, and
920
+ # over-blocking trains the override that disarms the Destructive-Op gate in this same hook.
921
+ #
922
+ # Calibration before wiring: the corpus supplied BOTH arms — peer-authored markers 4/4 PASS,
923
+ # author's own malformed 2/2 BLOCK, plus 6 synthetic shapes, 0 over-blocks. An earlier draft DID
924
+ # over-block 3 legitimate markers by looking for the closing paren on the first line when the
925
+ # grounds wrap across lines; the corpus caught it. Anchor: scripts/test_marker_thirdparty_lanes.sh
926
+ validate_thirdparty_leg() { # $1 = marker path
927
+ local m="$1" line val reason n
928
+ n=$(grep -cE '^[[:space:]]*thirdparty:' "$m" 2>/dev/null); n=${n:-0}
929
+ [ "$n" -eq 0 ] && return 0 # absent is governed by the ⓓ pointer check above, not here
930
+ if [ "$n" -gt 1 ]; then
931
+ echo " ❌ FAIL — marker carries MORE THAN ONE 'thirdparty:' line."
932
+ grep -nE '^[[:space:]]*thirdparty:' "$m" | sed 's/^/ /'
933
+ echo " Readers take the first, so an appended correction is silently shadowed."
934
+ return 1
935
+ fi
936
+ line=$(grep -m1 -E '^[[:space:]]*thirdparty:' "$m" | sed -E 's/^[[:space:]]*thirdparty:[[:space:]]*//')
937
+ val=$(printf '%s' "$line" | sed -E 's/\(.*$//' | sed -E 's/[[:space:]]*(—|--).*$//' | sed -E 's/[[:space:]]+$//')
938
+ # Cross-axis contamination — standpoint's tiers and crossfamily's tokens are different axes.
939
+ case "$val" in
940
+ tier1|tier1b|tier2|tier2b|tier3|panel|declined|DEGRADED_SINGLE_FAMILY|DEGRADED_PANEL_UNUSED|DEGRADED_NO_TARGET_ACCESS)
941
+ echo " ❌ FAIL — '$val' belongs to another axis, not thirdparty:."
942
+ echo " standpoint = whose repo was ground truth · crossfamily = which model family reviewed"
943
+ echo " thirdparty = was this claimed-new thing already done OUTSIDE this repo."
944
+ return 1 ;;
945
+ esac
946
+ case "$val" in
947
+ UNKNOWN)
948
+ echo " ⚠️ thirdparty leg: UNKNOWN — 안 봤다. Recorded, not silent." ;;
949
+ peer-review|checked|none-found|not-applicable|DEGRADED_NO_ACCESS|DEGRADED_NOT_RUN)
950
+ # The parenthetical may wrap across lines; only require that it OPENS with content after it.
951
+ case "$line" in *\(*) ;; *)
952
+ echo " ❌ FAIL — '$val' without a (<grounds>) parenthetical."
953
+ echo " none-found without naming WHAT you searched is indistinguishable from not looking."
954
+ return 1 ;;
955
+ esac
956
+ reason=$(printf '%s' "$line" | sed -E 's/^[^(]*\(//')
957
+ if [ "${#reason}" -lt 12 ]; then
958
+ echo " ❌ FAIL — '$val(...)' grounds are vacuous. Name what was searched and what came back."
959
+ return 1
960
+ fi
961
+ echo " ✅ thirdparty leg: $val" ;;
962
+ *)
963
+ echo " ❌ FAIL — '$val' is not a member of the thirdparty: enum."
964
+ echo " Closed enum: peer-review(<harness>/<persona> -> <target>, <what came back>) ·"
965
+ echo " checked(<searched/found>) · none-found(<searched>) ·"
966
+ echo " DEGRADED_NO_ACCESS(<why>) · DEGRADED_NOT_RUN(<why>) · UNKNOWN · not-applicable(<why>)"
967
+ echo " 🟥 ⓓ3자대면 = third-party PRIOR ART, not 'I talked to another session'. The two"
968
+ echo " markers that made this lane necessary both made exactly that substitution."
969
+ return 1 ;;
970
+ esac
971
+ return 0
972
+ }
973
+
974
+
975
+
720
976
  # ── 4축 자기 대조 — «어느 축을 돌렸고 어느 축을 안 돌렸나» 형식 검사 ────────────
721
977
  # CLAUDE.md §3층 자기 대조가 마커에 3줄을 요구한다. 그중 **기계로 볼 수 있는 두 줄**만 여기서
722
978
  # 강제한다. 나머지(①영혼을 «설계 전에» 썼는가)는 **원리적으로 확인 불가**라 안 넣는다 —
@@ -1032,7 +1288,7 @@ validate_marker_floor() {
1032
1288
  # ── Axes 2+3 — steel-quench + phantom-quench (full gate only) ─────────
1033
1289
  if [ "$GATE_MODE" = "full" ]; then
1034
1290
  echo "[Axis 2+3] Adversarial + Source-Grounding..."
1035
- MARKER_DIR="$REPO_ROOT/tracks/_meta"
1291
+ MARKER_DIR="$EVIDENCE_ROOT/tracks/_meta"
1036
1292
  MARKER="$MARKER_DIR/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1037
1293
 
1038
1294
  if [ -f "$MARKER" ]; then
@@ -1059,7 +1315,7 @@ fi
1059
1315
 
1060
1316
  # ── Axis 4 — Edit Manifest entry (always required) ────────────────────────────
1061
1317
  echo "[Axis 4] Edit Manifest..."
1062
- MANIFEST="$REPO_ROOT/tracks/_meta/edit_manifest.yaml"
1318
+ MANIFEST="$EVIDENCE_ROOT/tracks/_meta/edit_manifest.yaml"
1063
1319
  if [ ! -f "$MANIFEST" ]; then
1064
1320
  echo " ❌ FAIL — tracks/_meta/edit_manifest.yaml not found"
1065
1321
  echo " Run /edit-manifest RECORD or create the file manually."
@@ -1100,6 +1356,19 @@ for i, entry in enumerate(doc):
1100
1356
  if not isinstance(entry, dict):
1101
1357
  print("entry #%d is a %s, not a mapping — a list item may have drifted outside its parent"
1102
1358
  % (i, type(entry).__name__)); sys.exit(1)
1359
+ # 🟥 인터리브 지문 — 동시 append 는 **YAML 을 깨지 않고** 내용을 뒤섞는다.
1360
+ # 2026-08-18 실측(적대적 재현): 워커 4개가 한 엔트리를 여러 write 로 쪼개 append 하면
1361
+ # 파싱은 OK 인데 필드가 서로 섞이고(w3 의 branch 에 w2 의 impact) 나머지는 `date` 만 남는다.
1362
+ # ⇒ **형식 검사가 통과시키는 무음 손상.** 이 축이 없애려는 병을 이 축 자신이 통과시키고 있었다.
1363
+ # 지문은 스키마가 아니다 — 실물 407 엔트리의 키 분포가 느슨해서(date 100% · branch 38%)
1364
+ # 필수키를 걸면 과거를 과차단한다. known-pair 로 고른 지문은 **«date 만 있는 엔트리»**:
1365
+ # known-negative 실물 407건 → 0건 · known-positive 재현본 → 3/5.
1366
+ # 워크트리 병렬 커밋이 열리기 전에는 이 조건 자체가 안 생겼다 — #448 이 문을 여니 같이 닫는다.
1367
+ orphan = [i for i, e in enumerate(doc) if set(e.keys()) == {"date"}]
1368
+ if orphan:
1369
+ print("entry #%s has only `date` — concurrent-append interleaving signature "
1370
+ "(fields lost/mixed). Re-check the last appended entries by hand." % orphan[0])
1371
+ sys.exit(1)
1103
1372
  def norm(v):
1104
1373
  return v.isoformat() if hasattr(v, "isoformat") else str(v).strip()
1105
1374
  if not any(norm(e.get("date")) == sys.argv[2] for e in doc):
@@ -1301,8 +1570,10 @@ if [ -n "$LOADBEARING" ]; then
1301
1570
  else
1302
1571
  echo " ⚠️ degrade lint unavailable (scripts/degrade_direction_scan.sh missing) — advisory leg skipped"
1303
1572
  fi
1304
- MARKER_LB="$REPO_ROOT/tracks/_meta/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1573
+ MARKER_LB="$EVIDENCE_ROOT/tracks/_meta/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1305
1574
  if validate_crossfamily_leg "$MARKER_LB"; then :; else FAILED=1; fi
1575
+ if validate_standpoint_leg "$MARKER_LB"; then :; else FAILED=1; fi
1576
+ if validate_thirdparty_leg "$MARKER_LB"; then :; else FAILED=1; fi
1306
1577
  fi
1307
1578
 
1308
1579
  # ── staged-blob materializer (advisory linters) ───────────────────────────────
@@ -1411,6 +1682,35 @@ if [ -f "$PL_LINT" ]; then
1411
1682
  fi
1412
1683
  fi
1413
1684
 
1685
+ # ── Pre-ship read reminder (advisory) ─────────────────────────────────────────
1686
+ # WHY (2026-08-17, ⓓ3자대면 첫 실사용의 답습분). A third harness (gstack), wearing its own
1687
+ # `/plan-devex-review` persona, reviewed a peer harness's change and asked a question neither FH
1688
+ # nor that harness asks: **who opens this file, and what do they see in the first eight lines?**
1689
+ # Its finding: a human-facing report had been shipped to three surfaces where the body was 3 lines
1690
+ # under 21 lines of fixed template, the answer to "did it pass?" was buried in the 4th bullet, and
1691
+ # the last impression was "readers may skip this". The delta that shipped it was correct code.
1692
+ #
1693
+ # FH reached the same place independently the same day from a different axis — "readability is
1694
+ # measured only by rendering it or by a person; a static scan catches what is ABSENT, never what is
1695
+ # UNREADABLE", with 12 findings and 0 caught by static scanners. **Two independent arrivals** is
1696
+ # why this was the one item the internalization filter accepted out of four
1697
+ # (`harness_incubator_doctrine.md` §4-c).
1698
+ #
1699
+ # 🟥 WHAT THIS IS NOT. It does not judge readability, and it never blocks. Judging "is this
1700
+ # readable" is exactly the frozen-judgment class §Mechanization Boundary reserves for people, and a
1701
+ # vocabulary test for "human-facing document" over-blocked a legitimate case the same day it was
1702
+ # first tried. The trigger is therefore a PATH SET (what actually ships to readers), not a
1703
+ # vocabulary guess, and its whole output is one line of salience.
1704
+ _PRESHIP=$(printf '%s\n' "$STAGED" | grep -E '^(README([._a-z]*)?\.md|CHEATSHEET\.md|CATALOG\.md|docs/[^/]*\.md|templates/[^/]*\.md)$' || true)
1705
+ if [ -n "$_PRESHIP" ]; then
1706
+ echo "[Gate] reader-facing surface staged — pre-ship read reminder (advisory)..."
1707
+ printf '%s\n' "$_PRESHIP" | sed 's/^/ /'
1708
+ echo " ⚠️ 이 파일들은 «사람이 연다». 나가기 전에 한 번 «독자로서» 읽었나?"
1709
+ echo " 첫 8줄에 결론이 있나 · 본문이 템플릿에 안 덮이나 · 마지막 인상이 무엇인가"
1710
+ echo " 렌즈가 이미 있다: /sim-conductor A-1 (beginner cold-read) 또는 직접 렌더해서 읽기"
1711
+ echo " (advisory — 막지 않는다. 「읽었는가」는 이 훅이 보증할 수 없고 보증하려 들지도 않는다.)"
1712
+ fi
1713
+
1414
1714
  # ── Novelty-claim ledger (advisory) ───────────────────────────────────────────
1415
1715
  # "Ask the world before asserting novelty" is an INTENT trigger — un-hookable, like the
1416
1716
  # repo-go-public surface. So this does not force the asking; it makes NOT having asked visible
@@ -83,3 +83,25 @@ A pre-publish catch is a process win worth logging:
83
83
 
84
84
  **Done when**: Step 0 pre-flags clean (or escalated to full scan) · Step 1 both gates non-LEAK ·
85
85
  publish executed *after* the gates passed, never before.
86
+
87
+ ## Read it as a reader, once, before it goes out
88
+
89
+ Not a correctness check — the code can be right and this still fails. Open the thing a **reader**
90
+ opens and look at what they see:
91
+
92
+ - [ ] Is the **conclusion in the first ~8 lines**, or is it buried under banners, metadata and
93
+ "how to read this table"?
94
+ - [ ] Does the **body survive the template**? (measured case: 3 lines of content under 21 lines of
95
+ fixed boilerplate)
96
+ - [ ] What is the **last impression**? (measured case: the final line told the reader they could
97
+ skip it)
98
+ - [ ] Does it say **what the reader should do next**?
99
+
100
+ A lens already exists — `/sim-conductor` A-1 (`beginner` cold-read) — or just render it and read it.
101
+
102
+ 🟥 **Why this is a human step and not a scan**: a static check catches what is ABSENT, never what is
103
+ UNREADABLE. Two independent measurements arrived here the same day (2026-08-17): one harness axis
104
+ found 12 readability defects with **0** caught by static scanners; a third-harness review
105
+ (`harness_incubator_doctrine.md` §4-c) found a shipped report whose answer to "did it pass?" sat in
106
+ the fourth bullet. The pre-commit hook prints an advisory reminder when a reader-facing surface is
107
+ staged — it reminds, it does not certify.
@@ -64,13 +64,41 @@ if [ "${1:-}" = "--pr" ]; then
64
64
  fi
65
65
  PR_BRANCH="$2"
66
66
  BASE_BRANCH="${3:-main}"
67
- BASE_REF=$(git merge-base "$BASE_BRANCH" "$PR_BRANCH" 2>/dev/null)
67
+ # Resolve each side to a ref that actually EXISTS in this checkout.
68
+ #
69
+ # WHY (measured 2026-08-17, 5 of 5 SAMPLED CI runs — not an exhaustive audit): a bare branch
70
+ # NAME does not resolve in a GitHub Actions PR checkout. actions/checkout lands on a DETACHED
71
+ # HEAD and creates refs/remotes/origin/*, not local branches — so `git merge-base main <branch>`
72
+ # found neither side, returned empty, and this block exited 3. `fetch-depth: 0` was already set
73
+ # and is NOT the cause; the history was present, the NAMES were not. The workflow then rendered
74
+ # that instrument error as a green PASS (fixed in the same commit).
75
+ #
76
+ # 🟥 THE FIRST FIX FOR THIS INTRODUCED A WORSE HOLE, caught by cross-family review before it
77
+ # shipped. It resolved `ref -> origin/ref -> refs/remotes/origin/ref` and fell back to HEAD.
78
+ # `github.head_ref` is only a branch NAME, not owner-qualified, so a **fork PR whose branch is
79
+ # named `main`** resolved the PR side to the BASE repo's `main` — merge-base(main, main) = main,
80
+ # empty diff, SKIP, green. A guard silently comparing a branch to itself is worse than one that
81
+ # errors. So: NO name-guessing and NO silent HEAD fallback. Callers pass something
82
+ # unambiguous (a SHA, or an explicit `origin/<ref>`); anything that does not resolve EXACTLY
83
+ # is an instrument error, and the workflow now fails closed on that.
84
+ _rg_resolve_ref() { # $1 = ref-ish; echoes it iff it resolves EXACTLY as given (rc=1 otherwise)
85
+ git rev-parse --verify --quiet "${1}^{commit}" >/dev/null 2>&1 && printf '%s' "$1"
86
+ }
87
+ _BASE_RESOLVED=$(_rg_resolve_ref "$BASE_BRANCH") || _BASE_RESOLVED=""
88
+ _HEAD_RESOLVED=$(_rg_resolve_ref "$PR_BRANCH") || _HEAD_RESOLVED=""
89
+ if [ -z "$_BASE_RESOLVED" ] || [ -z "$_HEAD_RESOLVED" ]; then
90
+ echo "ERROR: ref does not resolve — base='$BASE_BRANCH'->'${_BASE_RESOLVED:-<none>}' head='$PR_BRANCH'->'${_HEAD_RESOLVED:-<none>}'" >&2
91
+ echo " Pass an unambiguous ref (a SHA, or origin/<branch>). Guessing is how a fork PR" >&2
92
+ echo " branch named 'main' silently compared the base repo's main to itself." >&2
93
+ exit 3
94
+ fi
95
+ BASE_REF=$(git merge-base "$_BASE_RESOLVED" "$_HEAD_RESOLVED" 2>/dev/null)
68
96
  if [ -z "$BASE_REF" ]; then
69
- echo "ERROR: cannot compute merge-base for $PR_BRANCH vs $BASE_BRANCH" >&2
97
+ echo "ERROR: cannot compute merge-base for $_HEAD_RESOLVED vs $_BASE_RESOLVED" >&2
70
98
  exit 3
71
99
  fi
72
- HEAD_REF="$PR_BRANCH"
73
- echo "PR MODE: merge-base=$(git rev-parse --short "$BASE_REF") branch=$PR_BRANCH"
100
+ HEAD_REF="$_HEAD_RESOLVED"
101
+ echo "PR MODE: merge-base=$(git rev-parse --short "$BASE_REF") base=$_BASE_RESOLVED head=$_HEAD_RESOLVED"
74
102
  elif [ "${1:-}" = "--staged" ]; then
75
103
  # Pre-commit context: evaluate the staged index against HEAD. On a direct-to-main
76
104
  # workflow, --pr's merge-base(main,main)=HEAD yields an empty diff, so staged changes