@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.
Files changed (47) hide show
  1. package/.claude/judgment_circuits.txt +14 -0
  2. package/.claude/rules/fh_4axis_gate.md +7 -0
  3. package/.claude-plugin/marketplace.json +2 -2
  4. package/AGENTS.md +25 -0
  5. package/CLAUDE.md +202 -12
  6. package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
  7. package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
  8. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
  9. package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
  10. package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
  11. package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
  12. package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
  13. package/package.json +23 -2
  14. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  16. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
  17. package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
  18. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
  19. package/scripts/branch_claim.sh +266 -0
  20. package/scripts/chamber_run.sh +64 -2
  21. package/scripts/chamber_witness.sh +439 -0
  22. package/scripts/compaction_probe.sh +456 -0
  23. package/scripts/digest_landing_check.sh +385 -0
  24. package/scripts/directional_diff_gate.sh +459 -0
  25. package/scripts/fh_env_delta_scan.sh +15 -0
  26. package/scripts/fh_session_load.sh +34 -1
  27. package/scripts/field_canon_preload.sh +129 -0
  28. package/scripts/hook_source_lib.sh +41 -0
  29. package/scripts/judgment_circuit_lint.sh +239 -0
  30. package/scripts/novelty_claim_check.sh +193 -0
  31. package/scripts/relay_channel.sh +645 -0
  32. package/scripts/reviewer_capability_corpus.tsv +124 -0
  33. package/scripts/selfcheck.sh +106 -0
  34. package/scripts/session_close_check.sh +134 -1
  35. package/scripts/test_branch_claim_lanes.sh +231 -0
  36. package/scripts/test_dispatch_log_lanes.sh +35 -1
  37. package/scripts/test_field_canon_lanes.sh +142 -0
  38. package/scripts/test_hook_source_gate_lanes.sh +81 -0
  39. package/scripts/test_marker_crossfamily_lanes.sh +132 -0
  40. package/scripts/test_marker_floor_lanes.sh +9 -8
  41. package/scripts/test_relay_channel_lanes.sh +583 -0
  42. package/scripts/test_reviewer_capability_conformance.sh +173 -0
  43. package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
  44. package/scripts/utterance_landing_check.sh +209 -0
  45. package/templates/.git-hooks/pre-commit +297 -13
  46. package/templates/settings.Compaction.snippet.json +56 -0
  47. 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 `none` with a reason. Same shape as the existing
805
- # below-floor-ack: the gate blocks on an unstated answer, never on the answer itself. Non-vacuity is
806
- # the marker's job; provenance is not (a fabricated line is the weekly-audit residual, as elsewhere).
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 [ -f "$MARKER_LB" ] && grep -qE '^[[:space:]]*crossfamily:[[:space:]]*[^[:space:]]' "$MARKER_LB"; then
827
- echo " ✅ cross-family leg recorded: $(grep -m1 -E '^[[:space:]]*crossfamily:' "$MARKER_LB" | cut -c1-90)"
828
- else
829
- echo " ❌ FAIL — load-bearing file staged with no 'crossfamily:' line in the Axes 2-3 marker."
830
- echo " Verdict/gate/irreversible-surface code shares the author's blind spot with a same-family"
831
- echo " reviewer. State the answer, whatever it is the gate blocks silence, not a 'none':"
832
- echo " crossfamily: codex/gpt-5.5 R1..R4, 16 findings, 15 fixed 1 refuted, CONVERGED"
833
- echo " crossfamily: none <why no different-family auditor was reachable/needed>"
834
- echo " Append one line to: $MARKER_LB"
835
- FAILED=1
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
+ }