@chrono-meta/fh-gate 1.4.72 → 1.4.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/.public-surface-patterns.defaults +44 -0
- package/.claude/rules/fh_4axis_gate.md +207 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +26 -2
- package/CATALOG.md +31 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +10 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +554 -0
- package/package.json +21 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/skills/context-doctor/SKILL.md +42 -4
- package/plugins/fh-meta/skills/context-doctor/SKILL_detail.md +38 -0
- package/scripts/chamber_candidate_collect.sh +223 -0
- package/scripts/degrade_direction_scan.sh +222 -0
- package/scripts/fh_session_load.sh +202 -0
- package/scripts/gate_pathspec_check.sh +166 -0
- package/scripts/prepush_guard_check.sh +374 -0
- package/scripts/psa_scan_lib.sh +153 -0
- package/scripts/public_surface_scan_files.sh +157 -0
- package/scripts/selfcheck.sh +16 -0
- package/scripts/session_close_check.sh +171 -0
- package/scripts/test_degrade_scan_shell_probes.sh +185 -0
- package/scripts/test_prepush_stdin_integrity.sh +119 -0
- package/scripts/universal_guard_check.sh +280 -0
- package/templates/.claude/rules/mcp_tool_gating.md +157 -0
- package/templates/.git-hooks/pre-commit +848 -0
- package/templates/.git-hooks/pre-push +585 -0
- package/templates/PRE-PUBLISH-CHECKLIST.md +85 -0
- package/templates/degrade_direction_scan.sh +222 -0
- package/templates/predelete_check.sh +72 -0
- package/templates/regression_guard.sh +563 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fh_session_load.sh — Mode D SessionStart companion-store freshness load (mechanical).
|
|
3
|
+
#
|
|
4
|
+
# WHY: the session-start companion-store load (refresh the private companion store + read its
|
|
5
|
+
# INDEX + card-vs-commit freshness) is documented in CLAUDE.local.md / modes_and_value.md
|
|
6
|
+
# §Session-start freshness as PROSE. Prose is salience-dependent: when the operator opens a
|
|
7
|
+
# session with an immediate task, the load silently does not fire and the agent operates on
|
|
8
|
+
# stale local memory. (Measured miss 2026-07-05: stale sidecar-tool version + a missed standing
|
|
9
|
+
# instruction, both because the companion refresh was skipped on task-first entry.) A
|
|
10
|
+
# SessionStart hook fires BEFORE the first user turn regardless of what the user types — so it
|
|
11
|
+
# closes the salience gap mechanically. This is the deferred hook in operational_adaptation.md
|
|
12
|
+
# §Guards whose measured revisit-trigger has now fired.
|
|
13
|
+
#
|
|
14
|
+
# WHAT: refresh the companion store, then emit a SHORT, IMPERATIVE freshness delta to stdout.
|
|
15
|
+
# A SessionStart hook's stdout is injected into the session context, so this block becomes
|
|
16
|
+
# unavoidable context the agent sees at turn 0.
|
|
17
|
+
#
|
|
18
|
+
# Graceful: if no companion store is configured (non-Mode-D user / ephemeral clone), emit
|
|
19
|
+
# nothing and exit 0 — this hook is a silent no-op outside Mode D. Offline-safe: refresh
|
|
20
|
+
# failure never blocks the session.
|
|
21
|
+
#
|
|
22
|
+
# Config (operator-local, opt-in): register in .claude/settings.local.json SessionStart and pass
|
|
23
|
+
# the companion-store path via the BE_DIR env in that gitignored registration (the public script
|
|
24
|
+
# hard-codes no private path). HUB_DIR overrides the hub path. Never commit the registration to
|
|
25
|
+
# the public settings.json — the hook is Mode-D-only.
|
|
26
|
+
|
|
27
|
+
set -uo pipefail
|
|
28
|
+
|
|
29
|
+
FH="${HUB_DIR:-${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}}"
|
|
30
|
+
|
|
31
|
+
# ── frontier-digest: 부재를 0으로 읽지 않는다 ────────────────────────────────────
|
|
32
|
+
# 왜: digest 는 launchd 로 매일 09:00 에 돌지만 **31회 중 6회(19%) 산출물 없이 끝났다**
|
|
33
|
+
# (2026-07-21 실측: exit 1 / 재시도 후에도 없음 / Attempt 에서 hang — 세 형태).
|
|
34
|
+
# 그런데 세션 시작은 "있으면 읽는다"만 했다 → **실패가 '오늘은 뉴스 없음'으로 읽혔다.**
|
|
35
|
+
# 부재와 실패는 0이 아니다. 로그가 있는데 산출물이 없으면 그건 부재가 아니라 FAILED 다.
|
|
36
|
+
# Portable mtime (epoch). GNU-first: on GNU/coreutils `stat -f %m` exits 0 with filesystem-format
|
|
37
|
+
# output (never reaching a BSD fallback), so probe `stat -c %Y` FIRST — on BSD/macOS it errors and
|
|
38
|
+
# falls through to `-f %m`. Always echoes a numeric value (0 on total failure) so `-gt` never breaks.
|
|
39
|
+
# (codex cross-family review 2026-07-05 [MED]: BSD-first order silently mis-parsed on GNU.)
|
|
40
|
+
_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
41
|
+
|
|
42
|
+
_FD_LOG="$FH/tracks/_meta/logs/frontier_digest_$(date +%Y_%m_%d).log"
|
|
43
|
+
_FD_LOCK="$FH/tracks/_meta/logs/.frontier_digest.lock"
|
|
44
|
+
# 스케줄 전 부재 ≠ 실패: 잡은 launchd 로 09:00 에 돈다(com.forge-harness.frontier-digest.plist).
|
|
45
|
+
# 2026-07-23 08:07 세션이 "잡이 안 돌았을 수 있다" 경고를 받았고 잡은 09:00 에 정상 완주 —
|
|
46
|
+
# 경고가 나중에 읽히면 오보처럼 보인다. 그래서 ① 스케줄 전엔 경고하지 않고 ② 모든 분기에
|
|
47
|
+
# 발화 시각을 스탬프하고 ③ 러너 생존 신호(로그 갱신 or 락)가 있는 동안은 실패 판정을 보류한다.
|
|
48
|
+
_FD_SCHED=900 # 09:00, HHMM 를 10진 정수로
|
|
49
|
+
_FD_NOW="${FD_NOW_HHMM:-$(date +%H%M)}" # FD_NOW_HHMM = known-pair 캘리브레이션 전용(숫자 4자리만)
|
|
50
|
+
_FD_STAMP="$(date +%H:%M) 기준"
|
|
51
|
+
# 존재 판정은 러너 digest_ready 와 동일 술어(glob + -size +1k). 정확명 [ -f ] 는 러너와 관대함이
|
|
52
|
+
# 갈린다 — partial 파일(>0 <1k)이 성공으로 오독되고, suffix 착지가 영구 오경보가 된다
|
|
53
|
+
# (divergent-leniency: 같은 상태를 두 술어가 다르게 읽으면 한쪽 결과가 무음으로 샌다).
|
|
54
|
+
_fd_ready() { find "$FH/tracks/_meta" -maxdepth 1 -name "frontier_digest_$(date +%Y_%m_%d)*.md" -size +1k 2>/dev/null | grep -q .; }
|
|
55
|
+
if _fd_ready; then
|
|
56
|
+
:
|
|
57
|
+
elif [ "$((10#$_FD_NOW))" -lt "$_FD_SCHED" ]; then
|
|
58
|
+
echo "ℹ️ [frontier-digest] 오늘 digest 는 09:00 예정 — 아직 전이다($_FD_STAMP). 부재는 정상."
|
|
59
|
+
elif [ -f "$_FD_LOG" ]; then
|
|
60
|
+
_FD_LOG_AGE=$(( $(date +%s) - $(_mtime "$_FD_LOG") ))
|
|
61
|
+
# 러너 생존 신호 2종 — 어느 쪽이든 있으면 FAILED 대신 보류:
|
|
62
|
+
# ① 로그 최근 갱신(임계 2100s > 러너 최장 침묵창 = watchdog 1800s)
|
|
63
|
+
# ② 락 존재이면서 비-stale — stale 임계는 러너 자신의 락-브레이크 술어(-mmin +240)와 동일.
|
|
64
|
+
# 락은 슬립-복귀 직후(로그는 오래됐지만 러너가 살아 재개하는 케이스)를 커버한다.
|
|
65
|
+
_FD_ALIVE=""
|
|
66
|
+
if [ -d "$_FD_LOCK" ] && [ -z "$(find "$_FD_LOCK" -maxdepth 0 -mmin +240 2>/dev/null)" ]; then
|
|
67
|
+
_FD_ALIVE="락 존재"
|
|
68
|
+
fi
|
|
69
|
+
[ "$_FD_LOG_AGE" -lt 2100 ] && _FD_ALIVE="${_FD_ALIVE:+$_FD_ALIVE · }로그 ${_FD_LOG_AGE}s 전 갱신"
|
|
70
|
+
if [ -n "$_FD_ALIVE" ]; then
|
|
71
|
+
echo "ℹ️ [frontier-digest] 잡이 아직 돌고 있는 중일 수 있다($_FD_STAMP, $_FD_ALIVE) — 실패 판정 보류, 나중에 재확인."
|
|
72
|
+
else
|
|
73
|
+
echo "⚠️ [frontier-digest] 오늘 잡은 돌았는데 **산출물이 없다**($_FD_STAMP) — 부재가 아니라 실패다."
|
|
74
|
+
echo " 마지막 로그: $(tail -1 "$_FD_LOG" 2>/dev/null | cut -c1-90)"
|
|
75
|
+
echo " → 수동 재실행하거나 실패 원인을 보라. '오늘은 뉴스 없음'으로 읽지 말 것."
|
|
76
|
+
fi
|
|
77
|
+
else
|
|
78
|
+
echo "⚠️ [frontier-digest] 스케줄(09:00) 지났는데 로그도 산출물도 없다($_FD_STAMP) — 잡이 아예 안 돌았을 수 있다(launchd 확인)."
|
|
79
|
+
fi
|
|
80
|
+
BE="${BE_DIR:-}" # companion-store path — supplied by the gitignored hook registration; no public default.
|
|
81
|
+
|
|
82
|
+
# Non-Mode-D / no companion store → silent no-op (this is the majority path for public users).
|
|
83
|
+
[ -d "$BE/.git" ] || exit 0
|
|
84
|
+
|
|
85
|
+
# (_mtime is defined above the frontier-digest block — single definition, both sections use it.)
|
|
86
|
+
|
|
87
|
+
# 1) Refresh the companion store — fail-fast, never block the first turn, never mutate into a
|
|
88
|
+
# merge/conflict. (codex cross-family review 2026-07-05 [HIGH]/[MED].)
|
|
89
|
+
# - fail-fast env: no credential/SSH/host-key prompts can hang SessionStart.
|
|
90
|
+
# - fetch + merge --ff-only: a fast-forward is the only safe hook mutation; a diverged companion
|
|
91
|
+
# simply does not advance (no merge commit, no conflict state left behind) and we say so.
|
|
92
|
+
export GIT_TERMINAL_PROMPT=0
|
|
93
|
+
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new}"
|
|
94
|
+
|
|
95
|
+
# Hard wall-clock deadline on the ONLY network step. ConnectTimeout bounds the handshake, but a
|
|
96
|
+
# slow/stalled TRANSFER after connect has no bound — measured 2026-07-12: SessionStart worst-case
|
|
97
|
+
# 17.5s with 2 hook-timeout kills, all attributable to the fetch. perl-alarm is the portable
|
|
98
|
+
# watchdog (macOS ships no coreutils `timeout`); on overrun the fetch dies and the offline branch
|
|
99
|
+
# reports honestly. FH_FETCH_DEADLINE overrides (seconds). If perl is absent the wrapper degrades
|
|
100
|
+
# to running the command with NO deadline — a missing watchdog must never become a permanently
|
|
101
|
+
# skipped fetch misreported as "offline" (challenger catch 2026-07-12).
|
|
102
|
+
if command -v perl >/dev/null 2>&1; then
|
|
103
|
+
_deadline() { perl -e 'alarm shift @ARGV; exec @ARGV' "$@"; }
|
|
104
|
+
else
|
|
105
|
+
_deadline() { shift; "$@"; }
|
|
106
|
+
fi
|
|
107
|
+
PULL_NOTE=""
|
|
108
|
+
if _deadline "${FH_FETCH_DEADLINE:-8}" git -C "$BE" fetch --quiet >/dev/null 2>&1; then
|
|
109
|
+
if git -C "$BE" merge --ff-only --quiet >/dev/null 2>&1; then
|
|
110
|
+
PULL_NOTE="fetched + fast-forwarded"
|
|
111
|
+
else
|
|
112
|
+
PULL_NOTE="fetched but NOT fast-forward (companion diverged — read local + newest remote)"
|
|
113
|
+
fi
|
|
114
|
+
else
|
|
115
|
+
PULL_NOTE="fetch skipped (offline or deadline hit — read local state)"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# 2) Session card date (the pointer the operator's close chain writes last).
|
|
119
|
+
CARD="$FH/tracks/_meta/reference_next_session_starter.md"
|
|
120
|
+
CARD_EPOCH=0
|
|
121
|
+
[ -f "$CARD" ] && CARD_EPOCH="$(_mtime "$CARD")"
|
|
122
|
+
|
|
123
|
+
# 3) Companion files NEWER than the card, in the surfaces that carry landed results/handoffs.
|
|
124
|
+
# (paper-signals = completed experiments; handoff = cross-session/cross-machine; tracks-meta
|
|
125
|
+
# = synced session meta.) These are exactly what a stale card fails to point at.
|
|
126
|
+
NEWER=""
|
|
127
|
+
for sub in paper-signals handoff tracks-meta digests; do
|
|
128
|
+
d="$BE/$sub"
|
|
129
|
+
[ -d "$d" ] || continue
|
|
130
|
+
while IFS= read -r f; do
|
|
131
|
+
[ -n "$f" ] || continue
|
|
132
|
+
fe="$(_mtime "$f")"
|
|
133
|
+
if [ "${fe:-0}" -gt "${CARD_EPOCH:-0}" ]; then
|
|
134
|
+
NEWER="${NEWER} - ${f#$BE/}\n"
|
|
135
|
+
fi
|
|
136
|
+
done <<EOF
|
|
137
|
+
$(find "$d" -type f -name '*.md' -maxdepth 2 2>/dev/null)
|
|
138
|
+
EOF
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
# 3b) Handoff/signal STATUS map — mtime-INDEPENDENT (patched 2026-07-10).
|
|
142
|
+
# WHY: the NEWER-than-card list (step 3) has a permanent blind spot — a status stamp
|
|
143
|
+
# (DONE/SUPERSEDED/RESOLVED) can land in the companion store, then the card gets rewritten
|
|
144
|
+
# WITHOUT reconciling that item; from then on the stamped file is "older than the card"
|
|
145
|
+
# forever and step 3 never surfaces it again. (Measured miss 2026-07-10: a Qwen heavy
|
|
146
|
+
# handoff stamped DONE 07-09 stayed listed as "awaiting RUN" in the card through a later
|
|
147
|
+
# card rewrite — company sessions push results to the companion store but never run the
|
|
148
|
+
# local close chain, so the card's ⑤ update is the ONLY reconcile point and it was prose.)
|
|
149
|
+
# FIX: emit ALL frontmatter status lines from handoff/ + paper-signals/ every session,
|
|
150
|
+
# regardless of mtime, so the turn-0 agent can mechanically cross-check card carry items.
|
|
151
|
+
STATUS_MAP=""
|
|
152
|
+
for sub in handoff paper-signals; do
|
|
153
|
+
d="$BE/$sub"
|
|
154
|
+
[ -d "$d" ] || continue
|
|
155
|
+
while IFS= read -r f; do
|
|
156
|
+
[ -n "$f" ] || continue
|
|
157
|
+
s="$(head -15 "$f" 2>/dev/null | grep -iE '^ *status:' | head -1 | sed 's/^ *//')"
|
|
158
|
+
[ -n "$s" ] || continue
|
|
159
|
+
# Match on the status VALUE's leading word only — a substring match anywhere in the line
|
|
160
|
+
# false-positives on prose like "Remaining for DONE:" inside a PARTIAL status.
|
|
161
|
+
v="$(printf '%s' "$s" | sed -E 's/^[Ss][Tt][Aa][Tt][Uu][Ss]: *//')"
|
|
162
|
+
case "$v" in
|
|
163
|
+
DONE*|SUPERSEDED*|RESOLVED*|CLOSED*) STATUS_MAP="${STATUS_MAP} - ${f#$BE/} → ${s}\n" ;;
|
|
164
|
+
esac
|
|
165
|
+
done <<EOF
|
|
166
|
+
$(find "$d" -type f -name '*.md' -maxdepth 2 2>/dev/null)
|
|
167
|
+
EOF
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
# 4) INDEX.md live pointers (the operator's wiki TOC — read-first per CLAUDE.local.md).
|
|
171
|
+
INDEX_HEAD=""
|
|
172
|
+
if [ -f "$BE/INDEX.md" ]; then
|
|
173
|
+
INDEX_HEAD="$(grep -iE 'live pointer|Live pointers' -A 8 "$BE/INDEX.md" 2>/dev/null | head -10)"
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# 5) Emit the freshness block (short + imperative). Only speak if there is something to say.
|
|
177
|
+
{
|
|
178
|
+
echo "🔄 [FH SessionStart] companion-store freshness — $PULL_NOTE."
|
|
179
|
+
if [ -n "$NEWER" ]; then
|
|
180
|
+
echo "⚠️ NEWER THAN SESSION CARD — READ THESE BEFORE ACTING (card may be stale):"
|
|
181
|
+
printf "%b" "$NEWER"
|
|
182
|
+
else
|
|
183
|
+
echo " (no companion files newer than the session card)"
|
|
184
|
+
fi
|
|
185
|
+
if [ -n "$STATUS_MAP" ]; then
|
|
186
|
+
echo "── handoff/signal STATUS map (mtime-independent — closed items) ──"
|
|
187
|
+
printf "%b" "$STATUS_MAP"
|
|
188
|
+
echo "→ CROSS-CHECK: any item above that the session card still lists as open/awaiting = stale card line. Fix it in this session's card update (⑤)."
|
|
189
|
+
fi
|
|
190
|
+
if [ -n "$INDEX_HEAD" ]; then
|
|
191
|
+
echo "── INDEX.md live pointers ──"
|
|
192
|
+
echo "$INDEX_HEAD"
|
|
193
|
+
fi
|
|
194
|
+
echo "Reminder: this is the Mode D auto-read (CLAUDE.local.md §Session-start companion load) —"
|
|
195
|
+
echo "it fires even when the first user message is a task. Do not treat 'pulled' as 'read'."
|
|
196
|
+
} 2>/dev/null
|
|
197
|
+
|
|
198
|
+
# 6) Substrate-jump detection (structure-enforcing — version drift lives outside any session's
|
|
199
|
+
# context boundary; silent when nothing changed). Detector, never a gate.
|
|
200
|
+
[ -x "$FH/scripts/substrate_jump_detector.sh" ] && bash "$FH/scripts/substrate_jump_detector.sh" "$FH" 2>/dev/null
|
|
201
|
+
|
|
202
|
+
exit 0
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gate_pathspec_check.sh — known-pair regression anchor for gate PATH COVERAGE.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS
|
|
5
|
+
# The gate-locality class has now recurred four times: scripts/ (2026-06-26), AGENTS.md
|
|
6
|
+
# inheritance (#111/#117), agent definitions (2026-06-27), and SKILL_detail.md (2026-07-26).
|
|
7
|
+
# Every instance had the same shape: an asset class the canonical rule *declared* covered, which
|
|
8
|
+
# the gate implementation's path term did not actually match — and the miss rendered as PASS,
|
|
9
|
+
# because "no file matched" and "all files passed" are indistinguishable downstream.
|
|
10
|
+
#
|
|
11
|
+
# The 07-26 instance was the sharpest: the term was the literal `SKILL\.md`, and the string
|
|
12
|
+
# `SKILL_detail.md` does not contain `SKILL.md` (the underscore breaks it). 17 files, 208,710 B,
|
|
13
|
+
# 27.7% of the skill-spec surface, 16 of 17 holding fenced code blocks — ungated. It leaked twice
|
|
14
|
+
# for real (371c04f, e661931: single-file edits to a GATE SKILL's own behavioral spec).
|
|
15
|
+
#
|
|
16
|
+
# So this is not a style check. It is the mechanical anchor for the fix, per the FH rule that a
|
|
17
|
+
# harness edit is a draft until a check fails when the mistake recurs.
|
|
18
|
+
#
|
|
19
|
+
# METHOD — known-pair, per CLAUDE.md §Instrument-Calibration. Every case asserts BOTH directions:
|
|
20
|
+
# a known-positive that MUST match and a known-negative that MUST NOT. A checker that only ever
|
|
21
|
+
# confirms positives cannot tell "covers everything" from "matches everything".
|
|
22
|
+
#
|
|
23
|
+
# Usage: bash scripts/gate_pathspec_check.sh # exit 0 = all pairs hold, 1 = a pair broke
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
27
|
+
HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
|
|
28
|
+
GUARD="$REPO_ROOT/templates/regression_guard.sh"
|
|
29
|
+
|
|
30
|
+
fail=0
|
|
31
|
+
pass=0
|
|
32
|
+
|
|
33
|
+
# Extract a live regex from the implementation instead of restating it here. A copy would drift
|
|
34
|
+
# from the thing it claims to verify — which is the very defect class this file exists to catch.
|
|
35
|
+
extract_term() { # $1 = file, $2 = variable-assignment marker
|
|
36
|
+
grep -A2 "^${2}=" "$1" 2>/dev/null | grep -oE '\| grep -E "[^"]+"' | head -1 \
|
|
37
|
+
| sed -E 's/^\| grep -E "//; s/"$//'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
check() { # $1 = label, $2 = regex, $3 = should-match path, $4 = should-NOT-match path
|
|
41
|
+
local label="$1" re="$2" pos="$3" neg="$4" ok=1 rc
|
|
42
|
+
# grep exits 2 on a MALFORMED regex. `if ! grep -q` would negate that 2 into "true" and report
|
|
43
|
+
# it as "known-positive not covered" — fail-closed in direction, but it misnames the cause, and
|
|
44
|
+
# a checker that misreports why it failed sends the next reader to fix the wrong thing.
|
|
45
|
+
echo "$pos" | grep -qE "$re"; rc=$?
|
|
46
|
+
if [ "$rc" -eq 2 ]; then
|
|
47
|
+
echo " ❌ $label — extracted pattern is not a valid regex (instrument error, NOT a coverage result)"
|
|
48
|
+
echo " pattern: $re"
|
|
49
|
+
fail=$((fail + 1)); return
|
|
50
|
+
fi
|
|
51
|
+
[ "$rc" -ne 0 ] && { echo " ❌ $label — known-POSITIVE not covered: $pos"; ok=0; }
|
|
52
|
+
if echo "$neg" | grep -qE "$re"; then
|
|
53
|
+
echo " ❌ $label — known-NEGATIVE wrongly covered: $neg"; ok=0
|
|
54
|
+
fi
|
|
55
|
+
if [ "$ok" -eq 1 ]; then
|
|
56
|
+
echo " ✅ $label"; pass=$((pass + 1))
|
|
57
|
+
else
|
|
58
|
+
fail=$((fail + 1))
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
echo "gate_pathspec_check — known-pair coverage anchors"
|
|
63
|
+
echo
|
|
64
|
+
|
|
65
|
+
# ── 1. pre-commit HEAVY term ──────────────────────────────────────────────────
|
|
66
|
+
HEAVY_RE="$(extract_term "$HOOK" HEAVY)"
|
|
67
|
+
if [ -z "$HEAVY_RE" ]; then
|
|
68
|
+
echo " ❌ could not extract HEAVY term from $HOOK — instrument error, NOT a pass"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
# The 07-26 regression: detail files must be HEAVY. Negative: a tracks/ record must not be.
|
|
72
|
+
check "HEAVY covers SKILL_detail.md" "$HEAVY_RE" \
|
|
73
|
+
"plugins/fh-meta/skills/frontier-digest/SKILL_detail.md" \
|
|
74
|
+
"tracks/_meta/fh_signal_2026-07-26_ai.md"
|
|
75
|
+
check "HEAVY still covers SKILL.md" "$HEAVY_RE" \
|
|
76
|
+
"plugins/fh-meta/skills/frontier-digest/SKILL.md" \
|
|
77
|
+
"README.md"
|
|
78
|
+
# Prior gate-locality instances — anchored so a future edit cannot silently drop them.
|
|
79
|
+
check "HEAVY covers agent definitions (seam #3)" "$HEAVY_RE" \
|
|
80
|
+
"plugins/fh-meta/agents/challenger.md" \
|
|
81
|
+
"docs/README.md"
|
|
82
|
+
check "HEAVY covers scripts/*.sh (seam #1)" "$HEAVY_RE" \
|
|
83
|
+
"scripts/gate_pathspec_check.sh" \
|
|
84
|
+
"tracks/_audit/session_2026_07_26_agentsmith-sister.md"
|
|
85
|
+
|
|
86
|
+
# ── 2. regression_guard GUARD_PATHSPEC ────────────────────────────────────────
|
|
87
|
+
# Read the array as the guard itself defines it; match with the same glob semantics git uses.
|
|
88
|
+
# NOTE: no `mapfile` — macOS ships bash 3.2, where it does not exist. This is the documented
|
|
89
|
+
# bash-3.2 portability class; a 4.x-only builtin here would make the anchor itself the thing that
|
|
90
|
+
# breaks on the operator's own machine.
|
|
91
|
+
SPEC=()
|
|
92
|
+
while IFS= read -r line; do
|
|
93
|
+
[ -n "$line" ] && SPEC+=("$line")
|
|
94
|
+
done < <(sed -n '/^GUARD_PATHSPEC=(/,/^)/p' "$GUARD" | grep -oE "'[^']+'" | tr -d "'")
|
|
95
|
+
if [ "${#SPEC[@]:-0}" -eq 0 ]; then
|
|
96
|
+
echo " ❌ could not extract GUARD_PATHSPEC from $GUARD — instrument error, NOT a pass"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
spec_matches() { # $1 = path
|
|
100
|
+
local p="$1" g
|
|
101
|
+
for g in "${SPEC[@]}"; do
|
|
102
|
+
# shellcheck disable=SC2254
|
|
103
|
+
case "$p" in $g) return 0 ;; esac
|
|
104
|
+
done
|
|
105
|
+
return 1
|
|
106
|
+
}
|
|
107
|
+
for pair in \
|
|
108
|
+
"plugins/fh-meta/skills/frontier-digest/SKILL_detail.md|tracks/_meta/x.md|PATHSPEC covers SKILL_detail.md" \
|
|
109
|
+
"plugins/fh-meta/skills/frontier-digest/SKILL.md|README.md|PATHSPEC still covers SKILL.md" \
|
|
110
|
+
"CLAUDE.md|CLAUDE.local.md|PATHSPEC covers CLAUDE.md but not the local override"
|
|
111
|
+
do
|
|
112
|
+
IFS='|' read -r pos neg label <<< "$pair"
|
|
113
|
+
ok=1
|
|
114
|
+
spec_matches "$pos" || { echo " ❌ $label — known-POSITIVE not covered: $pos"; ok=0; }
|
|
115
|
+
spec_matches "$neg" && { echo " ❌ $label — known-NEGATIVE wrongly covered: $neg"; ok=0; }
|
|
116
|
+
if [ "$ok" -eq 1 ]; then echo " ✅ $label"; pass=$((pass + 1)); else fail=$((fail + 1)); fi
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
# ── 3. Canonical-vs-implementation parity ─────────────────────────────────────
|
|
120
|
+
# regression_guard.sh's own comment: "두 목록이 갈리면 갈린 쪽이 조용히 무검사 구간이 된다."
|
|
121
|
+
# Anchor that warning mechanically for the asset class that just broke.
|
|
122
|
+
CANON="$REPO_ROOT/.claude/rules/fh_4axis_gate.md"
|
|
123
|
+
# Scope the match to the ASSET-LIST sentence, not the whole file. A bare whole-file grep would go
|
|
124
|
+
# green on a line that says "SKILL_detail.md is excluded" — i.e. it would certify parity against
|
|
125
|
+
# documentation that contradicts the code. Match the declaration line itself.
|
|
126
|
+
if grep -q 'Whenever the AI modifies FH assets.*SKILL_detail\.md' "$CANON" 2>/dev/null; then
|
|
127
|
+
echo " ✅ canonical rule declares SKILL_detail.md (in the asset-list line)"; pass=$((pass + 1))
|
|
128
|
+
else
|
|
129
|
+
echo " ❌ canonical rule ($CANON) no longer declares SKILL_detail.md — the two lists diverged,"
|
|
130
|
+
echo " which is exactly the silent no-check condition this anchor exists to prevent."
|
|
131
|
+
fail=$((fail + 1))
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# ── 4. Enumeration sweep — the anti-guessing check ────────────────────────────
|
|
135
|
+
# Every fix above answers "is THIS name covered?" — which only ever closes the names someone
|
|
136
|
+
# thought of. This one inverts it: enumerate what actually EXISTS under plugins/*/skills/ and
|
|
137
|
+
# assert the HEAVY term covers all of it. A new companion-file convention (SKILL_summary.md,
|
|
138
|
+
# a nested docs/ page, a deeper skill directory) then fails HERE, at introduction, instead of
|
|
139
|
+
# waiting for someone to notice the naming gap years later. Reality is the input, not a guess.
|
|
140
|
+
# (Adversarial credit: an Axis-2 sidecar pass argued the name-by-name fixes could not, in
|
|
141
|
+
# principle, close the class — correct, and this is the answer to it.)
|
|
142
|
+
uncovered=""
|
|
143
|
+
while IFS= read -r f; do
|
|
144
|
+
[ -z "$f" ] && continue
|
|
145
|
+
echo "$f" | grep -qE "$HEAVY_RE" || uncovered="$uncovered$f
|
|
146
|
+
"
|
|
147
|
+
done < <(cd "$REPO_ROOT" && find plugins -path '*/skills/*' -name '*.md' -type f 2>/dev/null | sort)
|
|
148
|
+
if [ -z "$uncovered" ]; then
|
|
149
|
+
echo " ✅ enumeration: every .md under plugins/*/skills/ is covered by the HEAVY term"
|
|
150
|
+
pass=$((pass + 1))
|
|
151
|
+
else
|
|
152
|
+
echo " ❌ enumeration: files exist under plugins/*/skills/ that NO gate term covers —"
|
|
153
|
+
printf '%s' "$uncovered" | sed 's/^/ /'
|
|
154
|
+
echo " Either widen the gate term, or state in fh_4axis_gate.md why this class is exempt."
|
|
155
|
+
fail=$((fail + 1))
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
echo
|
|
159
|
+
if [ "$fail" -eq 0 ]; then
|
|
160
|
+
echo "gate_pathspec_check: PASS ($pass pairs)"
|
|
161
|
+
exit 0
|
|
162
|
+
fi
|
|
163
|
+
echo "gate_pathspec_check: FAIL ($fail broken, $pass ok)"
|
|
164
|
+
echo "A gate path term stopped covering an asset class it is declared to cover."
|
|
165
|
+
echo "Do NOT relax the anchor to make it green — fix the term, or retire the pair deliberately."
|
|
166
|
+
exit 1
|