@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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +18 -0
  3. package/CHEATSHEET.md +1 -1
  4. package/knowledge/shared/harness-core/fh_detail_protocols.md +12 -0
  5. package/knowledge/shared/harness-core/ship_readiness_gate.md +7 -4
  6. package/knowledge/shared/learnings/subagent_invocations_log.yaml +43 -1
  7. package/package.json +6 -1
  8. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  9. package/plugins/fh-commons/agents/quench-challenger.md +49 -23
  10. package/plugins/fh-commons/skills/convergence-loop/SKILL.md +14 -0
  11. package/plugins/fh-commons/skills/deliberation/SKILL.md +14 -0
  12. package/plugins/fh-commons/skills/mcp-circuit-breaker/SKILL.md +10 -1
  13. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  14. package/plugins/fh-meta/CHANGELOG.md +36 -0
  15. package/plugins/fh-meta/agents/beginner.md +4 -1
  16. package/plugins/fh-meta/agents/challenger.md +7 -1
  17. package/plugins/fh-meta/agents/expert.md +1 -1
  18. package/plugins/fh-meta/agents/fact-checker.md +7 -1
  19. package/plugins/fh-meta/agents/hub-persona-auditor.md +2 -1
  20. package/plugins/fh-meta/agents/main-player.md +4 -1
  21. package/plugins/fh-meta/agents/persona-innovator.md +10 -2
  22. package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
  23. package/plugins/fh-meta/skills/apex-review/SKILL.md +5 -0
  24. package/plugins/fh-meta/skills/asset-placement-gate/SKILL.md +38 -8
  25. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +16 -2
  26. package/plugins/fh-meta/skills/context-doctor/SKILL_detail.md +45 -10
  27. package/plugins/fh-meta/skills/corpus-grounding-expander/SKILL.md +14 -5
  28. package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +93 -30
  29. package/plugins/fh-meta/skills/deep-clarify/SKILL.md +28 -9
  30. package/plugins/fh-meta/skills/fh/SKILL.md +4 -0
  31. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +64 -8
  32. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +20 -7
  33. package/plugins/fh-meta/skills/goal-quench/SKILL.md +48 -15
  34. package/plugins/fh-meta/skills/goal-quench/SKILL_detail.md +58 -11
  35. package/plugins/fh-meta/skills/harness-doctor/SKILL_detail.md +109 -33
  36. package/plugins/fh-meta/skills/harvest-loop/SKILL.md +6 -1
  37. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +126 -17
  38. package/plugins/fh-meta/skills/install-doctor/SKILL.md +50 -14
  39. package/plugins/fh-meta/skills/install-wizard/SKILL.md +26 -7
  40. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +68 -21
  41. package/plugins/fh-meta/skills/memory-hygiene/SKILL.md +64 -17
  42. package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +38 -4
  43. package/plugins/fh-meta/skills/persona-roster-expander/SKILL.md +15 -7
  44. package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +39 -11
  45. package/plugins/fh-meta/skills/plugin-recommender/SKILL_detail.md +24 -7
  46. package/plugins/fh-meta/skills/prompt-regression/SKILL.md +54 -11
  47. package/plugins/fh-meta/skills/salience-splitter/SKILL.md +120 -7
  48. package/plugins/fh-meta/skills/salience-splitter/SKILL_detail.md +46 -13
  49. package/plugins/fh-meta/skills/sim-conductor/SKILL_detail.md +28 -3
  50. package/plugins/fh-meta/skills/steel-quench/SKILL.md +3 -1
  51. package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +72 -14
  52. package/scripts/count_check.sh +47 -1
  53. package/scripts/degrade_direction_scan.sh +276 -6
  54. package/scripts/degrade_probe_capability.sh +105 -0
  55. package/scripts/package_coverage_check.sh +8 -0
  56. package/scripts/psa_probe_capability.sh +78 -0
  57. package/scripts/public_surface_scan_files.sh +8 -0
  58. package/scripts/selfcheck.sh +15 -0
  59. package/scripts/test_capability_entrypoint_shipping.sh +132 -0
  60. package/scripts/test_count_check_readme_format_lanes.sh +75 -0
  61. package/scripts/test_degrade_scan_shell_probes.sh +415 -0
  62. package/scripts/validate_yaml.sh +146 -0
  63. 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
@@ -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