@chrono-meta/fh-gate 1.4.89 → 1.4.91
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/judgment_circuits.txt +14 -0
- package/.claude/rules/fh_4axis_gate.md +7 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +25 -0
- package/CLAUDE.md +202 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
- package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
- package/package.json +23 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
- package/scripts/branch_claim.sh +266 -0
- package/scripts/chamber_run.sh +64 -2
- package/scripts/chamber_witness.sh +439 -0
- package/scripts/compaction_probe.sh +456 -0
- package/scripts/digest_landing_check.sh +385 -0
- package/scripts/directional_diff_gate.sh +459 -0
- package/scripts/fh_env_delta_scan.sh +15 -0
- package/scripts/fh_session_load.sh +34 -1
- package/scripts/field_canon_preload.sh +129 -0
- package/scripts/hook_source_lib.sh +41 -0
- package/scripts/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +106 -0
- package/scripts/session_close_check.sh +134 -1
- package/scripts/test_branch_claim_lanes.sh +231 -0
- package/scripts/test_dispatch_log_lanes.sh +35 -1
- package/scripts/test_field_canon_lanes.sh +142 -0
- package/scripts/test_hook_source_gate_lanes.sh +81 -0
- package/scripts/test_marker_crossfamily_lanes.sh +132 -0
- package/scripts/test_marker_floor_lanes.sh +9 -8
- package/scripts/test_relay_channel_lanes.sh +583 -0
- package/scripts/test_reviewer_capability_conformance.sh +173 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/utterance_landing_check.sh +209 -0
- package/templates/.git-hooks/pre-commit +297 -13
- package/templates/settings.Compaction.snippet.json +56 -0
- package/templates/settings.FieldCanon.snippet.json +51 -0
|
@@ -188,6 +188,40 @@ LIGHT=$(echo "$LIGHT" | grep -vE '^\s*$' || true)
|
|
|
188
188
|
# boundary, so they run on EVERY commit regardless of what is staged.
|
|
189
189
|
# Body is intentionally NOT indented: it contains heredocs whose terminators must sit at column 0.
|
|
190
190
|
run_universal_guards() {
|
|
191
|
+
# ── Branch-claim guard — «내가 믿는 브랜치 ≠ HEAD» (2026-08-09) ───────────────
|
|
192
|
+
# WHY HERE, WITH THE OTHER UNIVERSAL GUARDS: surface-scoped, not 4-axis-scoped — a commit
|
|
193
|
+
# lands on a branch whether or not an FH asset is staged, so it must run before the
|
|
194
|
+
# "no FH assets → exit 0" path.
|
|
195
|
+
#
|
|
196
|
+
# WHAT IT CATCHES (measured 2026-08-09, two parallel sessions, one checkout):
|
|
197
|
+
# `.git/HEAD` is one per working tree; session A's `git switch` moves B's ground too.
|
|
198
|
+
# B had already "cut a branch at the start of work" — that did not prevent it.
|
|
199
|
+
# Key insight: the judgment key is the SESSION, not the tree. A tree-level claim is
|
|
200
|
+
# defeated when BOTH sessions follow the protocol (A switches + re-claims → claim==HEAD →
|
|
201
|
+
# B passes) — i.e. it gets weaker as adoption rises. Per-session records
|
|
202
|
+
# (.git/fh-claims/<session_id>, keyed on CLAUDE_CODE_SESSION_ID which git hooks inherit)
|
|
203
|
+
# compare only MY record against HEAD, so peers can never over-block me.
|
|
204
|
+
#
|
|
205
|
+
# NOT A ROOT FIX: it does not block `git switch` (git has no switch hook) and does not
|
|
206
|
+
# protect the uncommitted worktree. Root fix = one worktree per session.
|
|
207
|
+
#
|
|
208
|
+
# DEGRADE: no record · unparseable · detached/rebase/bisect/merge · no LIVE peer session
|
|
209
|
+
# → PASS. Sole rationale: over-blocking trains the override and kills the gate.
|
|
210
|
+
# (Deliberately NOT "a commit is reversible" — this incident's recovery went through a
|
|
211
|
+
# shared-branch history rewrite, which that invariant classes fail-closed.)
|
|
212
|
+
# Anchor: scripts/test_branch_claim_lanes.sh — includes the S-1 regression lane (both
|
|
213
|
+
# sessions protocol-compliant) and controls proving the pass-lanes are not unconditional.
|
|
214
|
+
BCLAIM="$REPO_ROOT/scripts/branch_claim.sh"
|
|
215
|
+
if [ -f "$BCLAIM" ]; then
|
|
216
|
+
if ! bash "$BCLAIM" check; then
|
|
217
|
+
FAILED=1
|
|
218
|
+
fi
|
|
219
|
+
else
|
|
220
|
+
# 부재를 통과로 렌더하지 않는다 — 이웃 가드(PSA_LIB)는 부재 시 FAIL 인데 여기만 무음이면
|
|
221
|
+
# «가드가 안 돌았다»와 «가드가 통과했다»가 구분 불가해진다. 차단은 안 한다(과차단 회피).
|
|
222
|
+
echo " ⚠️ branch-claim guard NOT INSTALLED (scripts/branch_claim.sh) — skipped, not passed"
|
|
223
|
+
fi
|
|
224
|
+
|
|
191
225
|
# ── Privacy guard — tracks/ staged-path allowlist (structural, name-free) ─────
|
|
192
226
|
# tracks/** is local-by-default (frozen-seed policy); the only public lanes are
|
|
193
227
|
# tracks/_contrib/** (consent lane) and ALREADY-TRACKED .gitkeep skeleton files.
|
|
@@ -338,6 +372,16 @@ else
|
|
|
338
372
|
GATE_MODE="lightweight"
|
|
339
373
|
fi
|
|
340
374
|
|
|
375
|
+
# TOCTOU 완화: 앞선 branch-claim check 이후 게이트들이 도는 동안 peer 가 브랜치를 옮길 수
|
|
376
|
+
# 있다. 최종 판정 직전에 한 번 더 돌려 창을 좁힌다. (원자적 close 는 reference-transaction
|
|
377
|
+
# 훅의 몫 — 이건 완화이지 해결이 아니다.)
|
|
378
|
+
if [ -f "$REPO_ROOT/scripts/branch_claim.sh" ]; then
|
|
379
|
+
if ! bash "$REPO_ROOT/scripts/branch_claim.sh" check; then
|
|
380
|
+
echo " ↑ 게이트 실행 중에 브랜치가 옮겨졌다 (TOCTOU 창에서 포착)"
|
|
381
|
+
FAILED=1
|
|
382
|
+
fi
|
|
383
|
+
fi
|
|
384
|
+
|
|
341
385
|
echo ""
|
|
342
386
|
echo "══════════════════════════════════════════════"
|
|
343
387
|
echo " FH 4-Axis Gate — ${GATE_MODE} mode"
|
|
@@ -469,6 +513,144 @@ marker_recreate_hint() {
|
|
|
469
513
|
echo " printf 'axis2-engine: quench-challenger\\naxis2-model: opus\\nfloor-status: at-floor\\naxis2-evidence: PASS no-S\\n' > \"$1\""
|
|
470
514
|
}
|
|
471
515
|
|
|
516
|
+
# ── Cross-family leg — typed verdict (see the load-bearing block below for WHY) ──
|
|
517
|
+
# Grammar: crossfamily: <VALUE>[ — <reason>]
|
|
518
|
+
# VALUE is a closed enum; the three degrade values additionally require a non-vacuous reason
|
|
519
|
+
# on the SAME line (keeping the established one-line marker convention rather than adding a
|
|
520
|
+
# second field). Returns 0 = accepted, 1 = block.
|
|
521
|
+
validate_crossfamily_leg() { # $1 = marker path
|
|
522
|
+
local m="$1" line val reason fams
|
|
523
|
+
if [ ! -f "$m" ] || ! grep -qE '^[[:space:]]*crossfamily:[[:space:]]*[^[:space:]]' "$m"; then
|
|
524
|
+
echo " ❌ FAIL — load-bearing file staged with no 'crossfamily:' line in the Axes 2-3 marker."
|
|
525
|
+
echo " Verdict/gate/irreversible-surface code shares the author's blind spot with a"
|
|
526
|
+
echo " same-family reviewer. State the answer, whatever it is — the gate blocks silence:"
|
|
527
|
+
echo " crossfamily: panel(codex, gemini) — R1..R4, 16 findings, 15 fixed 1 refuted"
|
|
528
|
+
echo " crossfamily: DEGRADED_SINGLE_FAMILY — probed codex/agy/gemini, 0 capable reachable"
|
|
529
|
+
echo " crossfamily: DEGRADED_PANEL_UNUSED — codex+agy reachable, not recruited (<why>)"
|
|
530
|
+
echo " crossfamily: UNKNOWN — panel not probed this round (<why>)"
|
|
531
|
+
echo " crossfamily: declined — operator declined sidecars (UAP), chosen floor"
|
|
532
|
+
echo " Append one line to: $m"
|
|
533
|
+
return 1
|
|
534
|
+
fi
|
|
535
|
+
# Duplicate guard (cross-family finding, codex 2026-08-08): a marker is an APPEND-ONLY
|
|
536
|
+
# file in practice, and `grep -m1` takes the FIRST match — so a later corrected/degraded
|
|
537
|
+
# value is shadowed by a stale permissive one and the lane reads the wrong verdict. Two
|
|
538
|
+
# crossfamily lines is an ambiguous marker, not a resolvable one; fail closed rather than
|
|
539
|
+
# pick a winner, because either choice silently discards a stated verdict.
|
|
540
|
+
if [ "$(grep -cE '^[[:space:]]*crossfamily:' "$m")" -gt 1 ]; then
|
|
541
|
+
echo " ❌ FAIL — marker carries MORE THAN ONE 'crossfamily:' line."
|
|
542
|
+
grep -nE '^[[:space:]]*crossfamily:' "$m" | sed 's/^/ /'
|
|
543
|
+
echo " A later line does not supersede an earlier one here — the reader takes the"
|
|
544
|
+
echo " first, so an appended correction would be silently shadowed. Delete the stale"
|
|
545
|
+
echo " line and leave exactly one."
|
|
546
|
+
return 1
|
|
547
|
+
fi
|
|
548
|
+
line=$(grep -m1 -E '^[[:space:]]*crossfamily:' "$m" \
|
|
549
|
+
| sed -E 's/^[[:space:]]*crossfamily:[[:space:]]*//')
|
|
550
|
+
# Split on the em-dash separator, NOT on first whitespace: a family list legitimately
|
|
551
|
+
# contains ", " (`panel(codex, gemini)`), and splitting on whitespace truncated it to
|
|
552
|
+
# `panel(codex,` — a well-formed panel rejected as malformed. An over-block on the
|
|
553
|
+
# correct shape is the failure that teaches --no-verify, so it is a defect, not strictness.
|
|
554
|
+
val=$(printf '%s' "$line" | sed -E 's/[[:space:]]*(—|--).*$//' | sed -E 's/[[:space:]]+$//')
|
|
555
|
+
reason=$(printf '%s' "$line" | sed -E 's/^[^—-]*(—|--)[[:space:]]*//')
|
|
556
|
+
[ "$reason" = "$line" ] && reason=""
|
|
557
|
+
|
|
558
|
+
case "$val" in
|
|
559
|
+
declined)
|
|
560
|
+
echo " ✅ cross-family leg: declined (operator-chosen floor, first-class — not a degrade)" ;;
|
|
561
|
+
panel*)
|
|
562
|
+
if ! printf '%s' "$val" | grep -qE '^panel\([^)]+\)$'; then
|
|
563
|
+
echo " ❌ FAIL — panel claim without a family list: '$val'"
|
|
564
|
+
echo " Write panel(<families>), e.g. panel(codex, gemini). Source the list from"
|
|
565
|
+
echo " scripts/sidecar_calibrate.sh's PANEL line rather than recall — that line"
|
|
566
|
+
echo " records a pin verified for THIS run."
|
|
567
|
+
return 1
|
|
568
|
+
fi
|
|
569
|
+
fams=$(printf '%s' "$val" | sed -E 's/^panel\(//; s/\)$//')
|
|
570
|
+
# (a) AUTHOR-FAMILY EXCLUSION. The point of this field is decorrelation; a panel of
|
|
571
|
+
# the author's own family is a same-family pass wearing a panel label — the exact
|
|
572
|
+
# substitution the load-bearing block exists to forbid. FH's governor is Claude.
|
|
573
|
+
if printf '%s' "$fams" | grep -qiE '(^|[,[:space:]])(claude|opus|sonnet|haiku|fable)'; then
|
|
574
|
+
echo " ❌ FAIL — panel names the AUTHOR'S OWN family: '$fams'"
|
|
575
|
+
echo " A Claude-family auditor shares the governor's blind spots, so it is not"
|
|
576
|
+
echo " decorrelation. If that is genuinely all that was reachable, the honest value"
|
|
577
|
+
echo " is DEGRADED_SINGLE_FAMILY with a reason — not a panel claim."
|
|
578
|
+
return 1
|
|
579
|
+
fi
|
|
580
|
+
# (b) INELIGIBLE-FIRST. Tokens overlap: GLM-OCR matches both `ocr` and `glm`,
|
|
581
|
+
# Qwen3-Embedding matches both `embed` and `qwen`. Test capability-eligibility first
|
|
582
|
+
# and both pass silently (pmh-dev #41 design decision 3, anchored there and here).
|
|
583
|
+
if printf '%s' "$fams" | grep -qiE 'embed|rerank|[^a-z]rank|ocr|safeguard|moderat|classif|whisper|dall-e|-rm([^a-z]|$)|bge-|voyage|gte-|minilm'; then
|
|
584
|
+
echo " ❌ FAIL — panel names a review-INCAPABLE member: '$fams'"
|
|
585
|
+
echo " Embedding / reranker / reward-model / moderation / OCR / speech classes emit"
|
|
586
|
+
echo " vectors, scores or labels — they cannot return an adversarial finding, so"
|
|
587
|
+
echo " counting them inflates BOTH panel size and family-diversity. If nothing capable"
|
|
588
|
+
echo " remains, the honest value is DEGRADED_SINGLE_FAMILY with a reason."
|
|
589
|
+
return 1
|
|
590
|
+
fi
|
|
591
|
+
# (c) DEFAULT DENY on unknown families (pmh-dev #41 design decision 1: unmeasured is
|
|
592
|
+
# not eligible). A denylist alone cannot know `voyage-3` or `armorm` are not reviewers,
|
|
593
|
+
# so an unrecognised token is UNDECIDABLE, never assumed capable. Matching is by FAMILY
|
|
594
|
+
# PATTERN, not id enumeration (#41 decision 2), so a new build of a known family passes
|
|
595
|
+
# without rewiring — which is the reason the CLI route was chosen at all.
|
|
596
|
+
local unknown="" tok
|
|
597
|
+
for tok in $(printf '%s' "$fams" | tr ',' ' '); do
|
|
598
|
+
printf '%s' "$tok" | grep -qiE 'codex|gpt|openai|o[0-9]|gemini|agy|antigravity|glm|qwen|deepseek|mistral|llama|grok|command-|cohere|kimi|copilot|ollama|phi-|yi-' \
|
|
599
|
+
|| unknown="${unknown:+$unknown }$tok"
|
|
600
|
+
done
|
|
601
|
+
if [ -n "$unknown" ]; then
|
|
602
|
+
echo " ❌ FAIL — panel names unrecognised family/families: '$unknown'"
|
|
603
|
+
echo " Default-deny: an unrecognised token is UNDECIDABLE, not capable — that is how"
|
|
604
|
+
echo " 'panel(none)' and 'panel(unprobed)' would otherwise launder a non-run into a"
|
|
605
|
+
echo " pass. Either use the family pattern this runtime belongs to, or add the"
|
|
606
|
+
echo " pattern here in the same commit that first uses it."
|
|
607
|
+
return 1
|
|
608
|
+
fi
|
|
609
|
+
echo " ✅ cross-family leg: $val${reason:+ — $(printf '%s' "$reason" | cut -c1-60)}" ;;
|
|
610
|
+
DEGRADED_SINGLE_FAMILY|DEGRADED_PANEL_UNUSED|UNKNOWN)
|
|
611
|
+
# A degrade is allowed; being silent about WHICH degrade, or about its grounds, is not.
|
|
612
|
+
# Two conditions, because either alone is porous: enough substance to be a statement
|
|
613
|
+
# (length) AND a token naming what was actually looked at.
|
|
614
|
+
if [ "${#reason}" -lt 20 ] \
|
|
615
|
+
|| ! printf '%s' "$reason" | grep -qiE 'prob(e|ed|ing)|reachab|unreach|not reachable|0 |none reachable|empty|absent|declin|consent|codex|gemini|agy|copilot|gateway|cli|panel|scope|budget|offline'; then
|
|
616
|
+
echo " ❌ FAIL — $val without substantive grounds on the same line."
|
|
617
|
+
case "$val" in
|
|
618
|
+
UNKNOWN) echo " UNKNOWN = the panel was never probed. Undecidable, not zero." ;;
|
|
619
|
+
DEGRADED_SINGLE_FAMILY) echo " = probed, nothing CAPABLE reachable (a constraint)." ;;
|
|
620
|
+
DEGRADED_PANEL_UNUSED) echo " = a capable panel WAS reachable and was not recruited (a choice)." ;;
|
|
621
|
+
esac
|
|
622
|
+
echo " Name what you probed and what came back — the defect this field exists for"
|
|
623
|
+
echo " was a line that announced a state without naming its grounds:"
|
|
624
|
+
echo " crossfamily: $val — probed codex/agy/gemini, <what came back>"
|
|
625
|
+
return 1
|
|
626
|
+
fi
|
|
627
|
+
echo " ⚠️ cross-family leg: $val — recorded, not silent (weekly audit re-queues these)"
|
|
628
|
+
echo " $(printf '%s' "$reason" | cut -c1-80)" ;;
|
|
629
|
+
single-family|none|none-this-round|N/A|n/a)
|
|
630
|
+
# `single-family` is deliberately NOT accepted HERE. This block runs only on
|
|
631
|
+
# load-bearing changes, where "decorrelation not required" is a contradiction — and
|
|
632
|
+
# as a no-ack, no-warning pass it was a free bypass of the whole lane (any degrade
|
|
633
|
+
# could take this exit instead of DEGRADED_*). `none` is worse: it merges three states.
|
|
634
|
+
echo " ❌ FAIL — '$val' is not an accepted value on a LOAD-BEARING change."
|
|
635
|
+
echo " This block only runs when gate/verdict/irreversible-surface code is staged, so"
|
|
636
|
+
echo " 'decorrelation not required' does not apply — and a no-ack pass here would let"
|
|
637
|
+
echo " any degrade take this exit instead of naming itself. Say which state holds:"
|
|
638
|
+
echo " DEGRADED_SINGLE_FAMILY probed, nothing capable reachable (could not)"
|
|
639
|
+
echo " DEGRADED_PANEL_UNUSED panel reachable, not recruited (did not)"
|
|
640
|
+
echo " UNKNOWN never probed (did not look)"
|
|
641
|
+
echo " declined operator declined sidecars (chosen floor)"
|
|
642
|
+
return 1 ;;
|
|
643
|
+
*)
|
|
644
|
+
echo " ❌ FAIL — invalid crossfamily value: '$val'"
|
|
645
|
+
echo " Allowed: panel(<families>) | declined |"
|
|
646
|
+
echo " DEGRADED_SINGLE_FAMILY | DEGRADED_PANEL_UNUSED | UNKNOWN"
|
|
647
|
+
echo " Free prose is rejected on purpose: a prose verdict cannot be consumed by a gate,"
|
|
648
|
+
echo " and a later session cites it as though it were measured. That has happened."
|
|
649
|
+
return 1 ;;
|
|
650
|
+
esac
|
|
651
|
+
return 0
|
|
652
|
+
}
|
|
653
|
+
|
|
472
654
|
validate_marker_floor() {
|
|
473
655
|
local m="$1" fs model
|
|
474
656
|
# Leading whitespace tolerated on field lines (defense-in-depth vs indented writes).
|
|
@@ -801,9 +983,28 @@ fi
|
|
|
801
983
|
# the Surface-Class Degrade Invariant the fail-closed leg of this gate belongs at the MERGE boundary,
|
|
802
984
|
# not here; forcing a sidecar dispatch per commit would over-block and train the escape into reflex.
|
|
803
985
|
# It DOES forbid being SILENT about it. The marker must carry a `crossfamily:` line — either naming
|
|
804
|
-
# the engine and its verdict, or explicitly recording
|
|
805
|
-
# below-floor-ack: the gate blocks on an unstated answer, never on the answer itself.
|
|
806
|
-
# the marker's job; provenance is not (a fabricated line is the weekly-audit residual
|
|
986
|
+
# the engine and its verdict, or explicitly recording the degrade with a reason. Same shape as the
|
|
987
|
+
# existing below-floor-ack: the gate blocks on an unstated answer, never on the answer itself.
|
|
988
|
+
# Non-vacuity is the marker's job; provenance is not (a fabricated line is the weekly-audit residual).
|
|
989
|
+
#
|
|
990
|
+
# ── TYPED (2026-08-08) — why presence-only was not enough ─────────────────────────────────────
|
|
991
|
+
# The original check accepted any non-empty value, so the answer was FREE PROSE. That is how a
|
|
992
|
+
# false one got in: a sibling harness's marker recorded `crossfamily: none this round — 도달 불가`
|
|
993
|
+
# (unreachable), the claim was later found FALSE, and a subsequent session CITED it as grounds.
|
|
994
|
+
# A prose verdict laundered itself into evidence — the failure `[[feedback_typed_verdict_channel]]`
|
|
995
|
+
# names. Presence-checking catches silence; it cannot catch a confident wrong answer.
|
|
996
|
+
#
|
|
997
|
+
# The load-bearing split is that `none` is THREE different states, and merging them hides a
|
|
998
|
+
# different thing each time:
|
|
999
|
+
# DEGRADED_SINGLE_FAMILY probed, nothing CAPABLE reachable — could not
|
|
1000
|
+
# DEGRADED_PANEL_UNUSED panel reachable, not recruited — did not (a choice, not a limit)
|
|
1001
|
+
# UNKNOWN never probed — did not look
|
|
1002
|
+
# `UNKNOWN` collapsed into `none` renders an unrun probe as a zero finding (not-found-is-not-zero).
|
|
1003
|
+
# `DEGRADED_PANEL_UNUSED` collapsed into it renders an unused panel as an unavailable one — the
|
|
1004
|
+
# distinction a sibling harness stated about itself in its own words: "못 한 것이 아니라 안 한 것이다".
|
|
1005
|
+
#
|
|
1006
|
+
# Scope is unchanged: still load-bearing-only, still reversible-surface (blocks the unstated answer,
|
|
1007
|
+
# never demands a dispatch). Only the ANSWER is now typed. Fixtures: scripts/test_marker_crossfamily_lanes.sh
|
|
807
1008
|
# The degrade lint runs as an ADVISORY pre-screen, per its own doctrine (FP-tolerant, never a solo block).
|
|
808
1009
|
LOADBEARING=$(printf '%s\n%s\n' "$GATE_IMPL" "$UGUARD_IMPL" | grep -v '^[[:space:]]*$' | sort -u || true)
|
|
809
1010
|
if [ -n "$LOADBEARING" ]; then
|
|
@@ -823,18 +1024,101 @@ if [ -n "$LOADBEARING" ]; then
|
|
|
823
1024
|
echo " ⚠️ degrade lint unavailable (scripts/degrade_direction_scan.sh missing) — advisory leg skipped"
|
|
824
1025
|
fi
|
|
825
1026
|
MARKER_LB="$REPO_ROOT/tracks/_meta/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
|
|
826
|
-
if
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1027
|
+
if validate_crossfamily_leg "$MARKER_LB"; then :; else FAILED=1; fi
|
|
1028
|
+
fi
|
|
1029
|
+
|
|
1030
|
+
# ── staged-blob materializer (advisory linters) ───────────────────────────────
|
|
1031
|
+
# 두 advisory 린터는 파일을 직접 grep/sed 한다. 워킹트리 경로를 넘기면 **커밋되는 내용이 아니라
|
|
1032
|
+
# 지금 디스크에 있는 내용**을 재게 된다 — 스테이징 후 워킹트리에서 고치면 훅은 깨끗하다 보고하고
|
|
1033
|
+
# 문제 있는 blob 이 커밋된다(high 리뷰 #6, 낙관 방향 fail-open). 이 훅의 다른 슬라이스는 전부
|
|
1034
|
+
# `git diff --cached` / `git show :file` 로 추론한다. 여기만 예외였다.
|
|
1035
|
+
_stage_blob() { # $1=repo-relative path → echoes temp file holding the STAGED content
|
|
1036
|
+
local _p="$1" _t
|
|
1037
|
+
_t="$(mktemp "${TMPDIR:-/tmp}/fh_staged.XXXXXX")" || return 1
|
|
1038
|
+
if git -C "$REPO_ROOT" show ":$_p" > "$_t" 2>/dev/null; then printf '%s' "$_t"
|
|
1039
|
+
else rm -f "$_t"; return 1; fi
|
|
1040
|
+
}
|
|
1041
|
+
_STAGE_TMPS=()
|
|
1042
|
+
_STAGE_MAP=() # "tmpfile=repopath" — 보고를 사람이 읽을 수 있게 되돌리는 데 쓴다
|
|
1043
|
+
# 히트를 익명 mktemp 경로로 인쇄하면, 그 파일은 _stage_cleanup 이 곧 지워서 **읽는 순간 이미 없다**.
|
|
1044
|
+
# 대상이 둘 이상이면 어느 파일 얘긴지 귀속조차 안 된다. advisory 의 목적은 사람을 그 줄로 보내는 것이다.
|
|
1045
|
+
_unmap() { local _o="$1" _m; for _m in "${_STAGE_MAP[@]:-}"; do
|
|
1046
|
+
[ -n "$_m" ] && _o="${_o//${_m%%=*}/${_m#*=}}"; done; printf '%s' "$_o"; }
|
|
1047
|
+
_stage_cleanup() { [ "${#_STAGE_TMPS[@]}" -gt 0 ] && rm -f "${_STAGE_TMPS[@]}" 2>/dev/null; }
|
|
1048
|
+
|
|
1049
|
+
# ── Judgment-circuit lint (advisory — the PROMPT-layer sibling of the degrade lint above) ─────
|
|
1050
|
+
# Same proposition, different corpus: degrade_direction_scan reads .py/.sh verdict code and skips
|
|
1051
|
+
# .md by construction, so identity/circuit DOCUMENTS had no instrument at all. Measured basis
|
|
1052
|
+
# (105 runs, 2026-08-07~08): role-assignment ("너는 ~이다") is a net loss on the weak tier and
|
|
1053
|
+
# intensity orders ("꼼꼼히") multiply phantoms 3.1x — both are prose granting discretion with an
|
|
1054
|
+
# unconstrained degrade direction, the doctrine's prompt-layer form.
|
|
1055
|
+
#
|
|
1056
|
+
# Trigger is an EXPLICIT REGISTRY, not a guess: `.claude/judgment_circuits.txt` (one path per line).
|
|
1057
|
+
# Which documents are judgment circuits is a decision someone makes, not something a glob infers —
|
|
1058
|
+
# an inferred trigger here would fire on every .md and get itself disabled.
|
|
1059
|
+
# ADVISORY by construction: grep-heuristic, false positives expected, never blocks a commit.
|
|
1060
|
+
JC_REG="$REPO_ROOT/.claude/judgment_circuits.txt"
|
|
1061
|
+
JC_LINT="$REPO_ROOT/scripts/judgment_circuit_lint.sh"
|
|
1062
|
+
if [ -f "$JC_REG" ] && [ -f "$JC_LINT" ]; then
|
|
1063
|
+
# Array, not a space-joined string: a tracked path CAN contain a space, and the string form
|
|
1064
|
+
# silently splits `docs/first known.md` into two nonexistent args (cross-family, 2026-08-08).
|
|
1065
|
+
_jc_targets=()
|
|
1066
|
+
while IFS= read -r _jc_line; do
|
|
1067
|
+
case "$_jc_line" in ''|'#'*) continue ;; esac
|
|
1068
|
+
if printf '%s\n' "$STAGED" | grep -qxF "$_jc_line" 2>/dev/null; then
|
|
1069
|
+
_b="$(_stage_blob "$_jc_line")" && { _jc_targets+=("$_b"); _STAGE_TMPS+=("$_b"); _STAGE_MAP+=("$_b=$_jc_line"); }
|
|
1070
|
+
fi
|
|
1071
|
+
done < "$JC_REG"
|
|
1072
|
+
if [ "${#_jc_targets[@]}" -gt 0 ]; then
|
|
1073
|
+
echo "[Gate] registered judgment-circuit doc staged — circuit lint (advisory)..."
|
|
1074
|
+
_jc_out="$(bash "$JC_LINT" "${_jc_targets[@]}" 2>&1)"; _jc_rc=$?
|
|
1075
|
+
printf '%s\n' "$(_unmap "$_jc_out")" | sed 's/^/ /'
|
|
1076
|
+
echo " (경로는 스테이징 blob 을 레포 경로로 되돌려 표기했다 — 줄번호는 staged 내용 기준)"
|
|
1077
|
+
# exit 10 = the scanner's own HARNESS-ERROR (bad input / unreadable target). Neither PASS nor
|
|
1078
|
+
# FAIL — surfacing it is the whole point of having a typed exit; swallowing it would render a
|
|
1079
|
+
# dead instrument as a clean run.
|
|
1080
|
+
[ "$_jc_rc" = "10" ] && echo " 🟥 circuit lint HARNESS-ERROR (rc=10) — instrument did not measure"
|
|
1081
|
+
echo " (advisory — does not block. A hit means 'prove this is a circuit, not a persona'.)"
|
|
1082
|
+
fi
|
|
1083
|
+
fi
|
|
1084
|
+
|
|
1085
|
+
# ── Novelty-claim ledger (advisory) ───────────────────────────────────────────
|
|
1086
|
+
# "Ask the world before asserting novelty" is an INTENT trigger — un-hookable, like the
|
|
1087
|
+
# repo-go-public surface. So this does not force the asking; it makes NOT having asked visible
|
|
1088
|
+
# at commit time (shift-left ledger, not one more rule: the rule already existed and still failed).
|
|
1089
|
+
# Direction matters: defect claims (FAIL) have long required source-grounding while novelty/strength
|
|
1090
|
+
# claims (PASS) passed unchecked — the optimistic direction is the one that leaks. And an absence
|
|
1091
|
+
# claim is structurally worse: with nothing cited, phantom-detection cannot engage at all.
|
|
1092
|
+
# Scope = the same substantive doc surface the 4-axis carve-out already treats as claim-bearing.
|
|
1093
|
+
NC_CHECK="$REPO_ROOT/scripts/novelty_claim_check.sh"
|
|
1094
|
+
if [ -f "$NC_CHECK" ]; then
|
|
1095
|
+
_nc_targets=()
|
|
1096
|
+
while IFS= read -r _nc_line; do
|
|
1097
|
+
_b="$(_stage_blob "$_nc_line")" && { _nc_targets+=("$_b"); _STAGE_TMPS+=("$_b"); _STAGE_MAP+=("$_b=$_nc_line"); }
|
|
1098
|
+
done < <(printf '%s\n' "$STAGED" | grep -E '^(knowledge/|docs/).*\.md$' || true)
|
|
1099
|
+
if [ "${#_nc_targets[@]}" -gt 0 ]; then
|
|
1100
|
+
_nc_raw=$(bash "$NC_CHECK" "${_nc_targets[@]}" 2>&1); _nc_rc=$?
|
|
1101
|
+
# Same typed-exit rule as above: rc=10 is a dead instrument, not a clean corpus.
|
|
1102
|
+
[ "$_nc_rc" = "10" ] && echo " 🟥 novelty check HARNESS-ERROR (rc=10) — instrument did not measure"
|
|
1103
|
+
_nc_raw="$(_unmap "$_nc_raw")"
|
|
1104
|
+
# 부분 미스캔 경고를 **같이** 건진다. 호출부가 `🟥 무앵커` 만 grep 해서, 스크립트가 옳게 낸
|
|
1105
|
+
# "전수 스캔이 아니다" 를 통째로 버렸다 — 수리가 스크립트 안으로 옮겨졌을 뿐 이 표면에선
|
|
1106
|
+
# 미스캔 문서가 여전히 깨끗한 문서와 구분 불가였다(high 재리뷰 #5).
|
|
1107
|
+
_nc_warn=$(printf '%s\n' "$_nc_raw" | grep -F '전수 스캔이 아니다' | head -3 || true)
|
|
1108
|
+
if [ -n "$_nc_warn" ]; then
|
|
1109
|
+
echo "[Gate] novelty check: 부분 미스캔 (advisory):"
|
|
1110
|
+
printf '%s\n' "$_nc_warn" | sed 's/^/ /'
|
|
1111
|
+
echo " → 이 문서는 전수 스캔이 아니다. 제외된 줄에 실제 주장이 있을 수 있다."
|
|
1112
|
+
fi
|
|
1113
|
+
_nc_out=$(printf '%s\n' "$_nc_raw" | grep -E '🟥 무앵커' | head -5 || true)
|
|
1114
|
+
if [ -n "$_nc_out" ]; then
|
|
1115
|
+
echo "[Gate] novelty/absence claim without an external anchor (advisory):"
|
|
1116
|
+
printf '%s\n' "$_nc_out" | sed 's/^/ /'
|
|
1117
|
+
echo " Each = 'you asserted this about the world without asking it'. Does not block."
|
|
1118
|
+
fi
|
|
836
1119
|
fi
|
|
837
1120
|
fi
|
|
1121
|
+
_stage_cleanup
|
|
838
1122
|
|
|
839
1123
|
# SKILL.md ONLY here, deliberately (2026-07-26): the 07-26 sweep widened `SKILL\.md` →
|
|
840
1124
|
# `SKILL(_detail)?\.md` in the HEAVY and doc-coupling terms above, but NOT here. This slice asks
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": [
|
|
3
|
+
"Compaction hook snippet — TRACKED, so it arrives with a clone. Every .claude/settings*.json path",
|
|
4
|
+
"in this repo is gitignored (confirm with `git check-ignore -v .claude/settings.json`), so the hook",
|
|
5
|
+
"registration itself cannot be tracked. This is the tracked SOURCE that install-wizard merges into",
|
|
6
|
+
"the user's local settings. Without the merge step the hooks do not run — shipping the script is not",
|
|
7
|
+
"the same proposition as wiring it.",
|
|
8
|
+
"",
|
|
9
|
+
"What it does: seals a POINTER ledger of what the session holds just BEFORE compaction, then",
|
|
10
|
+
"re-injects it once on the next prompt. FH's structural advantage over generic compaction memory is",
|
|
11
|
+
"that the originals are already on disk — what compaction destroys is not the context but the",
|
|
12
|
+
"pointers to it, and a pointer is cheap to restore.",
|
|
13
|
+
"",
|
|
14
|
+
"Why the second hook is UserPromptSubmit and not PostCompact — this is a MEASURED constraint, not a",
|
|
15
|
+
"style choice: PostCompact does NOT support `additionalContext` (official hooks reference, verified",
|
|
16
|
+
"2026-08-08), so a PostCompact hook physically cannot put anything back into the session. It can only",
|
|
17
|
+
"write files. UserPromptSubmit is one of the three events whose exit-0 stdout becomes context Claude",
|
|
18
|
+
"can read. Wiring the re-injection to PostCompact would install a hook that runs and changes nothing.",
|
|
19
|
+
"",
|
|
20
|
+
"Both entries carry NO private path, so both belong in .claude/settings.json (project-local).",
|
|
21
|
+
"Merge, do not overwrite: preserve any PreCompact / UserPromptSubmit entries the user already has;",
|
|
22
|
+
"replace only the FH ones, keyed by script name.",
|
|
23
|
+
"",
|
|
24
|
+
"Never blocks: PreCompact CAN block compaction (exit 2) and this deliberately does not — compaction",
|
|
25
|
+
"is a reversible surface, and a gate that fires in the user's way is a gate that gets switched off.",
|
|
26
|
+
"The script always exits 0 for the same reason a non-zero hook exit discards its own stdout."
|
|
27
|
+
],
|
|
28
|
+
"project_settings_json": {
|
|
29
|
+
"hooks": {
|
|
30
|
+
"PreCompact": [
|
|
31
|
+
{
|
|
32
|
+
"matcher": "",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "bash -c 'HUB=\"${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}\"; bash \"$HUB/scripts/compaction_probe.sh\" seal; exit 0'",
|
|
37
|
+
"timeout": 15
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"UserPromptSubmit": [
|
|
43
|
+
{
|
|
44
|
+
"matcher": "",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "bash -c 'HUB=\"${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}\"; bash \"$HUB/scripts/compaction_probe.sh\" digest; exit 0'",
|
|
49
|
+
"timeout": 10
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": [
|
|
3
|
+
"field-canon 선적재 훅 스니펫 — TRACKED 라 clone 과 함께 도착한다. 이 레포의 모든",
|
|
4
|
+
".claude/settings*.json 경로는 gitignored 이므로(`git check-ignore -v .claude/settings.json` 으로",
|
|
5
|
+
"확인) **훅 등록 자체를 tracked 로 둘 수 없다.** 이것이 install-wizard 가 사용자 로컬 설정에",
|
|
6
|
+
"머지하는 tracked SOURCE 다. 머지 단계가 없으면 훅은 돌지 않는다 —",
|
|
7
|
+
"**스크립트를 출하하는 것과 배선하는 것은 같은 명제가 아니다.**",
|
|
8
|
+
"",
|
|
9
|
+
"이 파일이 왜 뒤늦게 생겼나 (2026-08-09 실측, 컨트롤 동반):",
|
|
10
|
+
" PR #299 가 scripts/field_canon_preload.sh 를 심으면서 스니펫도 files[] 등재도 안 했다.",
|
|
11
|
+
" npm pack --dry-run | grep field_canon → 0건",
|
|
12
|
+
" CONTROL 같은 명령 | grep compaction_probe → 1건 (계기 생존)",
|
|
13
|
+
" 즉 npm 사용자에게는 **스크립트도 배선도 아무것도 가지 않았다.** git clone 사용자는",
|
|
14
|
+
" 스크립트는 받지만 배선이 없어 훅이 안 돈다. 위 Compaction 스니펫의 _README 가 이미",
|
|
15
|
+
" 같은 문장을 적어놨는데 #299 가 그걸 안 따랐다 — 산문 규율의 한계이자, 이 스니펫이",
|
|
16
|
+
" 그 규율을 파일로 바꾸는 이유다.",
|
|
17
|
+
"",
|
|
18
|
+
"무엇을 하나: 프롬프트에 **매핑된 필드 하네스 이름**이 나오면 그 레포의 정본 경로",
|
|
19
|
+
"(README · CLAUDE.md · docs/governance/)를 **세션당 1회** 띄운다. 코드 역추론 전에 정본을",
|
|
20
|
+
"읽게 하는 것이 목적이다. 2026-08-09 실측: 한 세션이 종일 qasp 를 파면서 정본을 하나도",
|
|
21
|
+
"안 읽고 코드에서 역추론해 **네 번 정정당했고 자력 적발 0** 이었다 — 네 번 다 답이",
|
|
22
|
+
"정본에 있었고 그중 하나는 README 가 그 오해를 경고까지 하고 있었다.",
|
|
23
|
+
"",
|
|
24
|
+
"🟥 부재를 침묵으로 렌더하지 않는다. 초판 배선은 `[ -f \"$S\" ] && bash \"$S\"` 였는데,",
|
|
25
|
+
"그러면 **「이 install 에 파일이 없는 게 정상」과 「체크아웃이 옛날 컷 브랜치라 파일이",
|
|
26
|
+
"사라진 상태」가 구분되지 않는다.** 공유 체크아웃에서 후자가 실제로 반복 발생했고",
|
|
27
|
+
"(하루 6회 브랜치 이동), 그동안 훅은 exit 0 · 출력 0줄로 조용히 꺼져 있었다.",
|
|
28
|
+
"그래서 else 가지에서 **한 줄 경보**를 낸다 — 차단은 하지 않는다(UserPromptSubmit 은",
|
|
29
|
+
"차단 표면이 아니고, 사용자 앞을 막는 게이트는 꺼지는 게이트다).",
|
|
30
|
+
"",
|
|
31
|
+
"PRIVATE PATH 없음 → .claude/settings.json(프로젝트 로컬)에 속한다.",
|
|
32
|
+
"머지하되 덮어쓰지 마라: 사용자의 기존 UserPromptSubmit 엔트리를 보존하고 **스크립트",
|
|
33
|
+
"이름을 키로** FH 것만 교체해라."
|
|
34
|
+
],
|
|
35
|
+
"project_settings_json": {
|
|
36
|
+
"hooks": {
|
|
37
|
+
"UserPromptSubmit": [
|
|
38
|
+
{
|
|
39
|
+
"matcher": "",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "bash -c 'HUB=\"${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}\"; S=\"$HUB/scripts/field_canon_preload.sh\"; if [ -f \"$S\" ]; then bash \"$S\"; else echo \"\"; echo \"⚠️ [field-canon] 선적재 훅 스크립트가 없다 — 이 체크아웃이 옛날 컷 브랜치이거나 설치가 불완전할 수 있다.\"; echo \" 필드 하네스 정본 안내가 **꺼져 있다**. git log --oneline -1 로 브랜치를 확인해라.\"; fi; exit 0'",
|
|
44
|
+
"timeout": 10
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|