@chrono-meta/fh-gate 1.4.95 → 1.4.96
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-plugin/marketplace.json +2 -2
- package/AGENTS.md +18 -0
- package/CHEATSHEET.md +1 -1
- package/knowledge/shared/harness-core/fh_detail_protocols.md +12 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +7 -4
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +43 -1
- package/package.json +6 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/agents/quench-challenger.md +49 -23
- package/plugins/fh-commons/skills/convergence-loop/SKILL.md +14 -0
- package/plugins/fh-commons/skills/deliberation/SKILL.md +14 -0
- package/plugins/fh-commons/skills/mcp-circuit-breaker/SKILL.md +10 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +36 -0
- package/plugins/fh-meta/agents/beginner.md +4 -1
- package/plugins/fh-meta/agents/challenger.md +7 -1
- package/plugins/fh-meta/agents/expert.md +1 -1
- package/plugins/fh-meta/agents/fact-checker.md +7 -1
- package/plugins/fh-meta/agents/hub-persona-auditor.md +2 -1
- package/plugins/fh-meta/agents/main-player.md +4 -1
- package/plugins/fh-meta/agents/persona-innovator.md +10 -2
- package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
- package/plugins/fh-meta/skills/apex-review/SKILL.md +5 -0
- package/plugins/fh-meta/skills/asset-placement-gate/SKILL.md +38 -8
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +16 -2
- package/plugins/fh-meta/skills/context-doctor/SKILL_detail.md +45 -10
- package/plugins/fh-meta/skills/corpus-grounding-expander/SKILL.md +14 -5
- package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +93 -30
- package/plugins/fh-meta/skills/deep-clarify/SKILL.md +28 -9
- package/plugins/fh-meta/skills/fh/SKILL.md +4 -0
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +64 -8
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +20 -7
- package/plugins/fh-meta/skills/goal-quench/SKILL.md +48 -15
- package/plugins/fh-meta/skills/goal-quench/SKILL_detail.md +58 -11
- package/plugins/fh-meta/skills/harness-doctor/SKILL_detail.md +109 -33
- package/plugins/fh-meta/skills/harvest-loop/SKILL.md +6 -1
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +126 -17
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +50 -14
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +26 -7
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +68 -21
- package/plugins/fh-meta/skills/memory-hygiene/SKILL.md +64 -17
- package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +38 -4
- package/plugins/fh-meta/skills/persona-roster-expander/SKILL.md +15 -7
- package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +39 -11
- package/plugins/fh-meta/skills/plugin-recommender/SKILL_detail.md +24 -7
- package/plugins/fh-meta/skills/prompt-regression/SKILL.md +54 -11
- package/plugins/fh-meta/skills/salience-splitter/SKILL.md +120 -7
- package/plugins/fh-meta/skills/salience-splitter/SKILL_detail.md +46 -13
- package/plugins/fh-meta/skills/sim-conductor/SKILL_detail.md +28 -3
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +3 -1
- package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +72 -14
- package/scripts/count_check.sh +47 -1
- package/scripts/degrade_direction_scan.sh +276 -6
- package/scripts/degrade_probe_capability.sh +105 -0
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/psa_probe_capability.sh +78 -0
- package/scripts/public_surface_scan_files.sh +8 -0
- package/scripts/selfcheck.sh +15 -0
- package/scripts/test_capability_entrypoint_shipping.sh +132 -0
- package/scripts/test_count_check_readme_format_lanes.sh +75 -0
- package/scripts/test_degrade_scan_shell_probes.sh +415 -0
- package/scripts/validate_yaml.sh +146 -0
- package/templates/degrade_direction_scan.sh +276 -6
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# degrade_probe_capability.sh — degrade_direction_scan 의 **capability 진입점**.
|
|
3
|
+
#
|
|
4
|
+
# node A(psa) 와 다른 계보의 다른 질문이다: 내용이 아니라 «판정이 어느 방향으로
|
|
5
|
+
# 무너지나»(fail-open 여부)를 본다. 스캔 로직은 재구현하지 않고 기존 스캐너를 부른다.
|
|
6
|
+
#
|
|
7
|
+
# verdict enum (declared): 0=CLEAN 2=FINDINGS 3=NO_TARGET 10=HARNESS_ERROR
|
|
8
|
+
# 3 은 원 스캐너가 exit 0 으로 뭉개던 «스캔할 py/sh 대상이 없다» 를 분리한 값이다 —
|
|
9
|
+
# 그 0 은 «깨끗» 이 아니라 «시작조차 안 함» 이고, 정확히 §ⓑ.4 B1 이 막는 형태다.
|
|
10
|
+
#
|
|
11
|
+
# 🟥 이 파일의 초판이 이 레포의 `scripts/` 를 통째로 지웠다 (2026-08-11, 실발생).
|
|
12
|
+
# 기전: `TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT` 로 정리 트랩을 건 뒤,
|
|
13
|
+
# 무인자 분기에서 **같은 변수에 실제 레포 경로를 재대입**했다 → 트랩이 종료 시
|
|
14
|
+
# `rm -rf $REPO_ROOT/scripts` 를 실행. 트랩은 «내가 만든 것» 이 아니라
|
|
15
|
+
# «그 변수가 지금 가리키는 것» 을 지운다.
|
|
16
|
+
# **처방(구조적)**: 정리 대상(OWNED_TMP)과 스캔 대상(TARGET)을 **다른 변수로 분리**하고,
|
|
17
|
+
# 트랩은 OWNED_TMP 만 본다. OWNED_TMP 는 최초 대입 후 절대 재대입하지 않는다.
|
|
18
|
+
# 그리고 트랩 안에서 경로가 mktemp 산출물인지 한 번 더 검문한다(두 번째 방벽).
|
|
19
|
+
set -o pipefail
|
|
20
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
21
|
+
SCAN="$REPO_ROOT/scripts/degrade_direction_scan.sh"
|
|
22
|
+
[ -r "$SCAN" ] || { echo "degrade-probe: 스캐너 부재 — 계기가 없다"; exit 10; }
|
|
23
|
+
|
|
24
|
+
OWNED_TMP="$(mktemp -d)" # ← 내가 만든 것. 재대입 금지.
|
|
25
|
+
_cleanup() {
|
|
26
|
+
# 두 번째 방벽: 트랩이 지우는 경로가 정말 내가 만든 임시 디렉토리인지 검문한다.
|
|
27
|
+
case "$OWNED_TMP" in
|
|
28
|
+
/tmp/*|/var/folders/*) [ -d "$OWNED_TMP" ] && rm -rf "$OWNED_TMP" ;;
|
|
29
|
+
*) echo "degrade-probe: cleanup 거부 — OWNED_TMP 가 임시 경로가 아니다: $OWNED_TMP" >&2 ;;
|
|
30
|
+
esac
|
|
31
|
+
}
|
|
32
|
+
trap _cleanup EXIT
|
|
33
|
+
|
|
34
|
+
MODE="${1:-}"
|
|
35
|
+
TARGET="" # ← 스캔 대상. 실제 레포 경로를 가리킬 수 있다. 절대 안 지운다.
|
|
36
|
+
|
|
37
|
+
case "$MODE" in
|
|
38
|
+
--known-positive) # 답을 아는 양성: 교과서적 fail-open (검사 실패인데 성공 반환)
|
|
39
|
+
TARGET="$OWNED_TMP"
|
|
40
|
+
cat > "$TARGET/probe_target.sh" <<'FIX'
|
|
41
|
+
#!/bin/sh
|
|
42
|
+
verify_integrity() {
|
|
43
|
+
scan=$(run_scan) || return 0 # 검사가 에러났는데 성공으로 반환 = fail-open
|
|
44
|
+
echo "$scan"
|
|
45
|
+
}
|
|
46
|
+
FIX
|
|
47
|
+
;;
|
|
48
|
+
--known-negative) # 답을 아는 음성: 같은 모양인데 방향이 옳은 대조군
|
|
49
|
+
TARGET="$OWNED_TMP"
|
|
50
|
+
cat > "$TARGET/probe_target.sh" <<'FIX'
|
|
51
|
+
#!/bin/sh
|
|
52
|
+
verify_integrity() {
|
|
53
|
+
scan=$(run_scan) || return 1 # 검사 실패 → 실패 반환 = fail-closed
|
|
54
|
+
echo "$scan"
|
|
55
|
+
}
|
|
56
|
+
FIX
|
|
57
|
+
;;
|
|
58
|
+
--target) TARGET="${2:?--target 은 경로가 필요하다}" ;;
|
|
59
|
+
'') TARGET="$REPO_ROOT/scripts" ;;
|
|
60
|
+
*) echo "degrade-probe: 알 수 없는 모드 '$MODE'"; exit 10 ;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
# 대상 존재 여부를 **먼저** 판정한다 — 원 스캐너의 exit 0 은 「대상 없음」과
|
|
64
|
+
# 「깨끗」을 같은 값으로 렌더한다. 그 둘을 여기서 가른다.
|
|
65
|
+
# `.md` joined the候補 set 2026-08-12. Until then this counted only `.sh|.py`, so a pure-markdown
|
|
66
|
+
# target exited 3 = NO_TARGET **without ever invoking the scanner** — which means the release's
|
|
67
|
+
# headline feature (scanning ```bash/```python fences inside SKILL.md) was unreachable through the
|
|
68
|
+
# typed capability path, the one a cockpit caller actually uses. A capability that cannot reach its
|
|
69
|
+
# own advertised surface is not a narrow bug; it is the feature not shipping on that path.
|
|
70
|
+
# 이 pre-count 자체가 `find` 에 의존한다 — 부재하면 0 이 나오고 그건 NO_TARGET 으로 렌더된다.
|
|
71
|
+
# 스캐너 본체는 같은 상황에서 INSTRUMENT ERROR(exit 2)를 내는데 wrapper 가 그걸 «대상 없음»으로
|
|
72
|
+
# 덮어써서 HARNESS_ERROR 판정이 도달 불가였다(cross-family, A급). 계기 부재는 대상 부재가 아니다.
|
|
73
|
+
command -v find >/dev/null 2>&1 || {
|
|
74
|
+
echo "degrade-probe: HARNESS_ERROR — \`find\` 부재로 대상 열거 자체가 불가(대상 없음이 아니다)"; exit 10; }
|
|
75
|
+
# 계기가 **있어도** 이 타깃을 못 훑을 수 있다(권한 없는 디렉토리). 첫 수리는 `command -v` 만 봐서
|
|
76
|
+
# 그 경우가 여전히 NO_TARGET 으로 렌더됐다 — 절반만 닫은 수리다(cross-family 재지적, A급).
|
|
77
|
+
# 「대상이 없다」와 「대상을 못 셌다」는 다른 말이고, 후자는 HARNESS_ERROR 다.
|
|
78
|
+
_perr=$(find -H "$TARGET" -type f 2>&1 >/dev/null); _prc=$?
|
|
79
|
+
if [ "$_prc" -ne 0 ] || [ -n "$_perr" ]; then
|
|
80
|
+
echo "degrade-probe: HARNESS_ERROR — 대상 열거 실패(대상 없음이 아니다): ${_perr:-find exited $_prc}"; exit 10
|
|
81
|
+
fi
|
|
82
|
+
n=$(find -H "$TARGET" -type f \( -name '*.sh' -o -name '*.py' -o -name '*.md' \) 2>/dev/null | wc -l | tr -d ' ')
|
|
83
|
+
[ "${n:-0}" -eq 0 ] && { echo "degrade-probe: NO_TARGET — 스캔 대상 파일 0개(깨끗이 아니다)"; exit 3; }
|
|
84
|
+
|
|
85
|
+
out="$(bash "$SCAN" "$TARGET" 2>&1)"; rc=$? # verdict 는 직접 취한다(PIPE-VERDICT)
|
|
86
|
+
case "$rc" in
|
|
87
|
+
0) echo "degrade-probe: CLEAN (대상 $n)"; exit 0 ;;
|
|
88
|
+
2)
|
|
89
|
+
# rc=2 는 스캐너에서 **세 가지**를 뜻한다: 실제 findings · 못 잰 파일(UNMEASURED) ·
|
|
90
|
+
# 스캔 대상 아님(UNSCANNABLE). 셋을 전부 FINDINGS 로 접으면 「못 쟀다」가 「결함을 찾았다」로
|
|
91
|
+
# 렌더돼, 계기 고장이 대상의 결함으로 오귀속된다(반대 방향이지만 같은 계열의 오분류).
|
|
92
|
+
# 순서가 판정이다 — «못 쟀다» 를 **먼저** 본다. findings 검사를 앞에 두면 「일부는 쟀고 일부는
|
|
93
|
+
# 못 쟀다」가 그냥 FINDINGS 로 나가고, 미측정 표면이 판정에서 사라진다(cross-family 지적, A급).
|
|
94
|
+
# `_md_collect` 의 「err 가 got 을 이긴다」와 같은 규율을 이 층에도 건다.
|
|
95
|
+
if printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
|
|
96
|
+
echo "degrade-probe: HARNESS_ERROR — 스캐너가 대상을 측정하지 못했다(깨끗이 아니다)"
|
|
97
|
+
printf '%s\n' "$out" | head -4; exit 10
|
|
98
|
+
elif printf '%s' "$out" | grep -qE '\[[A-Z][0-9]?:'; then
|
|
99
|
+
echo "degrade-probe: FINDINGS (대상 $n)"; printf '%s\n' "$out" | head -6; exit 2
|
|
100
|
+
else
|
|
101
|
+
echo "degrade-probe: NO_TARGET — 대상 $n 개가 전부 스캔 대상 밖이다(깨끗이 아니다)"
|
|
102
|
+
printf '%s\n' "$out" | head -4; exit 3
|
|
103
|
+
fi ;;
|
|
104
|
+
*) echo "degrade-probe: HARNESS_ERROR — 스캐너가 enum 밖 $rc 반환"; exit 10 ;;
|
|
105
|
+
esac
|
|
@@ -104,6 +104,14 @@ ACCEPTED_ABSENT=(
|
|
|
104
104
|
# machine is a bill is worse than omitting it. selfcheck reports it NOT EXERCISED (exit 2)
|
|
105
105
|
# where the CLI is missing, so the package stays green without pretending the lanes ran.
|
|
106
106
|
"scripts/test_sessionstart_multihook_lanes.sh"
|
|
107
|
+
# The launchd-driven daily cadence runner. Two independent reasons it must not ship: it is half of
|
|
108
|
+
# a pair whose other half is a machine-local plist (`scripts/com.forge-harness.frontier-digest.plist`),
|
|
109
|
+
# and every run spends `claude` CLI calls on the consumer's account — the same "its only effect on
|
|
110
|
+
# a consumer's machine is a bill" rule as `ablation_calibrate.sh` above. It also writes into
|
|
111
|
+
# `tracks/`, which does not ship. The frontier-digest SKILL names it as the *hub's* production
|
|
112
|
+
# runner (labelled hub-local at the reference), which is a pointer for contributors, not a promise
|
|
113
|
+
# of a shipped artifact — the skill's own save path works without it.
|
|
114
|
+
"scripts/frontier_digest_daily.sh"
|
|
107
115
|
)
|
|
108
116
|
|
|
109
117
|
out=$(python3 - "${ACCEPTED_ABSENT[@]}" <<'PY'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# psa_probe_capability.sh — public-surface-audit 기계층의 **capability 진입점**.
|
|
3
|
+
#
|
|
4
|
+
# 왜 별도 파일인가: `public_surface_scan_files.sh` 는 publish 경계 전용(exit 1 = 차단)이고
|
|
5
|
+
# npm **배포 파일셋**(package.json files[])만 스캔한다. capability 로 등록하려면 §ⓑ.2 가
|
|
6
|
+
# 요구하는 것 — 닫힌 enum · 「안 돌았다」 구분 · 캘리브레이션 쌍 argv — 을 갖춘 진입점이
|
|
7
|
+
# 필요하고, 스코프도 다르다: 여기는 **전 tracked 파일**을 본다.
|
|
8
|
+
# ⚠️ 두 표면은 서로를 포함하지 않는다 — npm PASS 는 공개 표면 PASS 가 아니다
|
|
9
|
+
# (레포가 public 이면 files[] 밖 tracked 파일도 GitHub 이 이미 게시하고 있다).
|
|
10
|
+
# 스캔 로직은 재구현하지 않고 `psa_scan_lib.sh` 를 그대로 쓴다(단일 소스).
|
|
11
|
+
#
|
|
12
|
+
# verdict enum (declared): 0=CLEAN 1=LEAK 3=NOT_CONFIGURED 10=HARNESS_ERROR
|
|
13
|
+
# 3 은 「돌았는데 깨끗」이 아니라 「스캔할 패턴이 없어 아무것도 안 봤다」다 — §ⓑ.4 B1.
|
|
14
|
+
#
|
|
15
|
+
# 🟥 정리 트랩 규율(자매 파일 degrade_probe 의 실사고에서): 트랩이 지우는 변수는
|
|
16
|
+
# **재대입하지 않는다**. OWNED_TMP 는 내가 만든 것만 가리키고, 스캔 대상은 별 변수다.
|
|
17
|
+
set -o pipefail
|
|
18
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
19
|
+
LIB="$REPO_ROOT/scripts/psa_scan_lib.sh"
|
|
20
|
+
[ -r "$LIB" ] || { echo "psa-probe: scan lib 부재 — 계기가 없다"; exit 10; }
|
|
21
|
+
. "$LIB" || { echo "psa-probe: scan lib 로드 실패"; exit 10; }
|
|
22
|
+
|
|
23
|
+
OWNED_TMP="$(mktemp -d)" # 재대입 금지
|
|
24
|
+
_cleanup() {
|
|
25
|
+
case "$OWNED_TMP" in
|
|
26
|
+
/tmp/*|/var/folders/*) [ -d "$OWNED_TMP" ] && rm -rf "$OWNED_TMP" ;;
|
|
27
|
+
*) echo "psa-probe: cleanup 거부 — OWNED_TMP 가 임시 경로가 아니다: $OWNED_TMP" >&2 ;;
|
|
28
|
+
esac
|
|
29
|
+
}
|
|
30
|
+
trap _cleanup EXIT
|
|
31
|
+
|
|
32
|
+
MODE="${1:-}"
|
|
33
|
+
IN="$OWNED_TMP/in"
|
|
34
|
+
SCAN_NOTE=""
|
|
35
|
+
|
|
36
|
+
# 합성 양성 토큰은 **조각으로 보관하고 실행 시 조립**한다. 통짜 리터럴을 두면 이 파일 자체가
|
|
37
|
+
# MED 히트가 되어 pre-commit 기밀 스캔에 걸린다 — 실제로 걸렸고(2026-08-11), 그 차단은 옳았다.
|
|
38
|
+
# 우회(override)가 아니라 스킬 자신의 2층 규율(리터럴은 추적 파일에 두지 않는다)을 따르는 게 답이다.
|
|
39
|
+
_synth_home_token() { printf '/Users/%s/' "realuser1234"; }
|
|
40
|
+
|
|
41
|
+
case "$MODE" in
|
|
42
|
+
--known-positive) # 답을 아는 양성: 조립된 홈경로 토큰을 심은 합성 입력
|
|
43
|
+
printf 'synthetic_fixture.md\tsee %ssecret for detail\n' "$(_synth_home_token)" > "$IN" ;;
|
|
44
|
+
--known-negative) # 답을 아는 음성: 어떤 패턴에도 안 걸리는 합성 입력
|
|
45
|
+
printf 'synthetic_fixture.md\tperfectly ordinary documentation line\n' > "$IN" ;;
|
|
46
|
+
--tracked|'') # 실사용: 이 레포의 tracked 파일 전량
|
|
47
|
+
# `-z` (NUL 구분) 필수: 기본 `core.quotePath=true` 는 비-ASCII 파일명을 따옴표+8진
|
|
48
|
+
# 이스케이프로 내보내므로, 줄 단위로 읽으면 그 파일들이 **무음으로 스캔에서 빠지고**
|
|
49
|
+
# 결과는 CLEAN 이 된다(못 잰 것을 0으로 렌더). 드롭은 세어서 보고한다.
|
|
50
|
+
git -C "$REPO_ROOT" ls-files -z > "$OWNED_TMP/files" 2>/dev/null || { echo "psa-probe: git 도달 불가"; exit 10; }
|
|
51
|
+
: > "$IN"; scanned=0; dropped=0
|
|
52
|
+
while IFS= read -r -d '' f; do
|
|
53
|
+
if [ ! -f "$REPO_ROOT/$f" ]; then dropped=$((dropped+1)); continue; fi
|
|
54
|
+
if awk -v p="$f" '{printf "%s\t%s\n", p, $0}' "$REPO_ROOT/$f" >> "$IN" 2>/dev/null; then
|
|
55
|
+
scanned=$((scanned+1))
|
|
56
|
+
else dropped=$((dropped+1)); fi
|
|
57
|
+
done < "$OWNED_TMP/files"
|
|
58
|
+
SCAN_NOTE=" (스캔 $scanned 파일$([ "$dropped" -gt 0 ] && printf ', 드롭 %s' "$dropped"))"
|
|
59
|
+
[ "$dropped" -gt 0 ] && { echo "psa-probe: HARNESS_ERROR — 읽지 못한 tracked 파일 $dropped 개(부분 스캔은 CLEAN 을 증명하지 못한다)"; exit 10; } ;;
|
|
60
|
+
*) echo "psa-probe: 알 수 없는 모드 '$MODE'"; exit 10 ;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
psa_load "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" \
|
|
64
|
+
"${PSA_PATTERNS:-$REPO_ROOT/.claude/rules/.public-surface-patterns}" \
|
|
65
|
+
|| { echo "psa-probe: 패턴 로드 실패"; exit 10; }
|
|
66
|
+
|
|
67
|
+
# 「안 돌았다」와 「돌았는데 깨끗」의 구분 — 패턴 층이 하나도 없으면 스캔한 게 없다
|
|
68
|
+
if [ "${PSA_DEFAULTS_OK:-0}" -eq 0 ] && [ "${PSA_OVERRIDE_PRESENT:-0}" -eq 0 ]; then
|
|
69
|
+
echo "psa-probe: NOT_CONFIGURED — 패턴 층 부재, 아무것도 스캔하지 않았다"; exit 3
|
|
70
|
+
fi
|
|
71
|
+
[ "${PSA_BAD_ROWS:-0}" -gt 0 ] && { echo "psa-probe: HARNESS_ERROR — 못 쓰는 패턴 행 $PSA_BAD_ROWS 개"; exit 10; }
|
|
72
|
+
|
|
73
|
+
psa_scan_tagged < "$IN"; hit_rc=$? # verdict 는 직접 취한다(PIPE-VERDICT)
|
|
74
|
+
case "$hit_rc" in
|
|
75
|
+
0) echo "psa-probe: CLEAN${SCAN_NOTE}"; exit 0 ;;
|
|
76
|
+
1) echo "psa-probe: LEAK${SCAN_NOTE}"; exit 1 ;;
|
|
77
|
+
*) echo "psa-probe: HARNESS_ERROR — scan_tagged 가 enum 밖 $hit_rc 반환"; exit 10 ;;
|
|
78
|
+
esac
|
|
@@ -125,6 +125,14 @@ while IFS= read -r f; do
|
|
|
125
125
|
[ -z "$tok" ] && continue
|
|
126
126
|
printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
|
|
127
127
|
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$f"; then continue; fi
|
|
128
|
+
# file::token allowlist — the SAME rows the commit/push path honours (psa_scan_lib.sh).
|
|
129
|
+
# Before this line the publish path ignored them, so a token the operator had DELIBERATELY
|
|
130
|
+
# published had no expressible disposition **at the one boundary that matters most**: the only
|
|
131
|
+
# way past was the blanket `PUBLIC_SURFACE_OK=1` override. That is worse than a narrow row —
|
|
132
|
+
# a blanket override waves every OTHER finding through in the same run, and this repo's own
|
|
133
|
+
# doctrine says an override that becomes routine disarms the gate. Narrow, recorded, and
|
|
134
|
+
# logged beats broad and silent. (Rows are literal path + literal token, gitignored source.)
|
|
135
|
+
if psa_pair_allowlisted "$f" "$tok"; then continue; fi
|
|
128
136
|
echo " ❌ $sev leak — $f: '$tok' would ship to the registry"
|
|
129
137
|
LEAK=1
|
|
130
138
|
# -a forces every published file to scan as TEXT (challenger S1): -I skipped binary-classified
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -184,6 +184,21 @@ else
|
|
|
184
184
|
fail=1
|
|
185
185
|
fi
|
|
186
186
|
|
|
187
|
+
# count_check's README format override is a mandatory-pass gate whose pattern is caller-supplied.
|
|
188
|
+
# Same subject-present/anchor-gone shape as the block above: if the guard ships without its known
|
|
189
|
+
# pair, a template that defeats the gate (a rendered newline turns the pattern into an OR search)
|
|
190
|
+
# passes silently — measured 2026-08-12 on a stale README that the gate reported as PASS.
|
|
191
|
+
if [ ! -f scripts/count_check.sh ]; then
|
|
192
|
+
echo "SKIP count_check README-format lanes (subject scripts/count_check.sh absent)"
|
|
193
|
+
elif [ -f scripts/test_count_check_readme_format_lanes.sh ]; then
|
|
194
|
+
if ! bash scripts/test_count_check_readme_format_lanes.sh; then
|
|
195
|
+
fail=1
|
|
196
|
+
fi
|
|
197
|
+
else
|
|
198
|
+
echo "FAIL count_check README-format lanes: count_check.sh present but its anchor is missing"
|
|
199
|
+
fail=1
|
|
200
|
+
fi
|
|
201
|
+
|
|
187
202
|
# package-coverage — a shipped doc must not point at a file the tarball omits. Distinct from the
|
|
188
203
|
# ref-path check below: that one asks "does this path exist at all", this one asks "does the
|
|
189
204
|
# CONSUMER get it". Measured 2026-07-28: 35 paths existed, were named by a shipped doc, and were
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_capability_entrypoint_shipping.sh — every typed CAPABILITY ENTRY POINT must be in the
|
|
3
|
+
# npm published file set.
|
|
4
|
+
#
|
|
5
|
+
# WHY THIS EXISTS (measured 2026-08-12, cross-family gpt-5.5 + governor widening)
|
|
6
|
+
# `scripts/degrade_probe_capability.sh` AND `scripts/psa_probe_capability.sh` were both absent from
|
|
7
|
+
# `package.json` `files[]`, while their VALIDATOR (`capability_registry_check.sh`) shipped. An npm
|
|
8
|
+
# consumer therefore received the thing that checks capabilities and none of the capabilities — the
|
|
9
|
+
# release's headline feature (scanning ```bash/```python fences) was unreachable on the typed path
|
|
10
|
+
# it advertises.
|
|
11
|
+
#
|
|
12
|
+
# WHY `package_coverage_check.sh` DID NOT CATCH IT — and why this is a separate check rather than a
|
|
13
|
+
# rule added there: that checker walks *references* (a shipped doc names a path → the path must
|
|
14
|
+
# ship). These two files are referenced by NOTHING but their own header. A reference-follower is
|
|
15
|
+
# structurally blind to an ORPHAN; you cannot fix that by adding another pattern to it. The
|
|
16
|
+
# discriminator here is not "is it referenced" but "is it an entry point", which is knowable from
|
|
17
|
+
# the filename convention alone.
|
|
18
|
+
#
|
|
19
|
+
# CLASS: third occurrence of "a shipped surface points outside files[]" in this release cycle
|
|
20
|
+
# 1. SKILL.md pointed at a runner that was not shipped (caught by CI only)
|
|
21
|
+
# 2. AGENTS.md pointed at validate_yaml.sh, not shipped (caught by CI only)
|
|
22
|
+
# 3. the capability entry points themselves, not shipped (caught by cross-family only)
|
|
23
|
+
# The standing rule is N>=3 → mechanize at the front instead of relying on the next reviewer.
|
|
24
|
+
#
|
|
25
|
+
# Usage: bash scripts/test_capability_entrypoint_shipping.sh
|
|
26
|
+
# Exit: 0 = every entry point ships; 1 = at least one is missing (or the check could not run).
|
|
27
|
+
set -uo pipefail
|
|
28
|
+
|
|
29
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
30
|
+
cd "$REPO_ROOT" || { echo "FAIL: cannot cd to repo root"; exit 1; }
|
|
31
|
+
|
|
32
|
+
pass=0; fail=0
|
|
33
|
+
ok() { printf ' \342\234\205 %s\n' "$1"; pass=$((pass+1)); }
|
|
34
|
+
bad() { printf ' \342\235\214 %s\n' "$1"; fail=$((fail+1)); }
|
|
35
|
+
|
|
36
|
+
echo "capability entry-point shipping check"
|
|
37
|
+
|
|
38
|
+
command -v node >/dev/null 2>&1 || {
|
|
39
|
+
echo " INSTRUMENT ERROR: node unavailable — files[] cannot be read. NOT a pass."; exit 1; }
|
|
40
|
+
|
|
41
|
+
# Discover entry points by convention. A DISCOVERY of zero is an instrument failure, not a clean
|
|
42
|
+
# result: this check exists precisely because the surface it guards is invisible to reference
|
|
43
|
+
# walking, so "found nothing to check" must never render as "everything ships".
|
|
44
|
+
ENTRIES=$(find scripts -maxdepth 1 -type f -name '*_capability.sh' 2>/dev/null | sort)
|
|
45
|
+
n=$(printf '%s\n' "$ENTRIES" | grep -c . || true)
|
|
46
|
+
n=$(( ${n:-0} + 0 ))
|
|
47
|
+
if [ "$n" -eq 0 ]; then
|
|
48
|
+
echo " INSTRUMENT ERROR: zero *_capability.sh files discovered — the scan did not reach its target."
|
|
49
|
+
echo " (A real zero is possible only if this repo has no typed capabilities; verify by hand before believing it.)"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
FILES_JSON=$(node -e 'process.stdout.write(JSON.stringify(require("./package.json").files||[]))' 2>/dev/null) || {
|
|
54
|
+
echo " INSTRUMENT ERROR: could not read package.json files[]"; exit 1; }
|
|
55
|
+
|
|
56
|
+
printf '%s\n' "$ENTRIES" | while IFS= read -r e; do
|
|
57
|
+
[ -n "$e" ] || continue
|
|
58
|
+
if node -e 'const f=JSON.parse(process.argv[1]);process.exit(f.includes(process.argv[2])?0:1)' "$FILES_JSON" "$e"; then
|
|
59
|
+
echo " ok $e"
|
|
60
|
+
else
|
|
61
|
+
echo " MISSING $e"
|
|
62
|
+
fi
|
|
63
|
+
done > "${TMPDIR:-/tmp}/cap_entry_$$.txt"
|
|
64
|
+
|
|
65
|
+
missing=$(grep -c '^ MISSING ' "${TMPDIR:-/tmp}/cap_entry_$$.txt" || true)
|
|
66
|
+
missing=$(( ${missing:-0} + 0 ))
|
|
67
|
+
shipped=$(grep -c '^ ok ' "${TMPDIR:-/tmp}/cap_entry_$$.txt" || true)
|
|
68
|
+
shipped=$(( ${shipped:-0} + 0 ))
|
|
69
|
+
cat "${TMPDIR:-/tmp}/cap_entry_$$.txt"
|
|
70
|
+
rm -f "${TMPDIR:-/tmp}/cap_entry_$$.txt"
|
|
71
|
+
|
|
72
|
+
if [ "$missing" -eq 0 ]; then
|
|
73
|
+
ok "all $shipped capability entry point(s) are in package.json files[]"
|
|
74
|
+
else
|
|
75
|
+
bad "$missing capability entry point(s) absent from files[] — npm consumers get the validator without the capability"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# REVERSE DIRECTION (added 2026-08-12 after a cross-family round broke the first version).
|
|
79
|
+
# The check above walks disk → files[]. That direction alone is blind to the failure that actually
|
|
80
|
+
# loses a capability: MOVE OR DELETE the entry-point file. Discovery then simply does not see it,
|
|
81
|
+
# every remaining entry is still listed, and the lane goes green while `npm pack` ships one fewer
|
|
82
|
+
# capability. So also walk files[] → disk: anything declared as a capability entry point must exist.
|
|
83
|
+
# (A capability removed from BOTH sides is an intentional deletion and correctly flags nothing.)
|
|
84
|
+
DECLARED=$(node -e '
|
|
85
|
+
const f=JSON.parse(process.argv[1]);
|
|
86
|
+
process.stdout.write(f.filter(p=>/^scripts\/.*_capability\.sh$/.test(p)).join("\n"));
|
|
87
|
+
' "$FILES_JSON")
|
|
88
|
+
dangling=0
|
|
89
|
+
if [ -n "$DECLARED" ]; then
|
|
90
|
+
while IFS= read -r d; do
|
|
91
|
+
[ -n "$d" ] || continue
|
|
92
|
+
# `-f` alone is not enough, and both gaps were demonstrated (round 5): a SYMLINK satisfies `-f`
|
|
93
|
+
# but `npm pack` does not follow it — the tarball simply omits the file — and a ZERO-BYTE file
|
|
94
|
+
# satisfies every existence test while shipping an empty capability. Existence is the weakest
|
|
95
|
+
# of the three properties; assert all of them.
|
|
96
|
+
if [ ! -e "$d" ]; then
|
|
97
|
+
echo " DANGLING $d (declared in files[], absent on disk)"; dangling=$((dangling+1))
|
|
98
|
+
elif [ -L "$d" ]; then
|
|
99
|
+
echo " SYMLINK $d (npm pack does not follow symlinks — the tarball would omit it)"; dangling=$((dangling+1))
|
|
100
|
+
elif [ ! -f "$d" ]; then
|
|
101
|
+
# A DIRECTORY passes -e, is not a symlink, and `-s` reports non-zero size for it — so the
|
|
102
|
+
# three tests above all agreed a directory was a fine entry point (round 6). Regular-file-ness
|
|
103
|
+
# is the property actually being claimed; assert it rather than three proxies for it.
|
|
104
|
+
echo " NOT-A-FILE $d (declared as an entry point but is not a regular file)"; dangling=$((dangling+1))
|
|
105
|
+
elif [ ! -s "$d" ]; then
|
|
106
|
+
echo " EMPTY $d (zero bytes — ships an entry point that cannot run)"; dangling=$((dangling+1))
|
|
107
|
+
fi
|
|
108
|
+
done <<EOF
|
|
109
|
+
$DECLARED
|
|
110
|
+
EOF
|
|
111
|
+
fi
|
|
112
|
+
declared_n=$(printf '%s\n' "$DECLARED" | grep -c . || true); declared_n=$(( ${declared_n:-0} + 0 ))
|
|
113
|
+
if [ "$declared_n" -eq 0 ]; then
|
|
114
|
+
bad "files[] declares ZERO capability entry points — either the convention changed or the list was gutted; a disk-only check cannot notice that"
|
|
115
|
+
elif [ "$dangling" -eq 0 ]; then
|
|
116
|
+
ok "all $declared_n declared capability entry point(s) exist on disk (move/delete would be caught)"
|
|
117
|
+
else
|
|
118
|
+
bad "$dangling declared capability entry point(s) missing from disk — npm would ship a broken files[] and the disk-side check alone stays green"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# CONTROL — the check must be able to say NO. A checker that only ever prints ok is indistinguishable
|
|
122
|
+
# from a checker that is not looking; assert the negative arm on a name that cannot be in files[].
|
|
123
|
+
if node -e 'const f=JSON.parse(process.argv[1]);process.exit(f.includes(process.argv[2])?0:1)' \
|
|
124
|
+
"$FILES_JSON" "scripts/definitely_not_shipped_$$.sh"; then
|
|
125
|
+
bad "CONTROL DEAD — a nonexistent path reported as shipped; the membership test is not testing"
|
|
126
|
+
else
|
|
127
|
+
ok "control alive — a nonexistent path is correctly reported as not shipped"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
echo "----"
|
|
131
|
+
echo "capability entry-point shipping: $pass passed, $fail failed (entries=$n, missing=$missing)"
|
|
132
|
+
[ "$fail" -eq 0 ] || exit 1
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_count_check_readme_format_lanes.sh — known-pair anchor for the README format override guard
|
|
3
|
+
# in `scripts/count_check.sh`.
|
|
4
|
+
#
|
|
5
|
+
# WHY THIS EXISTS
|
|
6
|
+
# `count_check.sh` is a **mandatory-pass** gate wired into selfcheck → prepublishOnly, pre-commit
|
|
7
|
+
# and CI. Its README check renders a caller-supplied printf template and uses the RESULT as a
|
|
8
|
+
# `grep -qE` pattern. Two ways that goes wrong, both measured 2026-08-12:
|
|
9
|
+
# · a template rendering a NEWLINE turns the pattern into an OR search — a stale README (99/99)
|
|
10
|
+
# PASSED because the trailing fragment "8" matched "Node 18" elsewhere in the file;
|
|
11
|
+
# · `printf` failure and value-swallowing directives (`%.0s`) produced partial patterns, unchecked.
|
|
12
|
+
# The guard that closes this shipped with **zero** anchors in the same commit that added four
|
|
13
|
+
# anchors for a sibling fix — the exact "you can delete the feature and stay green" class that
|
|
14
|
+
# commit was closing. This file is that missing half.
|
|
15
|
+
#
|
|
16
|
+
# SCOPE, stated honestly: these lanes exercise the guard's DECISION LOGIC in isolation (render →
|
|
17
|
+
# inspect → accept/reject), not the whole count_check run. The full script needs a git worktree
|
|
18
|
+
# with a populated plugin tree; a fixture that lacks one fails for an unrelated reason ("0 active
|
|
19
|
+
# fh-meta skills") and every arm goes red for the SAME wrong cause — which is how a first attempt
|
|
20
|
+
# at this test nearly certified a guard that had not been exercised at all.
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
24
|
+
SUBJECT="$REPO_ROOT/scripts/count_check.sh"
|
|
25
|
+
pass=0; fail=0
|
|
26
|
+
ok() { printf ' \342\234\205 %s\n' "$1"; pass=$((pass+1)); }
|
|
27
|
+
bad() { printf ' \342\235\214 %s\n' "$1"; fail=$((fail+1)); }
|
|
28
|
+
|
|
29
|
+
# The guard block is extracted from the subject rather than restated here: a copy drifts from the
|
|
30
|
+
# thing it claims to verify, which is the divergent-normalizer defect this repo has hit before.
|
|
31
|
+
GUARD=$(awk '/^ README_STR=\$\(printf/,/^ \[ -n "\$README_STR" \] && count_check/' "$SUBJECT")
|
|
32
|
+
if [ -z "$GUARD" ]; then
|
|
33
|
+
bad "guard block not found in count_check.sh — the anchor cannot verify what it cannot locate"
|
|
34
|
+
echo "----"; echo "count_check README-format lanes: $pass passed, $fail failed"; exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
probe() { # $1 = template ; echoes rc + a reason token
|
|
38
|
+
local fmt="$1"
|
|
39
|
+
bash -c '
|
|
40
|
+
set -uo pipefail
|
|
41
|
+
total_sk=40; total_ag=8; fail=0
|
|
42
|
+
README_FMT="$1"
|
|
43
|
+
count_check() { echo "ACCEPTED:$3"; }
|
|
44
|
+
'"$GUARD"'
|
|
45
|
+
exit $fail
|
|
46
|
+
' _ "$fmt" 2>&1
|
|
47
|
+
return $?
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
run() { # $1 = label ; $2 = template ; $3 = expect (accept|reject)
|
|
51
|
+
local out rc; out=$(probe "$2"); rc=$?
|
|
52
|
+
case "$3" in
|
|
53
|
+
accept) if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q 'ACCEPTED:'; then
|
|
54
|
+
ok "$1 — accepted (no over-block)"
|
|
55
|
+
else bad "$1 — legitimate template was REJECTED (rc=$rc): $out"; fi ;;
|
|
56
|
+
reject) if [ "$rc" -ne 0 ]; then ok "$1 — rejected (rc=$rc)"
|
|
57
|
+
else bad "$1 — a broken template PASSED the gate: $out"; fi ;;
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
echo "== count_check README format override — known pair =="
|
|
62
|
+
# Accept arms. Two of them on purpose: the default and a localized one. The whole point of the
|
|
63
|
+
# override is downstream harnesses writing their own wording, so an English-only accept arm would
|
|
64
|
+
# not notice a guard that rejects every non-ASCII template.
|
|
65
|
+
run "A1 default English" '%s skills · %s agents' accept
|
|
66
|
+
run "A2 localized (Korean)" '스킬 %s개 · 에이전트 %s개' accept
|
|
67
|
+
run "A3 markdown decoration" '**%s** skills / **%s** agents' accept
|
|
68
|
+
# Reject arms — each must fail for its OWN reason, not a shared incidental one.
|
|
69
|
+
run "R1 renders a newline" '%s
|
|
70
|
+
%s' reject
|
|
71
|
+
run "R2 swallows a value" '%.0s%s agents' reject
|
|
72
|
+
|
|
73
|
+
echo "----"
|
|
74
|
+
echo "count_check README-format lanes: $pass passed, $fail failed"
|
|
75
|
+
[ "$fail" -eq 0 ] || exit 1
|