@chrono-meta/fh-gate 2.3.0 → 2.4.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 (31) hide show
  1. package/.claude/rules/fh_4axis_gate.md +25 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CLAUDE.md +51 -6
  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/field_verdict_crossfamily_gate.md +231 -7
  9. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
  10. package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
  11. package/knowledge/shared/learnings/subagent_invocations_log.yaml +24 -0
  12. package/package.json +6 -1
  13. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  14. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/CHANGELOG.md +57 -0
  16. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
  17. package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
  18. package/scripts/adapters/mate_agent_boundary.sh +27 -7
  19. package/scripts/capability_registry_check.sh +69 -3
  20. package/scripts/relay_channel.sh +151 -6
  21. package/scripts/selfcheck.sh +18 -0
  22. package/scripts/target_freeze.sh +239 -0
  23. package/scripts/test_adapter_lanes.sh +17 -0
  24. package/scripts/test_marker_standpoint_lanes.sh +83 -0
  25. package/scripts/test_marker_thirdparty_lanes.sh +73 -0
  26. package/scripts/test_regression_guard_ci_lanes.sh +170 -0
  27. package/scripts/test_relay_channel_lanes.sh +146 -17
  28. package/scripts/test_target_freeze_lanes.sh +203 -0
  29. package/templates/.git-hooks/pre-commit +242 -0
  30. package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
  31. package/templates/regression_guard.sh +32 -4
@@ -717,6 +717,217 @@ validate_crossfamily_leg() { # $1 = marker path
717
717
  return 0
718
718
  }
719
719
 
720
+ # ── standpoint: typed value lane (2026-08-17) ────────────────────────────────
721
+ # Grammar: standpoint: <VALUE>[ — <reason>]
722
+ #
723
+ # WHY NOW. This field shipped 2026-08-14 with its value DELIBERATELY unvalidated — the doctrine
724
+ # said "mechanize on the first recorded false value, not before". That value is now on record:
725
+ # a `release_2.3.0` marker wrote `not-applicable` on a delta whose OWN grounds line concedes
726
+ # "소비자 install 의 게이트 수용은 바뀐다 (BREAKING 2건)", and two 2026-08-14 deltas that changed
727
+ # shipped gate scripts / a shipped SKILL.md carried no line at all. The threshold this field set
728
+ # for itself is met.
729
+ #
730
+ # SCOPE — channel, not judgment (CLAUDE.md §Mechanization Boundary). This asserts properties of the
731
+ # RECORD: present · single · a member of the closed enum · non-vacuous · not wearing the OTHER
732
+ # axis's tokens. It never asserts the value is CORRECT — that stays with the reader, by design.
733
+ #
734
+ # 🟥 NAMED RESIDUAL — the EXECUTION claim warns, it does not block. tier2/tier2b/tier3 assert that
735
+ # something was RUN in the target, which §7 calls "the load-bearing half". Blocking on that needs a
736
+ # vocabulary grep, and on first contact with the real corpus that grep over-blocked a legitimate
737
+ # marker whose grounds read "그 레포에서 실제로 호출해 양·음 arm 을 확인했다" — it did not know 「호출」.
738
+ # Over-blocking trains `--no-verify`, which would disarm the Destructive-Op gate in this same hook.
739
+ # So the cost is stated instead of hidden: a fabricated tier2 with a fluent reason PASSES this lane.
740
+ # Closing that is §4-b's job (cross-family reads the marker), not this lane's.
741
+ #
742
+ # 🟥 DOGFOOD RESULT, STATED SO NOBODY READS THIS AS MORE THAN IT IS — the very marker whose false
743
+ # value MET this field's mechanization threshold (`release_2.3.0`, which wrote `not-applicable` on a
744
+ # release delta) **PASSES this lane**, because its grounds sentence is long and specific. That is not
745
+ # a bug in the lane; it is the Mechanization Boundary holding: the channel is checked, the judgment
746
+ # is not. Anyone reporting that this wiring "closed the self-report axis" is wrong. What closed is
747
+ # the shape of the record; what remains open is whether the recorded answer is true.
748
+ #
749
+ # Calibration before wiring (2026-08-17): known-pair 12/12 both directions; run against all 24
750
+ # corpus markers carrying the field → 7 blocked, every one a real defect (6 bare `not-applicable`,
751
+ # independently matching a hand count, + 1 marker carrying TWO contradictory standpoint lines),
752
+ # and 0 over-blocks. Anchor: scripts/test_marker_standpoint_lanes.sh
753
+ validate_standpoint_leg() { # $1 = marker path
754
+ local m="$1" line val reason n quoted=""
755
+ n=$(grep -cE '^[[:space:]]*standpoint:' "$m" 2>/dev/null); n=${n:-0}
756
+ if [ "$n" -eq 0 ]; then
757
+ echo " ❌ FAIL — load-bearing file staged with no 'standpoint:' line in the Axes 2-3 marker."
758
+ echo " State the answer, whatever it is — the gate blocks silence, not a modest value."
759
+ echo " Settle the TARGET CLASS first (§7 Q0), then the tier:"
760
+ echo " standpoint: not-applicable — <what you checked to conclude no target exists>"
761
+ echo " standpoint: tier1b(pmh-dev) — read the target's own files, executed nothing"
762
+ echo " standpoint: tier2(qasp-dev) — ran <command> there, saw <output>"
763
+ echo " standpoint: DEGRADED_NOT_RUN — target reachable, not run (<why>)"
764
+ echo " Append one line to: $m"
765
+ return 1
766
+ fi
767
+ # Duplicate guard — same defect class the crossfamily lane closes: a marker is append-only in
768
+ # practice and readers take the FIRST match, so an appended correction is silently shadowed.
769
+ # Measured in this corpus: one marker carries `tier1` at :32 and `not-applicable` at :89.
770
+ if [ "$n" -gt 1 ]; then
771
+ echo " ❌ FAIL — marker carries MORE THAN ONE 'standpoint:' line."
772
+ grep -nE '^[[:space:]]*standpoint:' "$m" | sed 's/^/ /'
773
+ echo " A later line does not supersede an earlier one — the reader takes the first."
774
+ echo " Delete the stale line and leave exactly one."
775
+ return 1
776
+ fi
777
+ line=$(grep -m1 -E '^[[:space:]]*standpoint:' "$m" | sed -E 's/^[[:space:]]*standpoint:[[:space:]]*//')
778
+ case "$line" in '"'*|"'"*) quoted=" [format: value wrapped in quotes — an auditor's grep for
779
+ 'standpoint: tier2' misses it; drop the quotes next time]";; esac
780
+ line=$(printf '%s' "$line" | sed -E 's/^["'"'"']//; s/["'"'"']$//')
781
+ val=$(printf '%s' "$line" | sed -E 's/[[:space:]]*(—|--).*$//' | sed -E 's/[[:space:]]+$//')
782
+ reason=$(printf '%s' "$line" | sed -E 's/^[^—-]*((—|--)[[:space:]]*)?//')
783
+ # Cross-axis contamination. `crossfamily:` and `standpoint:` each carry a three-way degrade
784
+ # triad and the doctrine says the literals are DISTINCT ON PURPOSE. Borrowing the other axis's
785
+ # token records the wrong axis while looking well-formed.
786
+ case "$val" in
787
+ DEGRADED_SINGLE_FAMILY*|DEGRADED_PANEL_UNUSED*|panel\(*|declined)
788
+ echo " ❌ FAIL — '$val' belongs to the crossfamily: axis, not standpoint:."
789
+ echo " crossfamily = WHICH MODEL FAMILY reviewed. standpoint = WHOSE REPO was ground truth."
790
+ echo " standpoint's own triad: DEGRADED_NO_TARGET_ACCESS (could not) /"
791
+ echo " DEGRADED_NOT_RUN (did not) / UNKNOWN (did not look)."
792
+ return 1 ;;
793
+ esac
794
+ case "$val" in
795
+ tier1)
796
+ echo " ✅ standpoint leg: tier1 (content-only — a scoping fact, not a failure)$quoted" ;;
797
+ tier1b\(*\)|tier2\(*\)|tier2b\(*\)|tier3\(*\))
798
+ case "$val" in
799
+ tier2\(*\)|tier2b\(*\)|tier3\(*\))
800
+ if [ "${#reason}" -lt 20 ] || ! printf '%s' "$reason" \
801
+ | grep -qiE 'ran |run |실행|돌렸|호출|invok|call|rc=|exit |output|출력|suite|스위트|test|bash |grep |npm |pytest|command|커맨드|arm'; then
802
+ echo " ⚠️ standpoint leg: $val — recorded, but the grounds do not NAME an execution."
803
+ echo " ${val%%(*} asserts code RAN in the target. The discriminator is mechanical:"
804
+ echo " name the command you ran and the output you saw. If you only read files,"
805
+ echo " the honest rung is tier1b — weaker on purpose, so the owed arm stays visible."
806
+ echo " Advisory by design (see the named residual above this function)."
807
+ else
808
+ echo " ✅ standpoint leg: $val$quoted"
809
+ fi ;;
810
+ *) echo " ✅ standpoint leg: $val$quoted" ;;
811
+ esac ;;
812
+ not-applicable)
813
+ # §7: asserting non-applicability without naming what was checked is indistinguishable from
814
+ # UNKNOWN wearing a permissive label. Measured: 6 bare instances in this corpus.
815
+ if [ "${#reason}" -lt 20 ]; then
816
+ echo " ❌ FAIL — bare 'not-applicable' with no grounds on the same line."
817
+ echo " That is UNKNOWN wearing a permissive label. Name what you checked:"
818
+ echo " no named peer carries this surface AND it changes no consumer-visible behavior."
819
+ return 1
820
+ fi
821
+ echo " ✅ standpoint leg: not-applicable$quoted" ;;
822
+ DEGRADED_NO_TARGET_ACCESS*|DEGRADED_NOT_RUN*|UNKNOWN*)
823
+ if [ "${#reason}" -lt 20 ]; then
824
+ echo " ❌ FAIL — $val without substantive grounds on the same line."
825
+ echo " could-not / did-not / did-not-look are separate values on purpose; collapsing"
826
+ echo " them renders an unrun probe as a zero finding. Name what you probed."
827
+ return 1
828
+ fi
829
+ echo " ⚠️ standpoint leg: $val — recorded, not silent" ;;
830
+ *)
831
+ echo " ❌ FAIL — '$val' is not a member of the standpoint: enum."
832
+ echo " Closed enum: tier1 · tier1b(<h>) · tier2(<h>) · tier2b(<h>) · tier3(<h>) ·"
833
+ echo " not-applicable · DEGRADED_NO_TARGET_ACCESS · DEGRADED_NOT_RUN · UNKNOWN"
834
+ echo " Default-deny: an unrecognised value is UNDECIDABLE, never assumed benign."
835
+ return 1 ;;
836
+ esac
837
+ return 0
838
+ }
839
+
840
+ # ── thirdparty: typed value lane (2026-08-17) ────────────────────────────────
841
+ # Grammar: thirdparty: <VALUE>(<grounds>) · UNKNOWN takes no parenthetical
842
+ #
843
+ # WHAT THIS AXIS IS — and it has TWO halves, which is what made getting it wrong so easy.
844
+ # The 6-axis canon (fh_three_layer_canon.md:243) defines ⓓ by what it RECEIVES: «문제 + 남의
845
+ # 코드베이스», and it asks two questions:
846
+ # ① prior art — has this claimed-new thing already been solved outside? checked/none-found
847
+ # ② harness-level — how does my change look from ANOTHER HARNESS's repo and peer-review
848
+ # adversarial persona? (operator: put Sonnet in gstack, wear the gstack
849
+ # persona, review the qasp change — a cross-FAMILY, harness-scoped
850
+ # adversarial review. FH is the GOVERNOR: it creates the situation,
851
+ # observes, and judges. What the 4-axis gate verifies is the OPINION
852
+ # that third harness produced, judged LOCALLY — it does not put a gate
853
+ # on someone else's repo.)
854
+ # It is NOT "I talked to another session". The canonical spec
855
+ # (`.claude/rules/fh_4axis_gate.md`) has said so since the field shipped, and the evidence that
856
+ # created it was 6 presentation claims that turned out to have prior art (mutation testing,
857
+ # promptfoo/DeepEval, an official cross-family plugin, …).
858
+ #
859
+ # WHY NOW — the first recorded false values exist, and they are the author's own. Measured across
860
+ # the marker corpus 2026-08-17: 4 markers used the enum correctly; **2 wrote free prose about
861
+ # peer-session contact** and matched no enum member at all. That is the field-canon failure this
862
+ # repo already names — normalizing a harness term into a general concept ("third party" → "another
863
+ # session"). Presence was checked; the value was not; nothing caught it.
864
+ #
865
+ # SCOPE — channel, not judgment (CLAUDE.md §Mechanization Boundary). This asserts the RECORD is a
866
+ # member of the closed enum and names non-vacuous grounds. It never asserts the prior-art search
867
+ # was thorough, nor that what a third party told you is TRUE — 🟥 that second one is deliberate and
868
+ # load-bearing: the axis exists to surface information you could not predict, and the truth of what
869
+ # comes back is adjudicated by the OTHER axes (adversarial · grounding · first-real-use), which is
870
+ # what the 4-axis gate is for. A wrong answer from a third party is that party's defect, possibly
871
+ # one to go fix; it is not something this field should be asked to certify.
872
+ #
873
+ # TRIGGER — validate IF PRESENT. Presence stays pointer-driven (`ⓓ=→thirdparty`), deliberately:
874
+ # requiring the field on every load-bearing change would force a prior-art search per commit, and
875
+ # over-blocking trains the override that disarms the Destructive-Op gate in this same hook.
876
+ #
877
+ # Calibration before wiring: the corpus supplied BOTH arms — peer-authored markers 4/4 PASS,
878
+ # author's own malformed 2/2 BLOCK, plus 6 synthetic shapes, 0 over-blocks. An earlier draft DID
879
+ # over-block 3 legitimate markers by looking for the closing paren on the first line when the
880
+ # grounds wrap across lines; the corpus caught it. Anchor: scripts/test_marker_thirdparty_lanes.sh
881
+ validate_thirdparty_leg() { # $1 = marker path
882
+ local m="$1" line val reason n
883
+ n=$(grep -cE '^[[:space:]]*thirdparty:' "$m" 2>/dev/null); n=${n:-0}
884
+ [ "$n" -eq 0 ] && return 0 # absent is governed by the ⓓ pointer check above, not here
885
+ if [ "$n" -gt 1 ]; then
886
+ echo " ❌ FAIL — marker carries MORE THAN ONE 'thirdparty:' line."
887
+ grep -nE '^[[:space:]]*thirdparty:' "$m" | sed 's/^/ /'
888
+ echo " Readers take the first, so an appended correction is silently shadowed."
889
+ return 1
890
+ fi
891
+ line=$(grep -m1 -E '^[[:space:]]*thirdparty:' "$m" | sed -E 's/^[[:space:]]*thirdparty:[[:space:]]*//')
892
+ val=$(printf '%s' "$line" | sed -E 's/\(.*$//' | sed -E 's/[[:space:]]*(—|--).*$//' | sed -E 's/[[:space:]]+$//')
893
+ # Cross-axis contamination — standpoint's tiers and crossfamily's tokens are different axes.
894
+ case "$val" in
895
+ tier1|tier1b|tier2|tier2b|tier3|panel|declined|DEGRADED_SINGLE_FAMILY|DEGRADED_PANEL_UNUSED|DEGRADED_NO_TARGET_ACCESS)
896
+ echo " ❌ FAIL — '$val' belongs to another axis, not thirdparty:."
897
+ echo " standpoint = whose repo was ground truth · crossfamily = which model family reviewed"
898
+ echo " thirdparty = was this claimed-new thing already done OUTSIDE this repo."
899
+ return 1 ;;
900
+ esac
901
+ case "$val" in
902
+ UNKNOWN)
903
+ echo " ⚠️ thirdparty leg: UNKNOWN — 안 봤다. Recorded, not silent." ;;
904
+ peer-review|checked|none-found|not-applicable|DEGRADED_NO_ACCESS|DEGRADED_NOT_RUN)
905
+ # The parenthetical may wrap across lines; only require that it OPENS with content after it.
906
+ case "$line" in *\(*) ;; *)
907
+ echo " ❌ FAIL — '$val' without a (<grounds>) parenthetical."
908
+ echo " none-found without naming WHAT you searched is indistinguishable from not looking."
909
+ return 1 ;;
910
+ esac
911
+ reason=$(printf '%s' "$line" | sed -E 's/^[^(]*\(//')
912
+ if [ "${#reason}" -lt 12 ]; then
913
+ echo " ❌ FAIL — '$val(...)' grounds are vacuous. Name what was searched and what came back."
914
+ return 1
915
+ fi
916
+ echo " ✅ thirdparty leg: $val" ;;
917
+ *)
918
+ echo " ❌ FAIL — '$val' is not a member of the thirdparty: enum."
919
+ echo " Closed enum: peer-review(<harness>/<persona> -> <target>, <what came back>) ·"
920
+ echo " checked(<searched/found>) · none-found(<searched>) ·"
921
+ echo " DEGRADED_NO_ACCESS(<why>) · DEGRADED_NOT_RUN(<why>) · UNKNOWN · not-applicable(<why>)"
922
+ echo " 🟥 ⓓ3자대면 = third-party PRIOR ART, not 'I talked to another session'. The two"
923
+ echo " markers that made this lane necessary both made exactly that substitution."
924
+ return 1 ;;
925
+ esac
926
+ return 0
927
+ }
928
+
929
+
930
+
720
931
  # ── 4축 자기 대조 — «어느 축을 돌렸고 어느 축을 안 돌렸나» 형식 검사 ────────────
721
932
  # CLAUDE.md §3층 자기 대조가 마커에 3줄을 요구한다. 그중 **기계로 볼 수 있는 두 줄**만 여기서
722
933
  # 강제한다. 나머지(①영혼을 «설계 전에» 썼는가)는 **원리적으로 확인 불가**라 안 넣는다 —
@@ -1303,6 +1514,8 @@ if [ -n "$LOADBEARING" ]; then
1303
1514
  fi
1304
1515
  MARKER_LB="$REPO_ROOT/tracks/_meta/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1305
1516
  if validate_crossfamily_leg "$MARKER_LB"; then :; else FAILED=1; fi
1517
+ if validate_standpoint_leg "$MARKER_LB"; then :; else FAILED=1; fi
1518
+ if validate_thirdparty_leg "$MARKER_LB"; then :; else FAILED=1; fi
1306
1519
  fi
1307
1520
 
1308
1521
  # ── staged-blob materializer (advisory linters) ───────────────────────────────
@@ -1411,6 +1624,35 @@ if [ -f "$PL_LINT" ]; then
1411
1624
  fi
1412
1625
  fi
1413
1626
 
1627
+ # ── Pre-ship read reminder (advisory) ─────────────────────────────────────────
1628
+ # WHY (2026-08-17, ⓓ3자대면 첫 실사용의 답습분). A third harness (gstack), wearing its own
1629
+ # `/plan-devex-review` persona, reviewed a peer harness's change and asked a question neither FH
1630
+ # nor that harness asks: **who opens this file, and what do they see in the first eight lines?**
1631
+ # Its finding: a human-facing report had been shipped to three surfaces where the body was 3 lines
1632
+ # under 21 lines of fixed template, the answer to "did it pass?" was buried in the 4th bullet, and
1633
+ # the last impression was "readers may skip this". The delta that shipped it was correct code.
1634
+ #
1635
+ # FH reached the same place independently the same day from a different axis — "readability is
1636
+ # measured only by rendering it or by a person; a static scan catches what is ABSENT, never what is
1637
+ # UNREADABLE", with 12 findings and 0 caught by static scanners. **Two independent arrivals** is
1638
+ # why this was the one item the internalization filter accepted out of four
1639
+ # (`harness_incubator_doctrine.md` §4-c).
1640
+ #
1641
+ # 🟥 WHAT THIS IS NOT. It does not judge readability, and it never blocks. Judging "is this
1642
+ # readable" is exactly the frozen-judgment class §Mechanization Boundary reserves for people, and a
1643
+ # vocabulary test for "human-facing document" over-blocked a legitimate case the same day it was
1644
+ # first tried. The trigger is therefore a PATH SET (what actually ships to readers), not a
1645
+ # vocabulary guess, and its whole output is one line of salience.
1646
+ _PRESHIP=$(printf '%s\n' "$STAGED" | grep -E '^(README([._a-z]*)?\.md|CHEATSHEET\.md|CATALOG\.md|docs/[^/]*\.md|templates/[^/]*\.md)$' || true)
1647
+ if [ -n "$_PRESHIP" ]; then
1648
+ echo "[Gate] reader-facing surface staged — pre-ship read reminder (advisory)..."
1649
+ printf '%s\n' "$_PRESHIP" | sed 's/^/ /'
1650
+ echo " ⚠️ 이 파일들은 «사람이 연다». 나가기 전에 한 번 «독자로서» 읽었나?"
1651
+ echo " 첫 8줄에 결론이 있나 · 본문이 템플릿에 안 덮이나 · 마지막 인상이 무엇인가"
1652
+ echo " 렌즈가 이미 있다: /sim-conductor A-1 (beginner cold-read) 또는 직접 렌더해서 읽기"
1653
+ echo " (advisory — 막지 않는다. 「읽었는가」는 이 훅이 보증할 수 없고 보증하려 들지도 않는다.)"
1654
+ fi
1655
+
1414
1656
  # ── Novelty-claim ledger (advisory) ───────────────────────────────────────────
1415
1657
  # "Ask the world before asserting novelty" is an INTENT trigger — un-hookable, like the
1416
1658
  # 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