@chrono-meta/fh-gate 2.15.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.claude/rules/fh_4axis_gate.md +38 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CLAUDE.md +1 -1
  4. package/README.md +6 -1
  5. package/knowledge/shared/harness-core/fh_three_layer_canon.md +47 -0
  6. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +10 -0
  7. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +15 -1
  8. package/knowledge/shared/harness-core/ship_readiness_gate.md +20 -3
  9. package/knowledge/shared/learnings/subagent_invocations_log.yaml +111 -0
  10. package/package.json +9 -1
  11. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  12. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  13. package/plugins/fh-meta/CHANGELOG.md +49 -0
  14. package/plugins/fh-meta/skills/salience-splitter/SKILL.md +1 -1
  15. package/scripts/backtick_guard.sh +194 -0
  16. package/scripts/capability_effect_probe.sh +14 -1
  17. package/scripts/context_continuity_score.sh +106 -12
  18. package/scripts/fh-gate.sh +3 -3
  19. package/scripts/files_manifest_shipping_check.sh +19 -0
  20. package/scripts/gate_pathspec_check.sh +1 -1
  21. package/scripts/package_coverage_check.sh +25 -2
  22. package/scripts/proposal_hook.sh +89 -0
  23. package/scripts/public_surface_scan_files.sh +11 -2
  24. package/scripts/revert_probe.sh +250 -0
  25. package/scripts/selfcheck.sh +67 -3
  26. package/scripts/sim_isolated_run.sh +97 -7
  27. package/scripts/test_backtick_guard_lanes.sh +115 -0
  28. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  29. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  30. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  31. package/scripts/test_lane_runner_lanes.sh +59 -33
  32. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  33. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  34. package/scripts/test_node_check_lanes.sh +34 -34
  35. package/scripts/test_package_coverage_lanes.sh +53 -27
  36. package/scripts/test_pipe_verdict_guard_lanes.sh +5 -5
  37. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  38. package/scripts/test_preprep_drift_anchor.sh +13 -4
  39. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  40. package/scripts/test_proposal_hook_lanes.sh +36 -0
  41. package/scripts/test_revert_probe_lanes.sh +146 -0
  42. package/scripts/test_session_close_lanes.sh +3 -5
  43. package/scripts/test_sim_isolated_run_lanes.sh +17 -0
  44. package/scripts/test_verdict_watermark_lanes.sh +27 -1
  45. package/scripts/utterance_landing_check.sh +2 -2
  46. package/templates/.git-hooks/pre-commit +27 -4
  47. package/templates/settings.PreToolUse.snippet.json +37 -1
  48. package/plugins/fh-commons/README.md +0 -38
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ # test_revert_probe_lanes.sh — known-pair calibration for scripts/revert_probe.sh
3
+ #
4
+ # WHY THESE LANES. revert_probe.sh exists to replace 15+ hand-written revert-and-observe scripts
5
+ # with one generic tool. The two failure modes that would make it worse than the hand-rolled
6
+ # scripts it replaces are exactly the two known-pair lanes below:
7
+ # L1 DECORATIVE anchor — a suite whose lanes never actually depend on the target file's content
8
+ # must be reported as decorative (exit 1), not silently scored as "passed".
9
+ # L2 REAL anchor — the mirror-image known-positive: a suite that DOES catch the revert
10
+ # must report the exact flipped lane and exit 0. Without this half, L1 proves nothing
11
+ # ([[feedback_control_presence_is_not_discrimination]]).
12
+ # L3 RESTORE GUARANTEED — the target file must come back byte-identical after the probe, in
13
+ # BOTH the normal-completion path and the harness-error path (suite crashes / produces no
14
+ # parseable output). A revert tool that leaves the baseline content sitting in the working
15
+ # tree on the failure path is strictly worse than not having the tool.
16
+ # L4+ usage guards, harness-error detection, and the git-show (not git-checkout) restore path.
17
+ #
18
+ # Fixtures are throwaway git repos + throwaway suite scripts, never this repo's own assets — a
19
+ # lane suite that references a repo-specific path fails on a fresh clone (portability_lint P9).
20
+ #
21
+ # Usage: bash scripts/test_revert_probe_lanes.sh
22
+
23
+ set -uo pipefail
24
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
25
+ SUT="${FH_REVERT_PROBE_BIN:-$ROOT/scripts/revert_probe.sh}"
26
+ pass=0; fail=0
27
+ ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
28
+ no() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
29
+
30
+ if [ ! -f "$SUT" ]; then
31
+ echo "FAIL test_revert_probe_lanes.sh: subject absent ($SUT) — skipped is not passed"
32
+ exit 1
33
+ fi
34
+
35
+ WORKROOT="$(mktemp -d "${TMPDIR:-/tmp}/revertlane-XXXXXX")"
36
+ trap 'rm -rf "$WORKROOT"' EXIT
37
+
38
+ # ── throwaway repo: commit 1 = baseline (missing the fix) · commit 2/HEAD = current (has the fix)
39
+ SRC="$WORKROOT/src"; mkdir -p "$SRC"
40
+ (
41
+ cd "$SRC" && git init -q . && git config user.email l@l && git config user.name l
42
+ printf 'value = old_and_broken\n' > guard.txt
43
+ git add guard.txt && git commit -qm "baseline (no fix)"
44
+ printf 'value = MARKER_OK\n' > guard.txt
45
+ git add guard.txt && git commit -qm "current (has the fix)"
46
+ ) >/dev/null 2>&1
47
+ TARGET="$SRC/guard.txt"
48
+ TARGET_SHA_BEFORE() { shasum -a 256 "$TARGET" 2>/dev/null | awk '{print $1}'; }
49
+
50
+ # ── fixture suites ──────────────────────────────────────────────────────────────────────────
51
+ # REAL anchor: the lane's pass/fail genuinely depends on the target file's content.
52
+ REAL_SUITE="$WORKROOT/real_suite.sh"
53
+ cat > "$REAL_SUITE" <<'EOF'
54
+ #!/usr/bin/env bash
55
+ if grep -q MARKER_OK "$FIXTURE_TARGET" 2>/dev/null; then
56
+ printf ' %s %s\n' "✅" "marker present in guard.txt"
57
+ else
58
+ printf ' %s %s\n' "❌" "marker present in guard.txt"
59
+ fi
60
+ exit 0
61
+ EOF
62
+ chmod +x "$REAL_SUITE"
63
+
64
+ # DECORATIVE anchor: always ✅, never actually reads the target file.
65
+ DECO_SUITE="$WORKROOT/deco_suite.sh"
66
+ cat > "$DECO_SUITE" <<'EOF'
67
+ #!/usr/bin/env bash
68
+ printf ' %s %s\n' "✅" "unrelated check that never reads guard.txt"
69
+ exit 0
70
+ EOF
71
+ chmod +x "$DECO_SUITE"
72
+
73
+ # HARNESS-ERROR suite: crashes before printing any ✅/❌ line at all.
74
+ CRASH_SUITE="$WORKROOT/crash_suite.sh"
75
+ cat > "$CRASH_SUITE" <<'EOF'
76
+ #!/usr/bin/env bash
77
+ echo "no verdict lines here, just noise" >&2
78
+ exit 1
79
+ EOF
80
+ chmod +x "$CRASH_SUITE"
81
+
82
+ echo "── revert_probe known-pair lanes ──────────────────────────────────"
83
+
84
+ # L1 — DECORATIVE anchor → exit 1, 0 flipped lanes
85
+ BEFORE1="$(TARGET_SHA_BEFORE)"
86
+ OUT1=$(FIXTURE_TARGET="$TARGET" bash "$SUT" "$TARGET" "$DECO_SUITE" --baseline HEAD~1 2>&1); RC1=$?
87
+ [ "$RC1" -eq 1 ] && ok "L1 decorative anchor → exit 1" || no "L1 decorative anchor: rc=$RC1 (want 1)"
88
+ printf '%s' "$OUT1" | grep -q "되돌린 레인 (✅→❌, 앵커가 실제로 잡은 것): 0개" \
89
+ && ok "L1b report states 0 flipped lanes" || no "L1b report did not state 0 flipped lanes"
90
+ AFTER1="$(TARGET_SHA_BEFORE)"
91
+ [ "$BEFORE1" = "$AFTER1" ] && ok "L1c target file restored after decorative run" \
92
+ || no "L1c target file NOT restored (before=$BEFORE1 after=$AFTER1)"
93
+
94
+ # L2 — REAL anchor → exit 0, exactly 1 flipped lane, names it
95
+ BEFORE2="$(TARGET_SHA_BEFORE)"
96
+ OUT2=$(FIXTURE_TARGET="$TARGET" bash "$SUT" "$TARGET" "$REAL_SUITE" --baseline HEAD~1 2>&1); RC2=$?
97
+ [ "$RC2" -eq 0 ] && ok "L2 real anchor → exit 0" || no "L2 real anchor: rc=$RC2 (want 0)"
98
+ printf '%s' "$OUT2" | grep -q "되돌린 레인 (✅→❌, 앵커가 실제로 잡은 것): 1개" \
99
+ && ok "L2b report states exactly 1 flipped lane" || no "L2b report did not state 1 flipped lane"
100
+ printf '%s' "$OUT2" | grep -q "marker present in guard.txt" \
101
+ && ok "L2c report names the exact flipped label" || no "L2c flipped label not named in report"
102
+ AFTER2="$(TARGET_SHA_BEFORE)"
103
+ [ "$BEFORE2" = "$AFTER2" ] && ok "L2d target file restored after real-anchor run" \
104
+ || no "L2d target file NOT restored (before=$BEFORE2 after=$AFTER2)"
105
+
106
+ # L3 — frontier warning header always present (both suites above already exercised it; assert once)
107
+ printf '%s' "$OUT2" | grep -q "2607.22880" && ok "L3 frontier-warning citation present in output" \
108
+ || no "L3 frontier-warning citation missing"
109
+ printf '%s' "$OUT2" | grep -q "되돌린 파일(뮤턴트) 수: 1" \
110
+ && ok "L3b mutant-count line present (n=1 caveat)" || no "L3b mutant-count line missing"
111
+
112
+ # L4 — HARNESS ERROR: suite produces zero ✅/❌ lines → exit 10, and restore STILL happens.
113
+ # This is the lane that matters most: a crash on the baseline run is exactly the moment a naive
114
+ # implementation would leave the baseline content sitting in the working tree.
115
+ BEFORE4="$(TARGET_SHA_BEFORE)"
116
+ OUT4=$(FIXTURE_TARGET="$TARGET" bash "$SUT" "$TARGET" "$CRASH_SUITE" --baseline HEAD~1 2>&1); RC4=$?
117
+ [ "$RC4" -eq 10 ] && ok "L4 zero-label suite → exit 10 (harness error, not a silent pass)" \
118
+ || no "L4 zero-label suite: rc=$RC4 (want 10)"
119
+ AFTER4="$(TARGET_SHA_BEFORE)"
120
+ [ "$BEFORE4" = "$AFTER4" ] && ok "L4b target file restored even on the harness-error path" \
121
+ || no "L4b RESTORE FAILED ON HARNESS-ERROR PATH (before=$BEFORE4 after=$AFTER4) — this is the defect class this suite exists to catch"
122
+
123
+ # L5 — usage guards
124
+ OUT5=$(bash "$SUT" 2>&1); RC5=$?
125
+ [ "$RC5" -eq 2 ] && ok "L5 no args → exit 2" || no "L5 no args: rc=$RC5"
126
+ OUT5b=$(bash "$SUT" "$TARGET" 2>&1); RC5b=$?
127
+ [ "$RC5b" -eq 2 ] && ok "L5b missing suite arg → exit 2" || no "L5b missing suite arg: rc=$RC5b"
128
+ OUT5c=$(bash "$SUT" "$WORKROOT/does_not_exist.txt" "$REAL_SUITE" 2>&1); RC5c=$?
129
+ [ "$RC5c" -eq 2 ] && ok "L5c missing target file → exit 2" || no "L5c missing target file: rc=$RC5c"
130
+ OUT5d=$(bash "$SUT" "$TARGET" "$WORKROOT/does_not_exist.sh" 2>&1); RC5d=$?
131
+ [ "$RC5d" -eq 2 ] && ok "L5d missing suite file → exit 2" || no "L5d missing suite file: rc=$RC5d"
132
+ OUT5e=$(bash "$SUT" "$TARGET" "$REAL_SUITE" --baseline totally-bogus-ref 2>&1); RC5e=$?
133
+ [ "$RC5e" -eq 2 ] && ok "L5e bad --baseline ref → exit 2" || no "L5e bad baseline ref: rc=$RC5e"
134
+
135
+ # L6 — the restore path uses `git show`, never `git checkout <ref> -- <path>` (the latter STAGES
136
+ # the revert into the index — [[feedback_git_checkout_path_stages_the_revert]]). Assert the repo's
137
+ # index carries no staged change after a probe run, both directions.
138
+ ( cd "$SRC" && git diff --cached --quiet ) \
139
+ && ok "L6 no staged changes left in the index after any probe run" \
140
+ || no "L6 the index has staged changes — a checkout-based restore would do this"
141
+ ( cd "$SRC" && git status --porcelain ) | grep -q . \
142
+ && no "L6b working tree not clean after probes (git status is non-empty)" \
143
+ || ok "L6b working tree clean after all probes (file content == committed HEAD)"
144
+
145
+ echo "revert_probe lanes: $pass passed, $fail failed"
146
+ [ "$fail" -eq 0 ] || exit 1
@@ -138,9 +138,7 @@ PREMISE_FAILED=0
138
138
  # Human-readable on both platforms: GNU prints a date for %y, BSD's %Fm prints a raw epoch, so the
139
139
  # BSD branch asks for a formatted date instead — the whole point of this line is eyeballing.
140
140
  _mtime_h() {
141
- stat -c %y "$1" 2>/dev/null \
142
- || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$1" 2>/dev/null \
143
- || echo "?"
141
+ stat -c %y "$1" 2>/dev/null || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$1" 2>/dev/null || echo "?"
144
142
  }
145
143
 
146
144
  _assert_newer() { # $1=expected-newer $2=reference $3=lane label — returns 1 if premise fails
@@ -298,7 +296,7 @@ _carry_fixture() { # $1=오늘카드 본문 $2=fh_completed 본문(빈 문자
298
296
  printf '%s\n' "$card_body" > "$T/tracks/_meta/reference_next_session_starter.md"
299
297
  [ -n "$done_body" ] && printf '%s\n' "$done_body" > "$T/tracks/_meta/fh_completed_${TODAY}.md"
300
298
  # companion store: 어제 날짜로 커밋된 «이전 카드» 미러 하나
301
- FUT=$(date -v+2d '+%m-%d' 2>/dev/null || date -d '+2 days' '+%m-%d')
299
+ FUT=$(date -d '+2 days' '+%m-%d' 2>/dev/null || date -v+2d '+%m-%d')
302
300
  ( cd "$S" && git init -q . && git config user.email a@l && git config user.name a \
303
301
  && mkdir -p tracks-meta \
304
302
  && printf '기한 %s 미팅 안건 [B]\n' "$FUT" > tracks-meta/reference_next_session_starter.md \
@@ -307,7 +305,7 @@ _carry_fixture() { # $1=오늘카드 본문 $2=fh_completed 본문(빈 문자
307
305
  git commit -qm prior ) >/dev/null 2>&1
308
306
  printf '%s|%s' "$T" "$S"
309
307
  }
310
- _carry_future() { date -v+2d '+%m-%d' 2>/dev/null || date -d '+2 days' '+%m-%d'; }
308
+ _carry_future() { date -d '+2 days' '+%m-%d' 2>/dev/null || date -v+2d '+%m-%d'; }
311
309
 
312
310
  _carry_lane() { # $1=name $2=pattern $3=expect $4="REPO|STORE" $5=extra-env(옵션)
313
311
  local name="$1" pat="$2" expect="$3" pair="$4" env5="${5:-}" T S out hit
@@ -261,5 +261,22 @@ if [ -x "$ROOT/scripts/round/arm_blind_probe.sh" ]; then
261
261
  || no "L24 눈가림 실패 — 팔이 정답지를 읽을 수 있다"
262
262
  else no "L24 프로브 없음 — 검사 못 함(스킵 아님)"; fi
263
263
 
264
+ # ── L25 ⓒ 날짜 오염 통제 필드 (six_axis_review_2026-09-04 강화 #3) — 존재만 본다, 값은 안 본다.
265
+ # 이 러너가 판정을 안 낸다는 것이 헤더의 약속이라, 레인도 «필드가 있나»만 잰다.
266
+ OUTDIR="$WORKROOT/o25"
267
+ ( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=say \
268
+ bash "$SUT" --arm a --reps 1 --prompt p --out "$OUTDIR" ) >/dev/null 2>&1
269
+ META="$OUTDIR/a_r1.meta.tsv"
270
+ if [ -f "$META" ]; then
271
+ grep -q '^corpus_head_date ' "$META" && ok "L25a corpus_head_date field recorded" \
272
+ || no "L25a corpus_head_date field missing"
273
+ grep -q '^sim_model ' "$META" && ok "L25b sim_model field recorded" \
274
+ || no "L25b sim_model field missing"
275
+ grep -q '^sim_model_cutoff ' "$META" && ok "L25c sim_model_cutoff field recorded" \
276
+ || no "L25c sim_model_cutoff field missing"
277
+ else
278
+ no "L25 meta.tsv not written at all ($META)"
279
+ fi
280
+
264
281
  echo "sim_isolated_run lanes: $pass passed, $fail failed"
265
282
  [ "$fail" -eq 0 ] || exit 1
@@ -11,7 +11,7 @@
11
11
  # ⇒ 쓰는 쪽에서 나르게 한다. 이 레인은 «한 줄만 복사해도 딸려오나»를 축자로 검정한다.
12
12
  set -uo pipefail
13
13
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14
- S="$ROOT/scripts/context_continuity_score.sh"
14
+ S="${CCS_UNDER_TEST:-$ROOT/scripts/context_continuity_score.sh}" # 되돌림 프로브용 오버라이드(원본 사본을 가리켜 L29 가 빨개지는지 본다)
15
15
  T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
16
16
  pass=0; fail=0
17
17
  ok(){ printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
@@ -423,5 +423,31 @@ if grep -q 'DELIVER" = 1 \] && \[ "$arm" = ARM' "$S"; then
423
423
  else no "L28b 사본을 못 만들었다 — 되돌림 미검증(스킵 아님)"; fi
424
424
  else no "L28 deliver 분기를 못 찾았다 — 검사 못 함(스킵 아님)"; fi
425
425
 
426
+
427
+ # ── L29 (2026-09-02, 전파 — eligcheck 와 같은 `_tsv_pipe` 프로세스 치환 구멍) ──
428
+ SEAL29="$T/seal_deadbeef-abc_20260901-101010.md"; : > "$SEAL29"
429
+ run29(){ ( cd "$ROOT" && bash "$S" --seal "$SEAL29" --qset "$1" --reps 1 --out "$2" --rescore 2>&1 </dev/null ); }
430
+ # ── L29 🟥 파서 실패/0 행이 «🟢 계기 생존»으로 접히지 않는다 ─────────────────────────
431
+ # 실측(2026-09-02, 원본, bash 5.3): 값에 `|` 하나 · 빈 qset → 두 루프가 0 행 → ROWS 비어 →
432
+ # 판정선 분모 전부 0 → 「🟢 계기 생존」 rc 0. bash 3.2 는 unbound 로 rc 1 — 우연이지 게이트가 아니다.
433
+ PT="ZZL29POS$(printf '%s' 4419)"
434
+ printf 'q1\tpositive\t질문P\t%s\t\t\n' "$PT" > "$T/q6.tsv" # 6 열 컨트롤 (round2 실물 폭)
435
+ printf 'q1\tpositive\t질문|P\t%s\t\t\n' "$PT" > "$T/q_pipe.tsv"
436
+ : > "$T/q_empty.tsv"
437
+ mkdir -p "$T/o"; printf '답은 %s 입니다.\n' "$PT" > "$T/o/q1_ARM_r1.txt"; printf '모르겠습니다. 기록이 없습니다.\n' > "$T/o/q1_CTRL_r1.txt"
438
+ O=$(run29 "$T/q6.tsv" "$T/o"); rc=$?
439
+ [ "$rc" = 0 ] && printf '%s' "$O" | grep -q '🟢' && ok "L29a CONTROL 6열 정상 qset → 🟢 rc 0" || no "L29a 컨트롤이 죽었다 (rc=$rc) — 아래는 무의미"
440
+ O=$(run29 "$T/q_pipe.tsv" "$T/o"); rc=$?
441
+ # 🟥 «크래시»는 게이트가 아니다 — bash 3.2 는 set -u 의 `"${ROWS[@]}"` unbound 로 rc 1 에 죽어 rc≠0 이
442
+ # «우연히» 나온다(bash 5.3 = CI 에선 🟢 rc 0). 그래서 unbound 를 명시적으로 배제한다.
443
+ if [ "$rc" -ne 0 ] && ! printf '%s' "$O" | grep -q '🟢 계기 생존' && ! printf '%s' "$O" | grep -q 'unbound variable'; then ok "L29b 값에 '|' → 타입된 차단(rc≠0 ∧ 🟢 없음 ∧ 크래시 아님) [rc=$rc]"; else no "L29b 파서 실패가 «🟢 계기 생존»으로 접히거나 크래시로만 막힌다 [rc=$rc]"; fi
444
+ O=$(run29 "$T/q_empty.tsv" "$T/o"); rc=$?
445
+ if [ "$rc" -ne 0 ] && ! printf '%s' "$O" | grep -q '🟢 계기 생존' && ! printf '%s' "$O" | grep -q 'unbound variable'; then ok "L29c 빈 qset → 타입된 차단(rc≠0 ∧ 🟢 없음 ∧ 크래시 아님) [rc=$rc]"; else no "L29c 빈 qset 이 «🟢 계기 생존»으로 접히거나 크래시로만 막힌다 [rc=$rc]"; fi
446
+ # ── L29d 🟥 오염 게이트가 6 열 qset 에서 눈을 감지 않는다 (read 가 뒤 열을 tok 에 삼키는 결함) ──
447
+ # 실측(원본): 4 열 + tracked 토큰 → exit 5 · 6 열 같은 토큰 → 통과. 토큰은 «이 파일 자신»에 확실히 있는 낱말.
448
+ printf 'q1\tpositive\t질문P\tcontext_continuity\t\t\n' > "$T/q6c.tsv"
449
+ O=$(run29 "$T/q6c.tsv" "$T/o"); rc=$?
450
+ [ "$rc" = 5 ] && ok "L29d 6열 qset 의 tracked 토큰 → 오염 차단(5)" || no "L29d 6열 qset 에서 오염 게이트가 눈을 감는다 [rc=$rc]"
451
+
426
452
  echo "verdict watermark lanes: $pass passed, $fail failed"
427
453
  [ "$fail" -eq 0 ] || exit 1
@@ -132,7 +132,7 @@ done
132
132
  _probe_hits() {
133
133
  local pat="$1"; shift
134
134
  grep -lE -- "$pat" "$@" 2>/dev/null
135
- return "${PIPESTATUS[0]:-$?}"
135
+ return "${PIPESTATUS[0]:-$?}" # portability-noqa: shebang (line 1) pins bash — PIPESTATUS is a real array there, unlike zsh
136
136
  }
137
137
 
138
138
  # ── 1단계: 컨트롤. 살아있음을 증명하기 전에는 타깃을 인쇄하지 않는다 ──
@@ -179,7 +179,7 @@ MISS=0; N=0
179
179
  while IFS=$'\t' read -r kind pat label; do
180
180
  [ "${kind:-}" = "TARGET" ] || continue
181
181
  N=$((N+1))
182
- hits=$(_probe_hits "$pat" "${FILES[@]}" | wc -l | tr -d ' '); _rc=${PIPESTATUS[0]}
182
+ hits=$(_probe_hits "$pat" "${FILES[@]}" | wc -l | tr -d ' '); _rc=${PIPESTATUS[0]} # portability-noqa: shebang (line 1) pins bash — PIPESTATUS is a real array there, unlike zsh
183
183
  if [ "$_rc" -ge 2 ]; then
184
184
  printf " 🟥 %-46s 프로브 오류\n" "${label:-$pat}"
185
185
  BAD_PAT="${BAD_PAT:+$BAD_PAT, }TARGET/${label:-$pat}"
@@ -1470,6 +1470,18 @@ validate_soul_present_leg() { # $1 = marker path
1470
1470
  echo " «이 기록이 템플릿 자신인가»만 본다 — 채널 검사다."
1471
1471
  return 1
1472
1472
  fi
1473
+ # ── ①영혼 2칸 존재 검사 (advisory only, 2026-09-04 — six_axis_review 판정안 4) ─────
1474
+ # «성공 정의»와 «절대 안 함» 두 낱말(변형 포함)이 둘 다 있는지만 본다. Mechanization
1475
+ # Boundary: 존재만 — 두 칸이 다 있어도 내용이 옳다는 보장은 없고, 하나가 없어도 커밋을
1476
+ # 막지 않는다(위 non-vacuity 검사가 이미 최소 6낱말을 요구·차단한다 — 이건 그 위에 얹는
1477
+ # 별도 관측이지 새 차단이 아니다). 「없음」 선언 분기는 위에서 이미 return 했으므로 여기
1478
+ # 도달하지 않는다.
1479
+ _soul_missing=""
1480
+ printf '%s' "$body" | grep -qE '성공 ?정의' || _soul_missing="성공 정의"
1481
+ printf '%s' "$body" | grep -qE '절대 ?안 ?함' || _soul_missing="${_soul_missing:+$_soul_missing · }절대 안 함"
1482
+ if [ -n "$_soul_missing" ]; then
1483
+ echo " ⚠️ ①영혼 2칸 미달 — 빠진 낱말: $_soul_missing (advisory, 커밋은 안 막는다)"
1484
+ fi
1473
1485
  echo " ✅ ①영혼 present: $words word(s)"
1474
1486
  return 0
1475
1487
  }
@@ -2658,14 +2670,25 @@ if [ -n "$MD_STAGED" ]; then
2658
2670
  echo "[Pointers] Detail-pointer resolution (staged markdown)..."
2659
2671
  PTR_FAIL=0; PTR_CHECKED=0
2660
2672
  for f in $MD_STAGED; do
2661
- [ -f "$REPO_ROOT/$f" ] || continue
2662
- fdir="$(cd "$REPO_ROOT/$(dirname "$f")" && pwd)"
2663
- blocks="$(awk '
2673
+ # Read the STAGED blob, not the working-tree file. diff-filter=ACMR already guarantees $f
2674
+ # exists in the index, but a file can be staged and then deleted or further-edited on disk
2675
+ # before commit — `git commit` commits the INDEX, so `[ -f "$REPO_ROOT/$f" ] || continue`
2676
+ # here silently dropped the check on a committed broken pointer (deleted-on-disk case) and,
2677
+ # for a further-edited case, would have checked the wrong bytes entirely. Reproduced 2026-09-03:
2678
+ # stage a .md with a broken `**Detail**: See §X` pointer, `rm` it from disk (index untouched),
2679
+ # commit — PTR_CHECKED stayed 0 and the broken pointer landed silently.
2680
+ _md_blob="$(git -C "$REPO_ROOT" show ":$f" 2>/dev/null)" || {
2681
+ echo " ❌ $f: staged (diff-filter=ACMR) but unreadable from the index — cannot verify its Detail pointers"
2682
+ PTR_FAIL=1; continue
2683
+ }
2684
+ fdir="$(cd "$REPO_ROOT/$(dirname "$f")" 2>/dev/null && pwd)"
2685
+ [ -n "$fdir" ] || fdir="$REPO_ROOT/$(dirname "$f")"
2686
+ blocks="$(printf '%s\n' "$_md_blob" | awk '
2664
2687
  inblk && /^>/ { blk=blk" "$0; next }
2665
2688
  inblk { print blk; inblk=0; blk="" }
2666
2689
  /\*\*Detail\*\*: See/ { blk=$0; inblk=1; next }
2667
2690
  END { if(inblk) print blk }
2668
- ' "$REPO_ROOT/$f")"
2691
+ ')"
2669
2692
  [ -z "$blocks" ] && continue
2670
2693
  while IFS= read -r block; do
2671
2694
  [ -z "$block" ] && continue
@@ -50,7 +50,28 @@
50
50
  "built on a 46-PR-behind clone and manufactured a duplicate of an upstream lane. found→extend",
51
51
  "applies at repo level; this is its mechanical layer. Same advisory contract; fail-open on",
52
52
  "every uncertainty (non-repo, no upstream, fetch failure).",
53
- " bash scripts/test_stale_clone_guard_lanes.sh # expect: all known pairs hold (count self-reported)"
53
+ " bash scripts/test_stale_clone_guard_lanes.sh # expect: all known pairs hold (count self-reported)",
54
+ "",
55
+ "FOURTH GUARD, Bash matcher — backtick_guard.sh (added 2026-09-03, weekly_audit_2026-09-02 HIGH #1):",
56
+ "advisory when a Bash call carries a backtick inside a shell DOUBLE-QUOTING context — an unquoted",
57
+ "heredoc body (<<EOF) or a \"double-quoted string\". There a backtick is command substitution: the text",
58
+ "between the backticks is REPLACED by the command's output — empty when it names no command, so the",
59
+ "text is silently DELETED and the record commits with a hole (every record hook checks presence, not",
60
+ "completeness). Measured 7x 2026-08-10..09-02 with a resident memory rule that failed each time because",
61
+ "the actor's task had a different name than the rule's title. Quote-aware state machine, not a regex:",
62
+ "single-quoted text, quoted heredocs (<<'EOF'), and escaped backticks are CLEAN. Same advisory contract;",
63
+ "escalate with FH_BACKTICK_BLOCK=1; opt out with `# noqa: backtick` (whole payload).",
64
+ " bash scripts/test_backtick_guard_lanes.sh # expect: all known pairs hold (count self-reported)",
65
+ "",
66
+ "FIFTH GUARD, Edit|Write|Bash matcher — proposal_hook.sh (added 2026-09-03, identity ⑤ r4):",
67
+ "advisory when a verdict/guard line in scripts/**/*.sh or templates/*.sh is about to change: puts ONE",
68
+ "instruction into the model context — offer the user a known-pair control + degrade_direction_scan.sh in",
69
+ "one line. Measured: the same rule as a CLAUDE.md table row fired 1/15 at floor tier (r3); as this hook",
70
+ "9/9 relay on editing reps and 0/5 on a usage-string edit (r4). Quote-aware: an edit confined to quoted",
71
+ "strings does not fire. Bash path (sed -i / > / tee into a .sh) covered with a weaker rule (named",
72
+ "residual in the script header). Evidence row in .claude/.proposal_hook_events.tsv. Relaying an",
73
+ "injected instruction is not initiative — see the r4 RESULT for what this does and does not claim.",
74
+ " bash scripts/test_proposal_hook_lanes.sh # expect: all known pairs hold (count self-reported)"
54
75
  ],
55
76
  "project_settings_json": {
56
77
  "hooks": {
@@ -67,6 +88,11 @@
67
88
  "type": "command",
68
89
  "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/destructive_pre_gate.sh\"",
69
90
  "timeout": 5
91
+ },
92
+ {
93
+ "type": "command",
94
+ "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/backtick_guard.sh\"",
95
+ "timeout": 5
70
96
  }
71
97
  ]
72
98
  },
@@ -79,6 +105,16 @@
79
105
  "timeout": 20
80
106
  }
81
107
  ]
108
+ },
109
+ {
110
+ "matcher": "Edit|Write|Bash",
111
+ "hooks": [
112
+ {
113
+ "type": "command",
114
+ "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/proposal_hook.sh\"",
115
+ "timeout": 5
116
+ }
117
+ ]
82
118
  }
83
119
  ]
84
120
  }
@@ -1,38 +0,0 @@
1
- # fh-commons — Contention-Born Skill Habitat
2
-
3
- **A different layer of plugin from fh-meta.**
4
-
5
- fh-meta = meta-engineering skills for operating, diagnosing, and improving the harness itself
6
- fh-commons = general-purpose utility skills born from the contention layer, transplantable to any project
7
-
8
- ## Placement Criteria
9
-
10
- When the `contention-layer` skill harvests contentions and generates new skill candidates:
11
-
12
- | Determination | Destination |
13
- |---|---|
14
- | Harness engineering in nature | `fh-meta` |
15
- | Project-general · domain-agnostic | **`fh-commons`** (this plugin) |
16
- | Domain/team specific | field harvest (decided by the field team) |
17
-
18
- ## Skill List
19
-
20
- | Skill | Description | Contention Parent |
21
- |---|---|---|
22
- | `convergence-loop` | General-purpose gate reinforcement that replaces single-pass structures with a convergence loop of up to N rounds | harvest-loop (recurring single-pass-distrust pattern across hub gates) |
23
- | `deliberation` | Innovator → Devil-Advocate → Mediator 3-layer multi-perspective synthesis. Generates conditional verdicts without binary win/loss | Migrated from fh-meta (2026-05-23 — domain-agnostic general decision structure) |
24
- | `ko-tech-writer` | Korean technical-writing pipeline — register detection, venue calibration, translationese scan, honesty layering, number/claim gate, perceptual QA | Field-session harvest (2026-08-10 — conference tech-doc revision session, operator-driven shipping) |
25
-
26
- ## The `origin` Field
27
-
28
- Commons skills include the following fields in their SKILL.md frontmatter:
29
-
30
- ```yaml
31
- # Contention-born (extracted by contention-layer)
32
- origin: contention-layer
33
- contention-parents: [skill-A, skill-B]
34
-
35
- # Migrated from fh-meta (reclassified as general-purpose)
36
- origin: fh-meta
37
- migration: "YYYY-MM-DD — reason for migration"
38
- ```