@chrono-meta/fh-gate 1.4.94 → 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/CLAUDE.md +1 -1
- package/knowledge/shared/harness-core/capability_composition_contract.md +25 -4
- 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 +67 -1
- package/package.json +7 -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 +40 -14
- package/plugins/fh-commons/skills/token-budget-gate/SKILL.md +31 -12
- 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/field-harvest/SKILL.md +34 -11
- 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/marketplace-gate/SKILL.md +59 -31
- 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/pipeline-conductor/SKILL.md +13 -3
- 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/public-surface-audit/SKILL.md +56 -22
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +50 -11
- package/plugins/fh-meta/skills/return-path-gate/SKILL.md +36 -7
- 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/capability_registry_check.sh +280 -0
- 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/psa_scan_lib.sh +53 -1
- package/scripts/public_surface_scan_files.sh +8 -0
- package/scripts/relay_channel.sh +6 -1
- 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,280 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# capability_registry_check.sh — 정체성 ① 의 «등록 시점» 검사기.
|
|
3
|
+
#
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# 왜 이게 지어졌나 — 스펙이 이름까지 붙여놓고 «없다» 고 적어둔 파일
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# `capability_composition_contract.md §Salience` 는 이렇게 적는다:
|
|
8
|
+
#
|
|
9
|
+
# "Named residual, not built: a scripts/capability_registry_check.sh that validates
|
|
10
|
+
# the schema and runs each declared M4 pair would make registration *measured*
|
|
11
|
+
# rather than reviewed. It does not exist."
|
|
12
|
+
#
|
|
13
|
+
# `relay_channel.sh` 헤더도 같은 말을 한다 — 자기는 **call moment** 만 보고, 선언이
|
|
14
|
+
# **진실인지**는 검사하지 않는다고. 이 파일이 그 나머지 절반이다.
|
|
15
|
+
#
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
# 이 검사기가 증명하지 않는 것 (과잉주장 금지 — 명명된 잔여)
|
|
18
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
# · **등록을 강제하지 않는다.** "지금 capability 를 등록하려 한다"를 관측하는 훅은 없다.
|
|
20
|
+
# 돌리면 판정하지만 돌리도록 강제하지는 못한다(relay_channel.sh 의 같은 잔여와 동형).
|
|
21
|
+
# · **M3(모델 독립성)은 선언 검사다.** reps 로 재지 않는다 — `judge:` 축 선언과, mechanical
|
|
22
|
+
# 선언인데 entry 가 LLM CLI 를 부르는 명백한 모순만 잡는다. M4 를 reps=2 로 돌려 부분 방어.
|
|
23
|
+
# · 🟥 **`writes:` 축은 검증 불가 — 그리고 그게 이 파일에서 실제로 터졌다.**
|
|
24
|
+
# 2026-08-11, 이 검사기를 통과한 capability(`writes: read-only` 선언)의 진입점이
|
|
25
|
+
# 정리 트랩 결함으로 **레포의 `scripts/` 를 rm -rf 했다.** M1(실행 가능)·M2(닫힌 enum)·
|
|
26
|
+
# M3(mechanical)·M4(known-pair 통과)·M5(cwd) 를 **전부 통과한 채로** 그랬다.
|
|
27
|
+
# 등록 바는 «선언이 형식에 맞나 · 답 아는 쌍을 가르나» 를 보지, **«선언이 사실인가»**
|
|
28
|
+
# 를 보지 않는다. read-only 선언의 진위는 여기서 닫히지 않는다.
|
|
29
|
+
# 부분 처방(오늘 적용): 진입점 쪽 트랩 규율(정리 대상 변수 재대입 금지 + 임시경로 검문).
|
|
30
|
+
# 구조 처방(미구축): 샌드박스/읽기전용 마운트에서 M4 를 돌려 쓰기 시도를 관측하는 것.
|
|
31
|
+
# 그 전까지 `writes: read-only` 는 **등록자 주장**이지 이 검사기의 판정이 아니다.
|
|
32
|
+
#
|
|
33
|
+
# 사용법
|
|
34
|
+
# capability_registry_check.sh <capfile> [<capfile> ...]
|
|
35
|
+
# capability_registry_check.sh --self-test
|
|
36
|
+
#
|
|
37
|
+
# exit code
|
|
38
|
+
# 0 REGISTRABLE 전 capfile 이 M1–M5 + 추가조항 통과
|
|
39
|
+
# 1 REJECTED 하나 이상 기준 미달 (등록 불가 — 그 표면은 dispatch 로 남는다)
|
|
40
|
+
# 10 HARNESS_ERROR capfile 도달 불가·파손·검사기 자신의 전제 파손
|
|
41
|
+
#
|
|
42
|
+
set -o pipefail
|
|
43
|
+
set -f # noglob — 선언 파일의 값은 데이터지 파일 패턴이 아니다 (relay_channel.sh 와 동일)
|
|
44
|
+
|
|
45
|
+
RC_OK=0; RC_REJECT=1; RC_HARNESS=10
|
|
46
|
+
|
|
47
|
+
CLOSED_KEYS="id entry requires_cwd verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream approval reversibility residency degrade tier_floor writes judge verdict_binding calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
|
|
48
|
+
|
|
49
|
+
# 「안 돌았다」를 뜻하는 이름들 — 추가조항(§ⓑ.4 B1)이 요구하는 구분항
|
|
50
|
+
DIDNOTRUN_NAMES="DID_NOT_RUN DIDNOTRUN NOT_RUN NO_TARGET SKIPPED UNMEASURED NOT_CONFIGURED HARNESS_ERROR"
|
|
51
|
+
|
|
52
|
+
FAILED=0 # 전 파일 누적 (종료코드용)
|
|
53
|
+
FILE_FAILED=0 # 현재 capfile 한정 — 파일마다 초기화한다. 이게 없으면 앞 파일의 실패가
|
|
54
|
+
# 뒤 파일의 M4 를 SKIPPED 로 만들어, 뒤 파일의 실제 결함이 안 보인다(보고 결함).
|
|
55
|
+
_fail() { printf ' ❌ %s — %s\n' "$1" "$2"; FAILED=1; FILE_FAILED=1; }
|
|
56
|
+
_ok() { printf ' ✅ %s — %s\n' "$1" "$2"; }
|
|
57
|
+
_die() { printf '❌ HARNESS_ERROR: %s\n' "$*" >&2; exit "$RC_HARNESS"; }
|
|
58
|
+
|
|
59
|
+
_parse() {
|
|
60
|
+
CAP_id=""; CAP_entry=""; CAP_requires_cwd=""; CAP_verdict_channel=""
|
|
61
|
+
CAP_verdict_enum=""; CAP_verdict_stdout_key=""; CAP_judge=""; CAP_writes=""
|
|
62
|
+
CAP_cal_pos_args=""; CAP_cal_pos_expect=""; CAP_cal_neg_args=""; CAP_cal_neg_expect=""
|
|
63
|
+
UNKNOWN_KEYS=""
|
|
64
|
+
local line key val
|
|
65
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
66
|
+
case "$line" in ''|'#'*) continue ;; esac
|
|
67
|
+
case "$line" in *:*) ;; *) UNKNOWN_KEYS="$UNKNOWN_KEYS malformed-line"; continue ;; esac
|
|
68
|
+
key="${line%%:*}"; val="${line#*:}"
|
|
69
|
+
# POSIX 문자클래스만 쓴다. BSD sed 에서 `[ \t]` 는 탭이 아니라 «공백·역슬래시·문자 t»
|
|
70
|
+
# 집합이라 `exit` 의 끝 t 가 잘려 `exi` 가 된다 — 이 검사기의 known-positive 레인이
|
|
71
|
+
# 실제로 그걸 잡았다(2026-08-11, 자기 계기가 자기 버그를 적발한 사례).
|
|
72
|
+
key="$(printf '%s' "$key" | tr -d '[:space:]')"
|
|
73
|
+
val="$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
74
|
+
case " $CLOSED_KEYS " in *" $key "*) ;; *) UNKNOWN_KEYS="$UNKNOWN_KEYS $key"; continue ;; esac
|
|
75
|
+
case "$key" in
|
|
76
|
+
id) CAP_id="$val" ;; entry) CAP_entry="$val" ;;
|
|
77
|
+
requires_cwd) CAP_requires_cwd="$val" ;; verdict_channel) CAP_verdict_channel="$val" ;;
|
|
78
|
+
verdict_enum) CAP_verdict_enum="$val" ;; verdict_stdout_key) CAP_verdict_stdout_key="$val" ;;
|
|
79
|
+
judge) CAP_judge="$val" ;; writes) CAP_writes="$val" ;;
|
|
80
|
+
calibration_positive_args) CAP_cal_pos_args="$val" ;;
|
|
81
|
+
calibration_positive_expect) CAP_cal_pos_expect="$val" ;;
|
|
82
|
+
calibration_negative_args) CAP_cal_neg_args="$val" ;;
|
|
83
|
+
calibration_negative_expect) CAP_cal_neg_expect="$val" ;;
|
|
84
|
+
esac
|
|
85
|
+
done < "$1"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_enum_name_of() { # $1=exit code, $2=enum string → 이름 or ""
|
|
89
|
+
local pair
|
|
90
|
+
for pair in $2; do [ "${pair%%=*}" = "$1" ] && { printf '%s' "${pair#*=}"; return 0; }; done
|
|
91
|
+
printf ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# 🟥 capfile 은 **실행 신뢰경계**다 — M4 는 선언된 arm 을 «실제로 실행» 하는 것이 요점이므로,
|
|
95
|
+
# 검사기에 넘긴 capfile 은 그 자체로 "이 명령을 돌려도 된다"는 선언이다. 신뢰하지 않는
|
|
96
|
+
# capfile 을 이 검사기에 넘기지 마라. 배포 전 보안 패스가 실증한 것: `entry: /usr/bin/touch`
|
|
97
|
+
# + cal args 로 **REJECTED 판정이 나는 와중에도 부작용이 이미 발생**했다(판정 전에 arm 이 돈다).
|
|
98
|
+
# 아래 검문은 그 경계를 없애지 못한다 — 우발적 형태만 막는다.
|
|
99
|
+
_validate_arm_args() { # $1=args → 셸 메타문자/상위경로 탈출을 거부
|
|
100
|
+
case "$1" in
|
|
101
|
+
*'|'*|*';'*|*'&'*|*'>'*|*'<'*|*'`'*|*'$('*|*$'\n'*)
|
|
102
|
+
_fail "M4" "캘리브레이션 args 에 셸 메타문자가 있다(entry 와 같은 인젝션 표면): $1"; return 1 ;;
|
|
103
|
+
*'../'*)
|
|
104
|
+
_fail "M4" "캘리브레이션 args 가 상위 경로로 탈출한다: $1"; return 1 ;;
|
|
105
|
+
esac
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_run_arm() { # $1=extra args → ARM_RC / ARM_NAME. 파이프로 읽지 않는다(PIPE-VERDICT).
|
|
110
|
+
( cd "$CAP_requires_cwd" 2>/dev/null || exit 127
|
|
111
|
+
# shellcheck disable=SC2086 # argv 토큰 분리는 의도 (noglob 로 확장은 막혀 있다)
|
|
112
|
+
set -- $CAP_entry $1; "$@" ) > /dev/null 2>&1
|
|
113
|
+
ARM_RC=$?
|
|
114
|
+
ARM_NAME="$(_enum_name_of "$ARM_RC" "$CAP_verdict_enum")"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_check_one() {
|
|
118
|
+
local f="$1"
|
|
119
|
+
[ -r "$f" ] || _die "capfile 도달 불가: $f"
|
|
120
|
+
_parse "$f"
|
|
121
|
+
FILE_FAILED=0
|
|
122
|
+
printf '\n── %s (%s)\n' "${CAP_id:-<id 미선언>}" "$f"
|
|
123
|
+
|
|
124
|
+
[ -n "$UNKNOWN_KEYS" ] && _fail "SCHEMA" "닫힌 키 목록 밖:$UNKNOWN_KEYS (오타 축 무음드롭 방지 — 무시하지 않는다)"
|
|
125
|
+
|
|
126
|
+
# ── M1 실행 가능한 진입점 ──────────────────────────────────────────────────
|
|
127
|
+
local first second
|
|
128
|
+
first="$(printf '%s' "$CAP_entry" | awk '{print $1}')"
|
|
129
|
+
second="$(printf '%s' "$CAP_entry" | awk '{print $2}')"
|
|
130
|
+
case "$CAP_entry" in
|
|
131
|
+
*'|'*|*';'*|*'&&'*|*'>'*|*'`'*|*'$('*)
|
|
132
|
+
_fail "M1" "entry 가 argv 가 아니라 셸 문자열이다(파이프/리다이렉트/치환 포함) — 인젝션 표면" ;;
|
|
133
|
+
'') _fail "M1" "entry 미선언" ;;
|
|
134
|
+
*)
|
|
135
|
+
if [ -x "$first" ] 2>/dev/null; then _ok "M1" "실행 가능: $first"
|
|
136
|
+
elif command -v "$first" >/dev/null 2>&1 && [ -r "$second" ]; then
|
|
137
|
+
_ok "M1" "선언된 인터프리터($first) + 읽을 수 있는 스크립트: $second"
|
|
138
|
+
else _fail "M1" "entry 를 셸이 모델 없이 실행할 수 없다: $CAP_entry"; fi ;;
|
|
139
|
+
esac
|
|
140
|
+
|
|
141
|
+
# ── M2 닫힌 채널의 typed verdict + 추가조항(ran ≠ did-not-run) ─────────────
|
|
142
|
+
case "$CAP_verdict_channel" in
|
|
143
|
+
exit|stdout-key|both) ;;
|
|
144
|
+
*) _fail "M2" "verdict_channel 이 {exit|stdout-key|both} 밖: '${CAP_verdict_channel}'" ;;
|
|
145
|
+
esac
|
|
146
|
+
case "$CAP_verdict_channel" in
|
|
147
|
+
stdout-key|both) [ -n "$CAP_verdict_stdout_key" ] || _fail "M2" "channel 이 stdout-key 를 포함하는데 verdict_stdout_key 미선언" ;;
|
|
148
|
+
esac
|
|
149
|
+
if [ -z "$CAP_verdict_enum" ]; then
|
|
150
|
+
_fail "M2" "verdict_enum 미선언 — 열린 채널은 등록 불가"
|
|
151
|
+
else
|
|
152
|
+
local pair bad=0 has_dnr=0 nm
|
|
153
|
+
for pair in $CAP_verdict_enum; do
|
|
154
|
+
case "$pair" in *=*) ;; *) bad=1 ;; esac
|
|
155
|
+
nm="${pair#*=}"
|
|
156
|
+
case " $DIDNOTRUN_NAMES " in *" $nm "*) has_dnr=1 ;; esac
|
|
157
|
+
done
|
|
158
|
+
[ "$bad" -eq 1 ] && _fail "M2" "verdict_enum 항목이 N=NAME 형식이 아니다: $CAP_verdict_enum"
|
|
159
|
+
if [ "$has_dnr" -eq 1 ]; then _ok "M2" "typed enum + 「안 돌았다」 구분항: $CAP_verdict_enum"
|
|
160
|
+
else _fail "M2+" "enum 에 「안 돌았다」를 뜻하는 값이 없다 ($CAP_verdict_enum) — PASS 가 no-op 과 구분 불가(§ⓑ.4 B1). PASS 는 적극 증거지 실패의 부재가 아니다"; fi
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# ── M3 모델 독립성 (선언 검사 + 명백한 모순만) ─────────────────────────────
|
|
164
|
+
case "$CAP_judge" in
|
|
165
|
+
mechanical)
|
|
166
|
+
case "$CAP_entry" in
|
|
167
|
+
*claude*|*codex*|*gemini*|*copilot*|*ollama*|*llm*)
|
|
168
|
+
_fail "M3" "judge: mechanical 선언인데 entry 가 모델 CLI 를 부른다: $CAP_entry" ;;
|
|
169
|
+
*) _ok "M3" "judge: mechanical (모델 미개입 선언)" ;;
|
|
170
|
+
esac ;;
|
|
171
|
+
model) _ok "M3" "judge: model — 선언됨(합법). 조합에서 이 PASS 는 NON_CLEARING 이다" ;;
|
|
172
|
+
'') _fail "M3" "judge 축 미선언 — 모델 개입 여부가 불명이면 조합이 계산될 수 없다" ;;
|
|
173
|
+
*) _fail "M3" "judge 값이 {mechanical|model} 밖: '$CAP_judge'" ;;
|
|
174
|
+
esac
|
|
175
|
+
|
|
176
|
+
# ── M5 선언된 cwd (M4 를 그 자리에서 돌리므로 먼저) ────────────────────────
|
|
177
|
+
case "$CAP_requires_cwd" in
|
|
178
|
+
/*) [ -d "$CAP_requires_cwd" ] && _ok "M5" "requires_cwd 실재: $CAP_requires_cwd" \
|
|
179
|
+
|| _fail "M5" "requires_cwd 가 절대경로지만 존재하지 않는다: $CAP_requires_cwd" ;;
|
|
180
|
+
'') _fail "M5" "requires_cwd 미선언 — 콕핏이 어디서 부를지 알 수 없다" ;;
|
|
181
|
+
*) _fail "M5" "requires_cwd 가 절대경로가 아니다: $CAP_requires_cwd" ;;
|
|
182
|
+
esac
|
|
183
|
+
|
|
184
|
+
# ── M4 캘리브레이션 쌍 — 선언 + **실행** ──────────────────────────────────
|
|
185
|
+
if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
|
|
186
|
+
_fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
|
|
187
|
+
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
|
|
188
|
+
printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
189
|
+
elif ! _validate_arm_args "$CAP_cal_pos_args" || ! _validate_arm_args "$CAP_cal_neg_args"; then
|
|
190
|
+
printf ' ⏭ M4 — args 검문 실패로 arm 을 실행하지 않았다(SKIPPED, PASS 아님)\n'
|
|
191
|
+
else
|
|
192
|
+
local pos_name neg_name pos_rc neg_rc pos_name2
|
|
193
|
+
_run_arm "$CAP_cal_pos_args"; pos_rc="$ARM_RC"; pos_name="$ARM_NAME"
|
|
194
|
+
[ "$pos_rc" = "127" ] && _fail "M4" "requires_cwd 로 진입 실패 — arm 을 돌릴 수 없다"
|
|
195
|
+
_run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME" # reps=2 (M3 부분 방어)
|
|
196
|
+
_run_arm "$CAP_cal_neg_args"; neg_rc="$ARM_RC"; neg_name="$ARM_NAME"
|
|
197
|
+
|
|
198
|
+
if [ -z "$pos_name" ]; then
|
|
199
|
+
_fail "M4" "양성 arm 의 exit $pos_rc 가 선언된 enum 밖 — enum 밖 값은 HARNESS_ERROR 이지 PASS 가 아니다"
|
|
200
|
+
elif [ "$pos_name" != "$CAP_cal_pos_expect" ]; then
|
|
201
|
+
_fail "M4" "양성 arm 이 선언과 다르다: expect=$CAP_cal_pos_expect actual=$pos_name (exit $pos_rc)"
|
|
202
|
+
elif [ "$pos_name" != "$pos_name2" ]; then
|
|
203
|
+
_fail "M4/M3" "같은 양성 arm 2회에 verdict 가 갈렸다: $pos_name vs $pos_name2 — 모델 독립성 미충족"
|
|
204
|
+
elif [ -z "$neg_name" ]; then
|
|
205
|
+
_fail "M4" "음성 arm 의 exit $neg_rc 가 선언된 enum 밖"
|
|
206
|
+
elif [ "$neg_name" != "$CAP_cal_neg_expect" ]; then
|
|
207
|
+
_fail "M4" "음성 arm 이 선언과 다르다: expect=$CAP_cal_neg_expect actual=$neg_name (exit $neg_rc)"
|
|
208
|
+
elif [ "$pos_name" = "$neg_name" ]; then
|
|
209
|
+
_fail "M4" "양성·음성이 같은 verdict($pos_name) — 답을 아는 두 케이스를 못 가른다"
|
|
210
|
+
else
|
|
211
|
+
_ok "M4" "known-pair 실행 통과: 양성→$pos_name(x2 일치) · 음성→$neg_name"
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# ── 검증 불가 축의 정직한 표기 (판정 아님) ────────────────────────────────
|
|
216
|
+
[ "$CAP_writes" = "read-only" ] && \
|
|
217
|
+
printf ' ⚠️ writes: read-only 는 **등록자 주장**이다 — 이 검사기는 그 진위를 못 잰다(헤더 §잔여 참조)\n'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# ── self-test (known-pair: 통과해야 할 선언 1 · 막혀야 할 선언 6) ─────────────
|
|
221
|
+
_self_test() {
|
|
222
|
+
local T; T="$(mktemp -d)"; local pass=0 fail=0
|
|
223
|
+
_t() {
|
|
224
|
+
local o; o="$(bash "$0" "$3" 2>&1)"; local r=$?
|
|
225
|
+
if [ "$r" = "$2" ]; then pass=$((pass+1)); printf ' ✅ %-34s rc=%s\n' "$1" "$r"
|
|
226
|
+
else fail=$((fail+1)); printf ' ❌ %-34s rc=%s (기대 %s)\n%s\n' "$1" "$r" "$2" "$o"; fi
|
|
227
|
+
}
|
|
228
|
+
printf 'capability_registry_check --self-test\n'
|
|
229
|
+
cat > "$T/good.cap" <<EOF
|
|
230
|
+
id: selftest:true-false
|
|
231
|
+
entry: bash $T/probe.sh
|
|
232
|
+
requires_cwd: $T
|
|
233
|
+
verdict_channel: exit
|
|
234
|
+
verdict_enum: 0=PASS 1=FAIL 3=DID_NOT_RUN
|
|
235
|
+
approval: auto
|
|
236
|
+
reversibility: reversible
|
|
237
|
+
residency: public
|
|
238
|
+
degrade: fail-closed
|
|
239
|
+
tier_floor: none
|
|
240
|
+
writes: read-only
|
|
241
|
+
judge: mechanical
|
|
242
|
+
verdict_binding: FAIL
|
|
243
|
+
calibration_positive_args: ok
|
|
244
|
+
calibration_positive_expect: PASS
|
|
245
|
+
calibration_negative_args: no
|
|
246
|
+
calibration_negative_expect: FAIL
|
|
247
|
+
EOF
|
|
248
|
+
printf '#!/bin/sh\n[ "$1" = ok ] && exit 0\nexit 1\n' > "$T/probe.sh"
|
|
249
|
+
_t "known-positive: 온전한 선언" 0 "$T/good.cap"
|
|
250
|
+
sed 's/^verdict_enum: .*/verdict_enum: 0=PASS 1=FAIL/' "$T/good.cap" > "$T/no_dnr.cap"
|
|
251
|
+
_t "추가조항: 「안 돌았다」 없음" 1 "$T/no_dnr.cap"
|
|
252
|
+
sed 's/^calibration_negative_expect: .*/calibration_negative_expect: PASS/' "$T/good.cap" > "$T/badpair.cap"
|
|
253
|
+
_t "M4: 음성이 선언과 불일치" 1 "$T/badpair.cap"
|
|
254
|
+
grep -v '^calibration_' "$T/good.cap" > "$T/nocal.cap"
|
|
255
|
+
_t "M4: 캘리브레이션 쌍 미선언" 1 "$T/nocal.cap"
|
|
256
|
+
sed 's/^judge: .*/degrad: fail-closed/' "$T/good.cap" > "$T/typo.cap"
|
|
257
|
+
_t "D3: 오타 축은 무시 아닌 실패" 1 "$T/typo.cap"
|
|
258
|
+
sed "s|^entry: .*|entry: bash $T/probe.sh \| grep x|" "$T/good.cap" > "$T/shellstr.cap"
|
|
259
|
+
_t "M1: entry 가 셸 문자열" 1 "$T/shellstr.cap"
|
|
260
|
+
sed 's|^requires_cwd: .*|requires_cwd: relative/path|' "$T/good.cap" > "$T/relcwd.cap"
|
|
261
|
+
_t "M5: requires_cwd 상대경로" 1 "$T/relcwd.cap"
|
|
262
|
+
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
263
|
+
rm -rf "$T"
|
|
264
|
+
[ "$fail" -eq 0 ] || return 1
|
|
265
|
+
return 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
[ "${1:-}" = "--self-test" ] && { _self_test; exit $?; }
|
|
269
|
+
[ $# -ge 1 ] || _die "capfile 인자가 없다. 사용법: $0 <capfile> [<capfile>...]"
|
|
270
|
+
|
|
271
|
+
printf 'capability_registry_check — M1–M5 + 추가조항 (등록 시점)\n'
|
|
272
|
+
for f in "$@"; do _check_one "$f"; done
|
|
273
|
+
|
|
274
|
+
printf '\n'
|
|
275
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
276
|
+
printf '✅ REGISTRABLE — 전 capfile 이 M1–M5 + 추가조항 통과\n'; exit "$RC_OK"
|
|
277
|
+
else
|
|
278
|
+
printf '❌ REJECTED — 등록 불가. 그 표면은 오늘 있던 자리(dispatch 엔트리)에 그대로 남는다 — 손실이 아니다\n'
|
|
279
|
+
exit "$RC_REJECT"
|
|
280
|
+
fi
|
package/scripts/count_check.sh
CHANGED
|
@@ -91,7 +91,53 @@ count_check() { # count_check <label> <file> <expected-string>
|
|
|
91
91
|
count_check "fh-meta plugin.json" plugins/fh-meta/.claude-plugin/plugin.json "${meta_sk} skills + ${meta_ag} agents"
|
|
92
92
|
count_check "fh-commons plugin.json" plugins/fh-commons/.claude-plugin/plugin.json "${com_sk} skills"
|
|
93
93
|
count_check "marketplace.json fh-meta" .claude-plugin/marketplace.json "${meta_sk} skills + ${meta_ag} agents"
|
|
94
|
-
|
|
94
|
+
# ── README 렌더링만 per-repo override 를 받는다 ──────────────────────────────────────────────
|
|
95
|
+
# 왜 이 한 줄만 다른가: plugin.json·marketplace.json 의 문자열은 **기계 결합**(영문 고정,
|
|
96
|
+
# 소비처가 파싱한다)이지만 README 헤더는 **사람이 읽는 산문**이라 하류 하네스가 자기 언어로 쓴다.
|
|
97
|
+
# 이 스크립트는 공유층이라 여기에 렌더링을 하드코딩하면 다음 sync 가 하류의 지역화를 덮고,
|
|
98
|
+
# 그 레인은 **이식 이후 한 번도 통과 못 하는 검사**가 된다 — 통과 불가능한 게이트는 게이트가
|
|
99
|
+
# 아니라 `--no-verify` 훈련기다(실측: pmh-dev #55 가 그 상태를 손으로 되돌려야 했고, 다음
|
|
100
|
+
# sync 에 다시 깨질 예정이었다. 이슈 chronoloy-dev/pmh-dev#57).
|
|
101
|
+
#
|
|
102
|
+
# 규약: 소비 레포는 `.claude/rules/count_readme_format` 에 printf 템플릿 한 줄을 둔다
|
|
103
|
+
# (`%s` 두 개 = skills 수, agents 수). 그 파일을 **자기 sync 제외목록에 등재**해야 한다 —
|
|
104
|
+
# 등재 안 하면 이 override 자체가 덮이므로, 그 등재가 이 규약의 전제다.
|
|
105
|
+
# 부재 → 영문 기본값. 즉 FH 자신과 지역화 안 한 설치는 종전과 동일하다(fail-closed 아님:
|
|
106
|
+
# README 렌더링은 가역 표면이고, 없는 파일을 요구하면 새 설치가 전부 막힌다).
|
|
107
|
+
README_FMT_FILE="${COUNT_README_FORMAT_FILE:-.claude/rules/count_readme_format}"
|
|
108
|
+
if [ -r "$README_FMT_FILE" ]; then
|
|
109
|
+
README_FMT=$(head -1 "$README_FMT_FILE")
|
|
110
|
+
case "$README_FMT" in
|
|
111
|
+
*%s*%s*) : ;; # %s 두 개 필수 — 하나뿐이면 두 번째 수치가 검사에서 빠진다(무음 축소)
|
|
112
|
+
*) echo "FAIL count: README format override 가 %s 를 두 개 갖지 않는다: '$README_FMT' ($README_FMT_FILE)"; fail=1; README_FMT="" ;;
|
|
113
|
+
esac
|
|
114
|
+
else
|
|
115
|
+
README_FMT='%s skills · %s agents'
|
|
116
|
+
fi
|
|
117
|
+
# 렌더 결과를 **검사한 뒤에** 패턴으로 쓴다. 렌더된 문자열이 그대로 `grep -qE` 패턴이 되므로:
|
|
118
|
+
# · 개행이 들어가면 grep -E 는 그것을 패턴 구분자로 읽어 **OR 검색**이 된다 — 실측: 템플릿
|
|
119
|
+
# `%s\n%s` 는 stale README(99/99)를 PASS 시켰다(뒷조각 `8` 이 "Node 18" 에 걸림).
|
|
120
|
+
# · printf 가 실패해도(잘못된 지시자) rc 를 안 봐서 **부분 렌더**가 패턴이 됐다.
|
|
121
|
+
# 이건 mandatory-pass 게이트이고 selfcheck→prepublishOnly · pre-commit · CI 세 경계에 물려 있다.
|
|
122
|
+
# 대상 독자가 하류 하네스 저자(한국어 템플릿 끝에 개행을 붙이는 건 자연스러운 실수)라 더 그렇다.
|
|
123
|
+
# 기존 가드 `*%s*%s*` 는 "%s 가 둘인가" 만 보지 **렌더 결과**를 안 본다. (배포 직전 보안 패스 A-2)
|
|
124
|
+
if [ -n "$README_FMT" ]; then
|
|
125
|
+
# shellcheck disable=SC2059 # 템플릿은 선언 파일에서 온다 — 그게 이 기능의 요점
|
|
126
|
+
README_STR=$(printf "$README_FMT" "$total_sk" "$total_ag") || {
|
|
127
|
+
echo "FAIL count: README format override 를 렌더할 수 없다: '$README_FMT' ($README_FMT_FILE)"; fail=1; README_STR=""; }
|
|
128
|
+
case "$README_STR" in
|
|
129
|
+
*"
|
|
130
|
+
"*) echo "FAIL count: README format override 가 개행을 렌더한다 — grep -E 패턴이 쪼개져 OR 검색이 된다: '$README_FMT'"; fail=1; README_STR="" ;;
|
|
131
|
+
esac
|
|
132
|
+
# 두 수치가 실제로 렌더 결과에 들어갔는지(`%.0s` 류가 값을 삼키는 경우 차단)
|
|
133
|
+
if [ -n "$README_STR" ]; then
|
|
134
|
+
case "$README_STR" in *"$total_sk"*) : ;; *) echo "FAIL count: 렌더 결과에 skills 수($total_sk)가 없다: '$README_STR'"; fail=1; README_STR="" ;; esac
|
|
135
|
+
fi
|
|
136
|
+
if [ -n "$README_STR" ]; then
|
|
137
|
+
case "$README_STR" in *"$total_ag"*) : ;; *) echo "FAIL count: 렌더 결과에 agents 수($total_ag)가 없다: '$README_STR'"; fail=1; README_STR="" ;; esac
|
|
138
|
+
fi
|
|
139
|
+
[ -n "$README_STR" ] && count_check "README header" README.md "$README_STR"
|
|
140
|
+
fi
|
|
95
141
|
count_check "local_fh_context fh-meta" templates/local_fh_context.md "(fh-meta, ${meta_sk})"
|
|
96
142
|
|
|
97
143
|
if [ "$fail" -ne 0 ]; then
|