@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.
- package/.claude/rules/fh_4axis_gate.md +25 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +51 -6
- package/README.ja.md +14 -2
- package/README.ko.md +14 -1
- package/README.md +16 -2
- package/README.zh.md +12 -1
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +231 -7
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +24 -0
- package/package.json +6 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +57 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
- package/scripts/adapters/mate_agent_boundary.sh +27 -7
- package/scripts/capability_registry_check.sh +69 -3
- package/scripts/relay_channel.sh +151 -6
- package/scripts/selfcheck.sh +18 -0
- package/scripts/target_freeze.sh +239 -0
- package/scripts/test_adapter_lanes.sh +17 -0
- package/scripts/test_marker_standpoint_lanes.sh +83 -0
- package/scripts/test_marker_thirdparty_lanes.sh +73 -0
- package/scripts/test_regression_guard_ci_lanes.sh +170 -0
- package/scripts/test_relay_channel_lanes.sh +146 -17
- package/scripts/test_target_freeze_lanes.sh +203 -0
- package/templates/.git-hooks/pre-commit +242 -0
- package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
- 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
|
-
|
|
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 $
|
|
97
|
+
echo "ERROR: cannot compute merge-base for $_HEAD_RESOLVED vs $_BASE_RESOLVED" >&2
|
|
70
98
|
exit 3
|
|
71
99
|
fi
|
|
72
|
-
HEAD_REF="$
|
|
73
|
-
echo "PR MODE: merge-base=$(git rev-parse --short "$BASE_REF")
|
|
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
|