@chrono-meta/fh-gate 1.4.97 → 1.4.98

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 (38) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CATALOG.md +19 -0
  3. package/CHEATSHEET.md +9 -1
  4. package/CLAUDE.md +28 -2
  5. package/README.ja.md +10 -0
  6. package/README.ko.md +10 -0
  7. package/README.md +11 -0
  8. package/README.zh.md +10 -0
  9. package/docs/pillars.svg +3 -7
  10. package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
  11. package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
  12. package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
  13. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
  14. package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
  15. package/knowledge/shared/learnings/subagent_invocations_log.yaml +55 -0
  16. package/package.json +5 -1
  17. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  18. package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
  19. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  20. package/plugins/fh-meta/CHANGELOG.md +65 -0
  21. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
  22. package/scripts/degrade_direction_scan.sh +10 -1
  23. package/scripts/digest_landing_check.sh +20 -4
  24. package/scripts/fh_node_check.sh +60 -1
  25. package/scripts/fh_session_load.sh +22 -2
  26. package/scripts/frontier_digest_autopilot.sh +229 -0
  27. package/scripts/lane_runner_check.sh +237 -25
  28. package/scripts/package_coverage_check.sh +17 -0
  29. package/scripts/postinstall_notice.js +34 -0
  30. package/scripts/selfcheck.sh +111 -5
  31. package/scripts/test_degrade_scan_shell_probes.sh +75 -0
  32. package/scripts/test_field_canon_lanes.sh +29 -5
  33. package/scripts/test_lane_runner_lanes.sh +221 -0
  34. package/scripts/test_stale_clone_guard_lanes.sh +21 -7
  35. package/scripts/test_version_lockstep_lanes.sh +62 -0
  36. package/scripts/version_lockstep_check.sh +143 -1
  37. package/templates/.git-hooks/pre-commit +22 -1
  38. package/templates/degrade_direction_scan.sh +10 -1
@@ -413,7 +413,9 @@ for _pair in \
413
413
  "templates/.git-hooks/pre-commit|scripts/test_marker_crossfamily_lanes.sh" \
414
414
  "templates/.git-hooks/pre-commit|scripts/test_marker_floor_lanes.sh" \
415
415
  "scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
416
- "scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh"
416
+ "scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
417
+ "scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
418
+ "scripts/stale_clone_guard.sh|scripts/test_stale_clone_guard_lanes.sh"
417
419
  do
418
420
  _subj="${_pair%%|*}"; _anc="${_pair##*|}"; _lbl="${_anc##*/}"
419
421
  if [ ! -f "$_subj" ]; then
@@ -466,14 +468,24 @@ do
466
468
  fi
467
469
  done
468
470
 
469
- # embedded --self-test suites (compaction_probe · judgment_circuit_lint · novelty_claim_check).
470
- # These three carry their lanes INSIDE the script (`--self-test`) rather than in a sibling
471
- # test_*_lanes.sh, so the name-list wiring above skipped them silently: 48 lanes existed and ran
471
+ # embedded --self-test suites (compaction_probe · judgment_circuit_lint · novelty_claim_check ·
472
+ # chamber_witness · digest_landing_check).
473
+ # These carry their lanes INSIDE the script (`--self-test`) rather than in a sibling
474
+ # test_*_lanes.sh, so the name-list wiring above skipped them silently: lanes existed and ran
472
475
  # only when a human typed the command. That is built-but-not-wired applied to the anchors themselves
473
476
  # — a later edit that breaks a lane stays green everywhere the project actually checks
474
477
  # (high re-review 2026-08-08). Same shape as the block above: subject absent → SKIP, subject present
475
478
  # but self-test missing → FAIL, never a silent pass.
476
- for _subj in compaction_probe judgment_circuit_lint novelty_claim_check; do
479
+ # chamber_witness/digest_landing_check joined 2026-08-15 (DEBT closed — lane_runner_check.sh
480
+ # flagged 4 undeclared self-test subjects; capability_registry_check and relay_channel are the
481
+ # other two and do NOT join this loop, each for its own reason, see their own blocks below).
482
+ # capability_registry_check was tried here first and reverted: its passing-run output never
483
+ # contains 캘리브레이션 as a VERDICT — the only occurrence is inside a Korean *test-case title*
484
+ # ("M4: 캘리브레이션 쌍 미선언", capability_registry_check.sh's own self-test), so a future rename
485
+ # or trim of that one test name would flip a real PASS into "dispatcher missing?" here (cross-family
486
+ # review caught this, 2026-08-15). chamber_witness/digest_landing_check both print a genuine
487
+ # terminal "캘리브레이션 통과/실패" line, so they stay.
488
+ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check chamber_witness digest_landing_check; do
477
489
  if [ ! -f "scripts/$_subj.sh" ]; then
478
490
  _absent_subject_verdict "$_subj --self-test" "scripts/$_subj.sh" || fail=1
479
491
  else
@@ -500,6 +512,18 @@ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check; do
500
512
  # nothing reads the announcement. Fixed by deleting one word.
501
513
  _to=""; command -v timeout >/dev/null 2>&1 && _to="timeout 120"
502
514
  _st_out="$($_to bash "scripts/$_subj.sh" --self-test < /dev/null 2>&1)"; _st_rc=$?
515
+ # rc=10 (or the sibling harness-error codes) means the subject could not even set up its own
516
+ # known-pair fixture (e.g. mktemp failed) — it never got to run a lane, so it never printed
517
+ # 캘리브레이션 either way. Falling through to the *) arm below would report the confident but
518
+ # WRONG cause "dispatcher missing?" for an instrument that was never reached. chamber_witness.sh
519
+ # and digest_landing_check.sh both have real mktemp-failure → return 10 paths (cross-family
520
+ # review 2026-08-15); the two original loop members never did, so this arm is a no-op for them.
521
+ case "$_st_rc" in
522
+ 10|2|126|127)
523
+ echo "HARNESS ERROR $_subj --self-test: the suite exited $_st_rc — it could not measure,"
524
+ echo " so its verdicts prove nothing about $_subj. Not a lane failure, not a pass either."
525
+ fail=1; continue ;;
526
+ esac
503
527
  case "$_st_out" in
504
528
  *캘리브레이션*) : ;;
505
529
  *) echo "FAIL $_subj: --self-test produced no calibration verdict (dispatcher missing?)"
@@ -514,6 +538,50 @@ for _subj in compaction_probe judgment_circuit_lint novelty_claim_check; do
514
538
  fi
515
539
  done
516
540
 
541
+ # capability_registry_check — one of the 4 embedded --self-test subjects lane_runner_check.sh
542
+ # flagged (2026-08-15), kept out of the loop above (see that loop's comment) because its own
543
+ # terminal line is `통과 N · 실패 N`, never 캘리브레이션 — gate on exit code instead, same shape
544
+ # as utterance_landing_check.sh below. Ships via package.json files[], so absence is FAIL, not SKIP.
545
+ if [ ! -f scripts/capability_registry_check.sh ]; then
546
+ echo "FAIL capability_registry_check.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
547
+ fail=1
548
+ elif _out=$(bash scripts/capability_registry_check.sh --self-test < /dev/null 2>&1); then
549
+ echo "PASS capability_registry_check.sh --self-test ($(printf '%s\n' "$_out" | grep -oE '통과 [0-9]+ · 실패 [0-9]+' | tail -1))"
550
+ else
551
+ echo "FAIL capability_registry_check.sh: --self-test failed"
552
+ _show_failure "$_out"
553
+ fail=1
554
+ fi
555
+
556
+ # relay_channel — the other embedded --self-test subject kept out of the loop above: its
557
+ # `--self-test` just `exec`s the sibling scripts/test_relay_channel_lanes.sh, whose own exit code
558
+ # is already disciplined (`[ "$fail" -eq 0 ] || exit 1`), so gating on exit code — same shape as
559
+ # utterance_landing_check.sh below — is both sufficient and correct here; a 캘리브레이션 substring
560
+ # check would be the wrong instrument for this subject's actual completion marker
561
+ # ("N PASS / M FAIL"). Ships via package.json files[] (no ACCEPTED_ABSENT declaration), so absence
562
+ # is FAIL, not SKIP — same reasoning as utterance_landing_check.sh's absence branch below.
563
+ # Existence check covers BOTH files: relay_channel.sh --self-test just execs the sibling lanes
564
+ # file, so a missing sibling alone produces an unrelated-looking `exec: No such file` (rc=127) that
565
+ # the generic FAIL branch would misreport as "the lane broke" rather than "the anchor is gone"
566
+ # (cross-family review 2026-08-15).
567
+ # `_LANE_TO` (defined above, the pair-suite loop's own guard) is reused rather than a fresh
568
+ # timeout var — this is, by lane count, the largest suite dispatched anywhere in this file
569
+ # (test_relay_channel_lanes.sh runs 45+ lanes, each spawning relay_channel.sh, each spawning its
570
+ # own node subprocess), and it was the one dispatch here with NO time bound at all; `< /dev/null`
571
+ # only stops a stdin-wait hang, not a runaway child (cross-family review 2026-08-15 — this file's
572
+ # own comment two loops up already names exactly this failure shape: an unguarded call does not go
573
+ # red, it hangs, and CI dies on a job timeout with the cause unattributable).
574
+ if [ ! -f scripts/relay_channel.sh ] || [ ! -f scripts/test_relay_channel_lanes.sh ]; then
575
+ echo "FAIL relay_channel.sh: missing (relay_channel.sh or its sibling test_relay_channel_lanes.sh) — ships via package.json files[], so absence is deletion, not package mode"
576
+ fail=1
577
+ elif _out=$($_LANE_TO bash scripts/relay_channel.sh --self-test < /dev/null 2>&1); then
578
+ echo "PASS relay_channel.sh --self-test ($(printf '%s\n' "$_out" | grep -oE 'relay_channel lanes: [0-9]+ PASS / [0-9]+ FAIL' | tail -1))"
579
+ else
580
+ echo "FAIL relay_channel.sh: --self-test failed"
581
+ _show_failure "$_out"
582
+ fail=1
583
+ fi
584
+
517
585
  # memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
518
586
  # edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
519
587
  # that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
@@ -1001,6 +1069,23 @@ if [ -f scripts/test_selfcheck_state_lanes.sh ]; then
1001
1069
  fi
1002
1070
  fi
1003
1071
 
1072
+ # test_lane_runner_lanes.sh — the checker that finds unrun suites had no suite of its own until
1073
+ # 2026-08-13. It guarded its own WIRING (it fails when selfcheck stops calling it) and nothing
1074
+ # measured its BEHAVIOUR — the shape it exists to catch, one level up. Wired in the same change
1075
+ # that ships it, for the reason the block above states: a suite landed unwired is the defect.
1076
+ if [ -f scripts/test_lane_runner_lanes.sh ]; then
1077
+ if _out=$(bash scripts/test_lane_runner_lanes.sh 2>&1); then
1078
+ echo "PASS test_lane_runner_lanes.sh (org seam: no-op · suppression · fail-closed · controls)"
1079
+ else
1080
+ echo "FAIL test_lane_runner_lanes.sh: the unrun-suite detector's own verdicts have drifted"
1081
+ _show_failure "$_out"
1082
+ fail=1
1083
+ fi
1084
+ elif _ships_per_files "scripts/test_lane_runner_lanes.sh"; then
1085
+ echo "FAIL test_lane_runner_lanes.sh is DECLARED SHIPPED but absent — deletion or broken install"
1086
+ fail=1
1087
+ fi
1088
+
1004
1089
  # sync_from_be_lanes.sh — the RETURN path's anchor. Wired in the same change that ships it: the
1005
1090
  # script had an operator-side caller (a SessionStart hook outside this repo) while its 70 lanes had
1006
1091
  # NO caller anywhere, which is the shape this repo keeps re-finding — a transport that writes into
@@ -1044,6 +1129,27 @@ else
1044
1129
  fail=1
1045
1130
  fi
1046
1131
 
1132
+ # sync_to_be_lanes.sh — the FORWARD path's anchor (added 2026-08-14, pmh-dev#69). Same shape as the
1133
+ # return-path block above and wired in the same change that ships it, for the same reason: a
1134
+ # transport with real reported defects (a real add/add conflict, a real hard-abort risk) had no
1135
+ # suite exercising it at all before this. Same subject-presence idiom, same run-once-capture
1136
+ # discipline (a lane suite discarded to /dev/null on a non-deterministic run already burned two days
1137
+ # on this exact file's sibling).
1138
+ if [ ! -f scripts/sync-to-be.sh ]; then
1139
+ echo "SKIP sync_to_be_lanes.sh (subject scripts/sync-to-be.sh absent)"
1140
+ elif [ -f scripts/sync_to_be_lanes.sh ]; then
1141
+ if _out=$(bash scripts/sync_to_be_lanes.sh 2>&1); then
1142
+ echo "PASS sync_to_be_lanes.sh (forward-path lanes)"
1143
+ else
1144
+ echo "FAIL sync_to_be_lanes.sh: forward-path lanes failed"
1145
+ _show_failure "$_out"
1146
+ fail=1
1147
+ fi
1148
+ else
1149
+ echo "FAIL sync_to_be_lanes.sh: sync-to-be.sh present but its anchor is missing"
1150
+ fail=1
1151
+ fi
1152
+
1047
1153
  # Referenced-path existence is a source-tree check. The npm package intentionally
1048
1154
  # ships a narrower runtime surface, so package-mode selfcheck skips this section.
1049
1155
  # ⚠️ The skip predicate used to be `[ -d ".claude/rules" ]`. That broke the moment the tarball
@@ -630,6 +630,81 @@ else
630
630
  bad "PY3 mixed-language markdown lost one language — shadows collide by name or the loop stops at the first hit"
631
631
  fi
632
632
 
633
+ # ── SCOPE: the scanner must HONOR its arguments ────────────────────────────────────────────────
634
+ # This is the contract templates/.git-hooks/pre-commit depends on since 2026-08-13. That hook used
635
+ # to invoke this scanner with NO ARGUMENTS — walking the whole repository — and then discard almost
636
+ # all of it through a `grep -Ff` over the staged load-bearing files it already had in hand.
637
+ # Measured: whole-repo 42s / 260 lines vs one staged file 0s / 2 lines; a sibling harness measured
638
+ # the same call at 2m07s inside a 7m54s–9m40s commit, one of which could not complete at all.
639
+ # The lane exists because the FIX IS SILENTLY REVERSIBLE: if this scanner ever stops honoring its
640
+ # arguments, the hook keeps working and simply becomes slow again — no verdict changes, nothing
641
+ # goes red, and the only symptom is a commit people start bypassing with --no-verify. The same hook
642
+ # carries the Destructive-Op gate, so a trained bypass disarms an irreversible-surface gate too.
643
+ # BEHAVIOURAL, not shape: it asserts the OUTPUT is confined to the named file, which is the property
644
+ # the hook relies on. A grep for "does the hook pass an argument" would pass on a scanner that
645
+ # accepts arguments and ignores them.
646
+ _scope_a="$TMP/scope_a.sh"; _scope_b="$TMP/scope_b.sh"
647
+ printf '#!/usr/bin/env bash\ncheck() { probe || return 0; }\n' > "$_scope_a"
648
+ printf '#!/usr/bin/env bash\nverify() { thing || exit 0; }\n' > "$_scope_b"
649
+ out=$(bash "$SCAN" "$_scope_a" 2>&1)
650
+ if printf '%s' "$out" | grep -q 'scope_a' && ! printf '%s' "$out" | grep -q 'scope_b'; then
651
+ ok "SCOPE1 a named target is scanned and a sibling file is NOT (the scanner honors argv)"
652
+ else
653
+ bad "SCOPE1 scanner did not confine itself to the named target — the pre-commit scoping fix is void"
654
+ fi
655
+ # The negative arm: with no argument at all it must NOT confine itself to nothing. Without this,
656
+ # a scanner that returned empty for every input would satisfy SCOPE1 and the lane would be theatre.
657
+ out=$(bash "$SCAN" "$TMP" 2>&1)
658
+ if printf '%s' "$out" | grep -q 'scope_a' && printf '%s' "$out" | grep -q 'scope_b'; then
659
+ ok "SCOPE2 control: given the containing directory it finds BOTH (it is not simply blind)"
660
+ else
661
+ bad "SCOPE2 control failed — the scanner found neither file, so SCOPE1 proved nothing"
662
+ fi
663
+
664
+ # ── C2: bare X-in-Y substring boolean, with the ALL-CAPS exclusion (2026-08-14) ────────────────
665
+ # Cross-family review found this exclusion shipped with zero fixtures anywhere in this repo —
666
+ # deleting the exclusion left every existing lane green, which is exactly the "advisory noise
667
+ # reduction with no anchor" class this repo's own 4-axis doctrine requires a mechanical test for.
668
+ # Two arms: the probe's own headline example (paid⊂prepaid style substring check) must still
669
+ # fire, and an ALL-CAPS collection-membership check — the shape the exclusion targets — must not.
670
+ _c2_pos="$TMP/c2_pos.sh"; _c2_neg="$TMP/c2_neg.sh"
671
+ printf 'if tok in text:\n pass\n' > "$_c2_pos"
672
+ printf 'if name in PLACEHOLDERS:\n pass\n' > "$_c2_neg"
673
+ out=$(bash "$SCAN" "$_c2_pos" 2>&1)
674
+ if printf '%s' "$out" | grep -q 'C2:substring-boolean'; then
675
+ ok "C2-KP substring-boolean on a bare string var still fires"
676
+ else
677
+ bad "C2-KP the probe's own headline shape (tok in text) no longer fires"
678
+ fi
679
+ out=$(bash "$SCAN" "$_c2_neg" 2>&1)
680
+ if ! printf '%s' "$out" | grep -q 'C2:substring-boolean'; then
681
+ ok "C2-KN ALL-CAPS collection membership (name in PLACEHOLDERS) stays silent"
682
+ else
683
+ bad "C2-KN the ALL-CAPS exclusion is not wired — a legitimate collection check still fires"
684
+ fi
685
+ # Revert probe, run inline rather than trusted by inspection: replace the exclusion clause's line
686
+ # in a COPY of the scanner with a bare closing paren (deleting the line outright would strand the
687
+ # preceding line's trailing backslash continuation and break the pipe chain) and confirm C2-KN
688
+ # flips to a hit — proves the exclusion is load-bearing for this specific fixture, not just
689
+ # present somewhere in the file.
690
+ _scan_reverted="$TMP/scan_reverted.sh"
691
+ _excl_line=$(grep -n "grep -vE 'in \[A-Z_\]" "$SCAN" | head -1 | cut -d: -f1)
692
+ if [ -z "$_excl_line" ]; then
693
+ bad "C2-REVERT could not locate the exclusion line by its known text — fixture cannot run"
694
+ else
695
+ sed "${_excl_line}s/.*/ )/" "$SCAN" > "$_scan_reverted"
696
+ chmod +x "$_scan_reverted"
697
+ # Captured, not piped directly — under `set -o pipefail` (this file's own top line) the scanner's
698
+ # own advisory exit code (non-zero on any finding) would override grep's match result in a direct
699
+ # pipe, reporting FAIL even when the fixture worked exactly as intended.
700
+ _revert_out=$(bash "$_scan_reverted" "$_c2_neg" 2>&1)
701
+ if printf '%s' "$_revert_out" | grep -q 'C2:substring-boolean'; then
702
+ ok "C2-REVERT removing the exclusion line makes C2-KN fire again (the exclusion is load-bearing)"
703
+ else
704
+ bad "C2-REVERT removing the exclusion line did not flip C2-KN — the fixture proves nothing"
705
+ fi
706
+ fi
707
+
633
708
  echo "----"
634
709
  echo "degrade-scan shell probes: $pass passed, $fail failed"
635
710
  [ "$fail" -eq 0 ] || exit 1
@@ -12,10 +12,32 @@ trap 'rm -rf "$TMP"' EXIT
12
12
  PASS=0; FAIL=0
13
13
  ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
14
14
  no(){ FAIL=$((FAIL+1)); printf ' ❌ %s — %s\n' "$1" "$2"; }
15
+
16
+ # ── DEBT fix (2026-08-14) — several lanes below depended on the OPERATOR'S REAL
17
+ # `${HOME}/projects/qasp-dev` mapping to resolve a repo and emit anything at all. On a
18
+ # machine/CI runner without that exact local layout, `repo` resolution in field_canon_preload.sh
19
+ # silently fails (`continue`), `emitted` stays 0, and EVERY assertion that checks "is output
20
+ # non-empty" trivially reads as the WRONG kind of empty — reproduced locally: PASS 12 / FAIL 7 with
21
+ # HOME pointed at an empty dir, exactly the CI split. The fix reuses the fixture shape lane ⑭
22
+ # already demonstrates below (a throwaway hub + a throwaway project root with a real `.git` and
23
+ # README.md) instead of depending on the operator's real filesystem — `found→extend`, not a new
24
+ # mechanism. This also turns ⑧'s prior "PASS" honest: before this fix it passed because repo
25
+ # resolution ALWAYS failed (output always empty regardless of dedup), not because dedup worked
26
+ # ([[feedback_anchor_can_be_decorative]]) — with a real resolvable repo, ⑧ now exercises the actual
27
+ # same-session dedup path.
28
+ FHUB="$TMP/fhub"; mkdir -p "$FHUB/tracks/qasp"
29
+ FPROJ="$TMP/fproj"; mkdir -p "$FPROJ/qasp-dev/.git" "$FPROJ/qasp-dev/docs/governance"
30
+ printf '# qasp fixture\n' > "$FPROJ/qasp-dev/README.md"
31
+ printf 'x\n' > "$FPROJ/qasp-dev/docs/governance/g1.md"
32
+
15
33
  # ★`${3:+VAR=v}` 는 파라미터 확장이라 bash 가 **환경 할당으로 안 읽는다** — env 로 넘긴다.
16
34
  # (이 파일 초판이 그렇게 써서 「레포 부재」 레인이 거짓 적색을 냈다. 계기 결함이었다.)
17
- run(){ local extra=""; [ -n "${3:-}" ] && extra="FIELD_CANON_PROJECT_ROOT=$3"
18
- printf '%s' "$1" | env FIELD_CANON_SENTINEL_DIR="$2" $extra bash "$H" 2>&1; }
35
+ # $3, when given, overrides FIELD_CANON_PROJECT_ROOT (the "레포 부재" control at line ~35 uses this
36
+ # to deliberately point at /nonexistent CLAUDE_PROJECT_DIR still resolves via the fixture so that
37
+ # control lane is testing "repo absent", not accidentally "mapped name absent" too).
38
+ run(){ local root="$FPROJ"; [ -n "${3:-}" ] && root="$3"
39
+ printf '%s' "$1" | env CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$root" \
40
+ FIELD_CANON_SENTINEL_DIR="$2" bash "$H" 2>&1; }
19
41
 
20
42
  echo "== field-canon preload 레인 =="
21
43
 
@@ -47,12 +69,14 @@ printf '' | FIELD_CANON_SENTINEL_DIR="$TMP/6" bash "$H" >/dev/null 2>&1
47
69
  # (`[[feedback_anchor_can_be_decorative]]` — 레인이 결함 지점을 비켜 가면 초록은 정보가 아니다.)
48
70
  # ⚠️ macOS 는 bash **3.2** 다. `set -u` 아래서 빈 배열 `"${a[@]}"` 는 unbound variable 로 죽는다
49
71
  # — 초판 레인이 정확히 그렇게 죽어 ⑦⑨⑩ 이 **거짓 적색**을 냈다(대상이 아니라 계기의 사망).
50
- # 배열 없이 분기한다.
72
+ # 배열 없이 분기한다. (FHUB/FPROJ fixture — see the DEBT-fix comment near the top of this file.)
51
73
  runD(){ # $1=payload $2=TMPDIR [$3=CLAUDE_SESSION_ID 를 환경에 심을지]
52
74
  if [ -n "${3:-}" ]; then
53
- printf '%s' "$1" | env TMPDIR="$2" CLAUDE_SESSION_ID="$3" bash "$H" 2>&1
75
+ printf '%s' "$1" | env TMPDIR="$2" CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$FPROJ" \
76
+ CLAUDE_SESSION_ID="$3" bash "$H" 2>&1
54
77
  else
55
- printf '%s' "$1" | env TMPDIR="$2" bash "$H" 2>&1
78
+ printf '%s' "$1" | env TMPDIR="$2" CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$FPROJ" \
79
+ bash "$H" 2>&1
56
80
  fi; }
57
81
  T7="$TMP/def"; mkdir -p "$T7"
58
82
 
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env bash
2
+ # test_lane_runner_lanes.sh — behavioural lanes for scripts/lane_runner_check.sh
3
+ #
4
+ # Why this file exists at all: until 2026-08-13 the checker that detects unrun lane suites had
5
+ # **no lane suite of its own**. It guarded its own WIRING (it fails if selfcheck.sh stops calling
6
+ # it) but nothing measured its BEHAVIOUR. That is the same shape it exists to catch, one level up —
7
+ # a check whose verdicts nobody exercises is prose with an exit code.
8
+ #
9
+ # The lanes below drive it against SYNTHETIC fixture trees, not against this repo. Driving it
10
+ # against the live tree would make every lane depend on today's suite list, so a lane would go red
11
+ # for reasons that have nothing to do with the predicate under test.
12
+ #
13
+ # Script-relative, NOT `git rev-parse --show-toplevel`: in a vendored tree rev-parse returns the
14
+ # OUTER repo and this suite would then test someone else's checkout.
15
+ set -uo pipefail
16
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ CHECK="$REPO_ROOT/scripts/lane_runner_check.sh"
18
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
19
+ pass=0; fail=0
20
+ ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
21
+ bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
22
+
23
+ # ── fixture: a tree with ONE unwired suite and a selfcheck.sh that calls the checker ──────────
24
+ # The `bash scripts/lane_runner_check.sh` line is load-bearing: the checker refuses to run in a
25
+ # tree whose selfcheck.sh does not invoke it (its self-reference guard).
26
+ mkfixture() { # $1 = dir
27
+ local d="$1"
28
+ mkdir -p "$d/scripts"
29
+ cp "$CHECK" "$d/scripts/lane_runner_check.sh"
30
+ printf '#!/usr/bin/env bash\nbash scripts/lane_runner_check.sh\n' > "$d/scripts/selfcheck.sh"
31
+ printf '#!/usr/bin/env bash\n: orphan lane suite\n' > "$d/scripts/test_orphan_lanes.sh"
32
+ }
33
+ run() { ( cd "$1" && bash scripts/lane_runner_check.sh 2>&1 ); }
34
+ rcof() { ( cd "$1" && bash scripts/lane_runner_check.sh >/dev/null 2>&1; echo $? ); }
35
+
36
+ # ── L1 known-POSITIVE: an undeclared orphan suite must FAIL ───────────────────────────────────
37
+ # Without this arm every lane below proves nothing: a checker that passed unconditionally would
38
+ # satisfy all the "declaration suppresses it" lanes.
39
+ D="$T/l1"; mkfixture "$D"
40
+ out=$(run "$D"); rc=$(rcof "$D")
41
+ if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q 'test_orphan_lanes.sh'; then
42
+ ok "L1 known-positive: undeclared orphan suite → rc=1 and it is named"
43
+ else
44
+ bad "L1 known-positive did not fire (rc=$rc) — every lane below is meaningless without it"
45
+ fi
46
+
47
+ # ── L2 NO-OP: no declaration file → behaviour is exactly the L1 behaviour ─────────────────────
48
+ # This is the arm that protects THIS repo, which ships no company/ directory.
49
+ D="$T/l2"; mkfixture "$D"
50
+ out=$(run "$D")
51
+ if ! printf '%s' "$out" | grep -q 'lane_declarations'; then
52
+ ok "L2 no-op: absent declaration file is silent (no mention in output)"
53
+ else
54
+ bad "L2 no-op: output mentions the declaration file when none exists"
55
+ fi
56
+
57
+ # ── L3 org EXEMPT suppresses the orphan ───────────────────────────────────────────────────────
58
+ D="$T/l3"; mkfixture "$D"; mkdir -p "$D/company"
59
+ printf 'exempt:\n - test_orphan_lanes.sh # org-owned, runs in the org CI only\n' > "$D/company/lane_declarations.yaml"
60
+ out=$(run "$D"); rc=$(rcof "$D")
61
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q '1 exempt'; then
62
+ ok "L3 org exempt: declared suite no longer fails the check (rc=0, counted exempt)"
63
+ else
64
+ bad "L3 org exempt did not suppress (rc=$rc) — out: $(printf '%s' "$out" | tail -1)"
65
+ fi
66
+
67
+ # ── L4 org DEBT suppresses AND is counted as debt ─────────────────────────────────────────────
68
+ D="$T/l4"; mkfixture "$D"; mkdir -p "$D/company"
69
+ printf 'debt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
70
+ out=$(run "$D"); rc=$(rcof "$D")
71
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'declared debt'; then
72
+ ok "L4 org debt: declared suite suppressed and reported as debt"
73
+ else
74
+ bad "L4 org debt did not land (rc=$rc)"
75
+ fi
76
+
77
+ # ── L5 VISIBILITY: an org-declared list must never look like this repo's own zero ─────────────
78
+ D="$T/l5"; mkfixture "$D"; mkdir -p "$D/company"
79
+ printf 'debt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
80
+ out=$(run "$D")
81
+ if printf '%s' "$out" | grep -q 'org-declared'; then
82
+ ok "L5 visibility: output names the org file and its counts"
83
+ else
84
+ bad "L5 visibility: org declarations are invisible in the output"
85
+ fi
86
+
87
+ # ── L6 FAIL-CLOSED on an unparseable file ─────────────────────────────────────────────────────
88
+ # The load-bearing distinction. A malformed file must NOT read as "nothing declared" — that would
89
+ # render UNMEASURED as ZERO, and it would do so on the arm where a fork is relying on the file.
90
+ D="$T/l6"; mkfixture "$D"; mkdir -p "$D/company"
91
+ printf 'exempt:\n this line is not an entry\n' > "$D/company/lane_declarations.yaml"
92
+ out=$(run "$D"); rc=$(rcof "$D")
93
+ if [ "$rc" = "2" ] && printf '%s' "$out" | grep -q 'UNMEASURED'; then
94
+ ok "L6 fail-closed: unparseable declaration file → rc=2 and says UNMEASURED"
95
+ else
96
+ bad "L6 fail-closed FAILED (rc=$rc) — a malformed file was read as 'no declarations'"
97
+ fi
98
+
99
+ # ── L6b control for L6: the SAME orphan tree with a WELL-FORMED file must not exit 2 ──────────
100
+ # Without this, L6 would also pass on a checker that exits 2 for any declaration file at all.
101
+ D="$T/l6b"; mkfixture "$D"; mkdir -p "$D/company"
102
+ printf 'exempt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
103
+ if [ "$(rcof "$D")" != "2" ]; then
104
+ ok "L6b control: a well-formed file does NOT trip the fail-closed arm"
105
+ else
106
+ bad "L6b control: every declaration file exits 2 — L6 proved nothing"
107
+ fi
108
+
109
+ # ── L7 unknown section is a parse failure, not a silent skip ──────────────────────────────────
110
+ D="$T/l7"; mkfixture "$D"; mkdir -p "$D/company"
111
+ printf 'allowed:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
112
+ out=$(run "$D")
113
+ if [ "$(rcof "$D")" = "2" ] && printf '%s' "$out" | grep -q "unknown section"; then
114
+ ok "L7 unknown section → rc=2 and names the section"
115
+ else
116
+ bad "L7 unknown section was tolerated — a typo'd header would silently declare nothing"
117
+ fi
118
+
119
+ # ── L8 the same name in both sections is ambiguous, not last-wins ─────────────────────────────
120
+ D="$T/l8"; mkfixture "$D"; mkdir -p "$D/company"
121
+ printf 'exempt:\n - test_orphan_lanes.sh\ndebt:\n - test_orphan_lanes.sh\n' > "$D/company/lane_declarations.yaml"
122
+ out=$(run "$D")
123
+ if [ "$(rcof "$D")" = "2" ] && printf '%s' "$out" | grep -q "both exempt and debt"; then
124
+ ok "L8 same suite in both sections → rc=2 (ambiguous, not silently resolved)"
125
+ else
126
+ bad "L8 double declaration was silently resolved"
127
+ fi
128
+
129
+ # ── L9 a stale entry WARNS but does not block (same voice as upstream DEBT hygiene) ───────────
130
+ # Deliberately advisory: making the downstream mirror stricter than the upstream rule it mirrors
131
+ # would train forks to delete the file, which costs more than a stale line.
132
+ D="$T/l9"; mkfixture "$D"; mkdir -p "$D/company"
133
+ printf 'exempt:\n - test_orphan_lanes.sh\n - test_deleted_lanes.sh\n' > "$D/company/lane_declarations.yaml"
134
+ out=$(run "$D"); rc=$(rcof "$D")
135
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'test_deleted_lanes.sh'; then
136
+ ok "L9 stale entry: warns and names it, does not block (rc=0)"
137
+ else
138
+ bad "L9 stale entry handling wrong (rc=$rc)"
139
+ fi
140
+
141
+ # ── L10 comments and blank lines are not entries ──────────────────────────────────────────────
142
+ D="$T/l10"; mkfixture "$D"; mkdir -p "$D/company"
143
+ printf '# org lane declarations\n\nexempt:\n\n - test_orphan_lanes.sh # reason lives here\n' > "$D/company/lane_declarations.yaml"
144
+ if [ "$(rcof "$D")" = "0" ]; then
145
+ ok "L10 comments/blank lines tolerated (a file people can annotate)"
146
+ else
147
+ bad "L10 a commented, spaced file failed to parse"
148
+ fi
149
+
150
+ # ── L11/L12 embedded --self-test subjects (found→extend, 2026-08-14) ──────────────────────────
151
+ # A lane suite living INSIDE its subject as a `--self-test` flag has no `test_*.sh`/`*_lanes.sh`
152
+ # filename, so the suites glob above cannot see it at all — measured on this repo's own
153
+ # lane_runner_check.sh header before this feature existed: 4 such subjects had zero callers and
154
+ # nothing here said so. mkfixture without the orphan file, so these lanes isolate the self-test
155
+ # behaviour from L1's own blocking orphan.
156
+ mkfixture_clean() { # $1 = dir
157
+ local d="$1"
158
+ mkdir -p "$d/scripts"
159
+ cp "$CHECK" "$d/scripts/lane_runner_check.sh"
160
+ # A wired ordinary suite so the `suites` set is non-empty — an empty tree fails EXTRACTOR_BROKE
161
+ # before the self-test report section runs at all, which would make L11/L12 pass vacuously.
162
+ printf '#!/usr/bin/env bash\n: wired lane suite\n' > "$d/scripts/test_ok_lanes.sh"
163
+ printf '#!/usr/bin/env bash\nbash scripts/lane_runner_check.sh\nbash scripts/test_ok_lanes.sh\n' > "$d/scripts/selfcheck.sh"
164
+ }
165
+
166
+ # L11 known-POSITIVE: an unwired --self-test subject is reported, advisory (rc stays 0).
167
+ D="$T/l11"; mkfixture_clean "$D"
168
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
169
+ out=$(run "$D"); rc=$(rcof "$D")
170
+ if [ "$rc" = "0" ] && printf '%s' "$out" | grep -q 'orphan_probe'; then
171
+ ok "L11 known-positive: unwired --self-test subject named, advisory (rc=0)"
172
+ else
173
+ bad "L11 known-positive did not fire (rc=$rc) — self-test subject not reported"
174
+ printf '%s\n' "$out" | sed 's/^/ │ /'
175
+ fi
176
+
177
+ # L12 known-NEGATIVE: the SAME subject, wired through selfcheck.sh's _subj for-loop idiom, is
178
+ # NOT reported — the exact shape scripts/selfcheck.sh:478 actually uses.
179
+ D="$T/l12"; mkfixture_clean "$D"
180
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
181
+ cat > "$D/scripts/selfcheck.sh" <<'SC'
182
+ #!/usr/bin/env bash
183
+ bash scripts/lane_runner_check.sh
184
+ bash scripts/test_ok_lanes.sh
185
+ for _subj in orphan_probe; do
186
+ bash "scripts/$_subj.sh" --self-test
187
+ done
188
+ SC
189
+ out=$(run "$D")
190
+ if ! printf '%s' "$out" | grep -q 'orphan_probe'; then
191
+ ok "L12 known-negative: --self-test subject wired via _subj for-loop is not reported"
192
+ else
193
+ bad "L12 known-negative: wired subject still reported as unwired"
194
+ printf '%s\n' "$out" | sed 's/^/ │ /'
195
+ fi
196
+
197
+ # L13 known-NEGATIVE: the SAME subject, wired through a DIRECT dispatch line — the shape
198
+ # scripts/selfcheck.sh:898/933 actually use (`bash scripts/<name>.sh ... --self-test`), no
199
+ # for-loop at all. Cross-family review 2026-08-14 caught the first draft shipping only the
200
+ # for-loop branch, which meant probe_scope_check.sh and utterance_landing_check.sh — both wired
201
+ # directly, both real — were reported UNDECLARED. This lane pins that class so it cannot silently
202
+ # come back: without it, L11/L12 alone only ever exercise the for-loop shape.
203
+ D="$T/l13"; mkfixture_clean "$D"
204
+ printf '#!/usr/bin/env bash\n[ "${1:-}" = "--self-test" ] && { echo ok; exit 0; }\n' > "$D/scripts/orphan_probe.sh"
205
+ cat > "$D/scripts/selfcheck.sh" <<'SC'
206
+ #!/usr/bin/env bash
207
+ bash scripts/lane_runner_check.sh
208
+ bash scripts/test_ok_lanes.sh
209
+ bash scripts/orphan_probe.sh --self-test >/dev/null 2>&1
210
+ SC
211
+ out=$(run "$D")
212
+ if ! printf '%s' "$out" | grep -q 'orphan_probe'; then
213
+ ok "L13 known-negative: --self-test subject wired via direct dispatch is not reported"
214
+ else
215
+ bad "L13 known-negative: directly-wired subject still reported as unwired"
216
+ printf '%s\n' "$out" | sed 's/^/ │ /'
217
+ fi
218
+
219
+ echo "----"
220
+ echo "lane-runner lanes: $pass passed, $fail failed"
221
+ [ "$fail" -eq 0 ] || exit 1
@@ -58,7 +58,7 @@ git clone -q "$WORK/origin.git" "$WORK/C" 2>/dev/null
58
58
  run "non-origin remote name still fires" HIT "$WORK/C/newfile.py"
59
59
 
60
60
  # Throttle: SAME marker dir, two calls — second must be silent.
61
- mdir=$(mktemp -d "$WORK/marker.throttle")
61
+ mdir=$(mktemp -d "$WORK/marker.throttle.XXXXXX")
62
62
  o1=$(payload "$WORK/B/x.py" | FH_STALE_CLONE_NO_FETCH=1 FH_STALE_CLONE_MARKER_DIR="$mdir" bash "$G" 2>&1)
63
63
  o2=$(payload "$WORK/B/y.py" | FH_STALE_CLONE_NO_FETCH=1 FH_STALE_CLONE_MARKER_DIR="$mdir" bash "$G" 2>&1)
64
64
  if printf '%s' "$o1" | grep -q STALE-CLONE && [ -z "$o2" ]; then
@@ -68,7 +68,7 @@ else
68
68
  fi
69
69
 
70
70
  # Contract: HIT emits one JSON object with both channels and no permissionDecision.
71
- mdir=$(mktemp -d "$WORK/marker.json")
71
+ mdir=$(mktemp -d "$WORK/marker.json.XXXXXX")
72
72
  jo=$(payload "$WORK/B/z.py" | FH_STALE_CLONE_NO_FETCH=1 FH_STALE_CLONE_MARKER_DIR="$mdir" bash "$G" 2>/dev/null)
73
73
  if printf '%s' "$jo" | python3 -c '
74
74
  import json,sys
@@ -114,25 +114,39 @@ run "slashed remote name still fires" HIT "$WORK/D/newfile.py"
114
114
  # budget, exit 0, arm the day-throttle — instead of letting the RUNNER's 20s timeout kill it
115
115
  # (which skipped the marker write and re-stalled every Write of the day). PATH shim makes `git
116
116
  # fetch` hang; budget is set to 0.5s; the real git serves every other subcommand.
117
+ #
118
+ # DEBT fix (2026-08-14): `elapsed -lt 10` was a bare wall-clock assertion on a mandatory path — a
119
+ # loaded CI runner's fork/exec + scheduling overhead can push actual elapsed time past a fixed
120
+ # small constant even when the guard's OWN 0.5s internal budget fired correctly (named and
121
+ # predicted by an adversarial review of the original delta; CI then reproduced it, 2/17 lanes red
122
+ # where local was 17/17). What this lane actually needs to prove is RELATIVE, not absolute: the
123
+ # guard returned because its budget bounded it, not because the wedge itself woke up (30s) or an
124
+ # external timeout killed the process. So: widen the wedge to 60s (cheap — the guard should never
125
+ # come close to waiting it out) and assert elapsed is well under HALF of that, not a small fixed
126
+ # number. This keeps strong discriminating power (a guard that stopped bounding itself would still
127
+ # blow well past 30s) while absorbing CI-runner overhead that has nothing to do with the guard's
128
+ # own logic.
129
+ WEDGE_SLEEP=60
117
130
  SHIM="$WORK/shim"; mkdir -p "$SHIM"
118
131
  REALGIT=$(command -v git)
119
132
  cat > "$SHIM/git" <<EOF
120
133
  #!/bin/bash
121
- for a in "\$@"; do [ "\$a" = "fetch" ] && sleep 30; done
134
+ for a in "\$@"; do [ "\$a" = "fetch" ] && sleep $WEDGE_SLEEP; done
122
135
  exec "$REALGIT" "\$@"
123
136
  EOF
124
137
  chmod +x "$SHIM/git"
125
- mdir=$(mktemp -d "$WORK/marker.wedge")
138
+ mdir=$(mktemp -d "$WORK/marker.wedge.XXXXXX")
126
139
  t0=$(date +%s)
127
140
  w_out=$(payload "$WORK/B/wedge.py" | PATH="$SHIM:$PATH" FH_STALE_CLONE_FETCH_BUDGET_TENTHS=5 \
128
141
  FH_STALE_CLONE_MARKER_DIR="$mdir" bash "$G" 2>&1); w_rc=$?
129
142
  t1=$(date +%s)
130
143
  elapsed=$((t1 - t0))
144
+ elapsed_cap=$((WEDGE_SLEEP / 2))
131
145
  marker_count=$(ls "$mdir" 2>/dev/null | wc -l | tr -d ' ')
132
- if [ "$w_rc" -eq 0 ] && [ -z "$w_out" ] && [ "$elapsed" -lt 10 ] && [ "$marker_count" -ge 1 ]; then
133
- printf ' ✅ %-52s OK (%ss)\n' "wedged fetch: bounded, silent, throttle armed" "$elapsed"; pass=$((pass+1))
146
+ if [ "$w_rc" -eq 0 ] && [ -z "$w_out" ] && [ "$elapsed" -lt "$elapsed_cap" ] && [ "$marker_count" -ge 1 ]; then
147
+ printf ' ✅ %-52s OK (%ss < %ss)\n' "wedged fetch: bounded, silent, throttle armed" "$elapsed" "$elapsed_cap"; pass=$((pass+1))
134
148
  else
135
- printf ' ❌ %-52s rc=%s elapsed=%ss markers=%s out=%s\n' "wedged fetch: bounded, silent, throttle armed" "$w_rc" "$elapsed" "$marker_count" "$w_out"; fail=$((fail+1))
149
+ printf ' ❌ %-52s rc=%s elapsed=%ss(cap %ss) markers=%s out=%s\n' "wedged fetch: bounded, silent, throttle armed" "$w_rc" "$elapsed" "$elapsed_cap" "$marker_count" "$w_out"; fail=$((fail+1))
136
150
  fi
137
151
 
138
152
  # Budget cap (terra round 2): an all-digit literal wider than the shell's integer width made the