@chrono-meta/fh-gate 1.4.95 → 1.4.97
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 +11 -4
- package/README.md +19 -0
- 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 +113 -1
- package/package.json +10 -2
- 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/compaction_probe.sh +153 -10
- package/scripts/consent_registry_check.sh +63 -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/lane_runner_check.sh +425 -0
- package/scripts/package_coverage_check.sh +190 -35
- package/scripts/psa_probe_capability.sh +78 -0
- package/scripts/psa_scan_lib.sh +164 -0
- package/scripts/public_surface_scan_files.sh +38 -0
- package/scripts/selfcheck.sh +346 -28
- package/scripts/test_capability_entrypoint_shipping.sh +132 -0
- package/scripts/test_consent_registry.sh +54 -0
- package/scripts/test_count_check_readme_format_lanes.sh +75 -0
- package/scripts/test_degrade_scan_shell_probes.sh +415 -0
- package/scripts/test_marker_crossfamily_lanes.sh +10 -1
- package/scripts/test_marker_floor_lanes.sh +25 -1
- package/scripts/test_psa_singlefile_lanes.sh +288 -0
- package/scripts/test_reviewer_capability_conformance.sh +10 -1
- package/scripts/test_selfcheck_state_lanes.sh +173 -0
- package/scripts/validate_yaml.sh +146 -0
- package/templates/degrade_direction_scan.sh +276 -6
- package/templates/goal-quench-settings-merged.json +30 -0
|
@@ -40,10 +40,159 @@ PASS='(True|"PASS"|'"'"'PASS'"'"'|"ALLOW"|'"'"'ALLOW'"'"'|"OK"|'"'"'OK'"'"'|"VAL
|
|
|
40
40
|
# incl. this gate's own pre-push/pre-commit-hook trigger category — must not be invisibly dropped).
|
|
41
41
|
# Anything else is tracked as UNSCANNABLE so a load-bearing surface in another language is reported
|
|
42
42
|
# as "not covered", never silently folded into an "advisory clean" (M#2, steel-quench 2026-07-03).
|
|
43
|
-
FILES=(); UNSCANNABLE=()
|
|
43
|
+
FILES=(); UNSCANNABLE=(); MD_ORIGIN=(); UNMEASURED=(); DISCOVERY_ERR=()
|
|
44
|
+
# UNSCANNABLE vs UNMEASURED — 한 통에 넣으면 S 가 살아난다(재검 2026-08-12 지목).
|
|
45
|
+
# UNSCANNABLE = 측정 대상이 아니다(펜스 없는 산문 md, py/sh 아닌 파일). 정상 상태.
|
|
46
|
+
# UNMEASURED = 재려 했는데 **못 쟀다**(python3/cksum 부재로 추출 실패). 이건 절대 CLEAN 이 아니다.
|
|
47
|
+
# 첫 수리는 둘을 UNSCANNABLE 하나에 담았고, 그 배열은 `FILES` 가 **완전히 빌 때만** exit 2 로
|
|
48
|
+
# 승격된다 — 스캔 가능한 .sh 가 하나라도 섞이면 추출 실패가 note 한 줄로 강등되고 rc=0 이 나갔다.
|
|
49
|
+
# 즉 「측정 못 함을 CLEAN 으로 렌더」가 지배 경로(디렉터리 스캔 → typed capability)에 그대로 남아
|
|
50
|
+
# 있었다 — 이 릴리스가 고치겠다고 선언한 바로 그것이다.
|
|
51
|
+
|
|
52
|
+
# ── Markdown fence extraction (2026-08-11) ────────────────────────────────────────────────────
|
|
53
|
+
# The bash this harness actually EXECUTES largely does not live in .sh files — it lives in ```bash
|
|
54
|
+
# fences inside SKILL.md / SKILL_detail.md, and the scanner could not see a single line of it.
|
|
55
|
+
# Measured that day: one sweep batch extracted 61 fenced blocks across 12 skills and found the
|
|
56
|
+
# S5 pipefail-fallback class alive in four of them (harness-doctor E3/E7/Step-11, install-doctor),
|
|
57
|
+
# each rendering a dead check as a PASS. The scanner was not wrong before — it reported those files
|
|
58
|
+
# as UNSCANNABLE (exit 2, "NOT scanned, NOT clean"), which is the honest degrade. But "honestly
|
|
59
|
+
# unmeasured" on the surface where the code actually lives is a coverage hole, not a resolution.
|
|
60
|
+
#
|
|
61
|
+
# Method: for a markdown file, write a shadow .sh whose fenced-block lines sit at their ORIGINAL
|
|
62
|
+
# line numbers and whose every other line is blank. Line numbers then map 1:1, so an emitted
|
|
63
|
+
# finding points at the real file:line with no offset arithmetic to get wrong. A markdown file
|
|
64
|
+
# with NO bash fence stays UNSCANNABLE — nothing was extracted, so nothing was measured.
|
|
65
|
+
# python3 는 이 레포의 선언된 런타임이 아니다(`package.json engines` = node 만). 부재하면 추출이
|
|
66
|
+
# 불가능하고, 그때 md 를 FILES 에도 UNSCANNABLE 에도 넣지 않으면 **요약에서 통째로 사라져**
|
|
67
|
+
# "no smells in N scanned files / exit 0" 이 된다 — 이 릴리스가 고치는 바로 그 클래스를 수리가
|
|
68
|
+
# 재생산한 것이다(배포 직전 보안 패스가 적발, 2026-08-12). 자매 스크립트들은 이미 옳게 한다:
|
|
69
|
+
# validate_yaml → UNCALIBRATED + exit 2, package_coverage → exit 1. 여기도 같은 방향으로 맞춘다.
|
|
70
|
+
_MD_OK=1
|
|
71
|
+
# `cut` joined this list 2026-08-12 (cross-family, gpt-5.5): the uniqueness token is produced by
|
|
72
|
+
# `cksum | cut`, and gating only `cksum` left the pipeline half-guarded — with `cut` missing the
|
|
73
|
+
# token is EMPTY, which restores the A-1 shadow-name collision the cksum guard exists to prevent.
|
|
74
|
+
# Guarding one stage of a pipeline is not guarding the pipeline.
|
|
75
|
+
for _dep in python3 cksum cut; do command -v "$_dep" >/dev/null 2>&1 || _MD_OK=0; done
|
|
76
|
+
# `find` is not optional and its absence is not a small result: EVERY target file — .sh, .py and
|
|
77
|
+
# .md alike — is discovered through it, and the discovery calls redirect stderr. Without this gate
|
|
78
|
+
# a missing/broken `find` yields an empty file list and the run prints "no scannable target files"
|
|
79
|
+
# and exits 0 = clean, on a corpus it never looked at. Same class the fence work exists to close,
|
|
80
|
+
# one layer further up (cross-family, gpt-5.5, 2026-08-12).
|
|
81
|
+
if ! command -v find >/dev/null 2>&1; then
|
|
82
|
+
echo "degrade-scan: INSTRUMENT ERROR — \`find\` is unavailable, so NO file was discovered."
|
|
83
|
+
echo "This is not 'clean' and not 'no targets'; nothing was measured. Fix PATH and re-run."
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
# cksum 도 미선언 의존성이다 — 첫 수리는 python3 만 가드하고 cksum 은 rc 미검사로 도입했다.
|
|
87
|
+
# 부재 시 유일성 토큰이 **빈 문자열**이 돼 shadow 이름 충돌(A-1)이 그대로 부활한다(실증).
|
|
88
|
+
# mktemp 실패 시 예측 가능한 경로(/tmp/degradescan.$$)로 폴백하던 것을 제거했다: 그 경로를 mkdir
|
|
89
|
+
# 하지 않아 정상 폴백은 100% 실패하고, 반대로 공격자가 그 이름에 심링크를 심어두면 **성공해서
|
|
90
|
+
# 레포 밖에 쓴다**(실증됨). 즉 "공격받을 때만 동작하는 코드" 였다.
|
|
91
|
+
_MD_TMP="$(mktemp -d 2>/dev/null)" || { _MD_TMP=""; _MD_OK=0; }
|
|
92
|
+
trap '[ -n "$_MD_TMP" ] && rm -rf "$_MD_TMP"' EXIT
|
|
93
|
+
# LANGUAGE-SPLIT shadows (2026-08-12). The first version extracted only ```bash-family fences, and
|
|
94
|
+
# extending it to ```python by widening the same regex would have written python source into a `.sh`
|
|
95
|
+
# shadow — where `is_sh` is decided by the extension, so the SHELL probes would run on python and the
|
|
96
|
+
# python probes' own file-shape assumptions would be met by accident. That is precisely the
|
|
97
|
+
# "collected but probed with the wrong family" false-clean this scanner already names (n+10 below).
|
|
98
|
+
# So the extraction is per-language and each language gets its own shadow extension; one markdown
|
|
99
|
+
# file can now yield up to two shadows, and `is_sh` keeps deciding correctly by extension.
|
|
100
|
+
# WHY python at all: `except: pass` / `.get(k, True)` — the shapes probes A–E exist for — live in
|
|
101
|
+
# ```python fences, not in .py files, in this corpus. install-doctor's S came from exactly there.
|
|
102
|
+
_md_shadow() { # $1 = markdown path; $2 = fence language class (sh|py)
|
|
103
|
+
# echoes shadow path, or nothing when that language has no fence in the file
|
|
104
|
+
# 반환 규약(2026-08-12, cross-family 지적으로 분리): 0 = 추출됨 · **1 = 그 언어 펜스가 없다(정상)**
|
|
105
|
+
# · **2 = 추출을 시도했으나 실패했다(못 쟀다)**. 첫 판은 둘을 똑같이 1 로 접었고, 그래서 읽을 수
|
|
106
|
+
# 없는 SKILL.md 한 개가 「펜스 없음」= UNSCANNABLE 로 렌더돼 같은 디렉토리에 깨끗한 .sh 가 하나라도
|
|
107
|
+
# 있으면 전체가 exit 0 = clean 으로 나갔다. 이 파일이 존재하는 이유인 바로 그 클래스다.
|
|
108
|
+
local src="$1" lang="$2" out ext pat tok rc
|
|
109
|
+
[ "$_MD_OK" = 1 ] || return 2 # 추출 불가 → 호출부가 UNMEASURED 로 보낸다(드롭 금지)
|
|
110
|
+
case "$lang" in
|
|
111
|
+
sh) ext=sh; pat='bash|sh|shell|zsh' ;;
|
|
112
|
+
py) ext=py; pat='python|python3|py' ;;
|
|
113
|
+
*) return 2 ;; # 알 수 없는 언어 클래스 = 추출 실패, 조용한 성공 금지
|
|
114
|
+
esac
|
|
115
|
+
# 경로 해시를 붙인다: `tr '/' '_'` 만 쓰면 a/b.md 와 a_b.md 가 **같은 shadow** 로 매핑돼
|
|
116
|
+
# 뒤엣것이 앞엣것을 덮고, 결함 파일이 통째로 사라진 채 카운트는 2로 찍힌다(실증됨).
|
|
117
|
+
# 확장자가 언어를 가르므로 같은 md 의 sh/py shadow 는 서로 안 덮는다.
|
|
118
|
+
tok=$(printf '%s' "$src" | cksum | cut -d' ' -f1)
|
|
119
|
+
# 토큰이 비면 유일성이 사라져 a/b.md 와 a_b.md 가 같은 shadow 로 접힌다. 빈 값을 그냥 쓰지 말고
|
|
120
|
+
# 「못 쟀다」로 나가라 — 조용히 충돌한 쪽이 항상 더 나쁘다.
|
|
121
|
+
case "$tok" in ''|*[!0-9]*) return 2 ;; esac
|
|
122
|
+
out="$_MD_TMP/$(echo "$src" | tr '/' '_')-$tok.$ext"
|
|
123
|
+
python3 - "$src" "$out" "$pat" <<'PYEOF'
|
|
124
|
+
import sys, re
|
|
125
|
+
src, out, pat = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
126
|
+
lines = open(src, encoding='utf-8', errors='replace').read().split('\n')
|
|
127
|
+
shadow = [''] * len(lines)
|
|
128
|
+
inside = False
|
|
129
|
+
found = False
|
|
130
|
+
for i, l in enumerate(lines):
|
|
131
|
+
s = l.strip()
|
|
132
|
+
if not inside and re.match(r'^```(' + pat + r')\s*$', s):
|
|
133
|
+
inside = True; continue
|
|
134
|
+
if inside and s == '```':
|
|
135
|
+
inside = False; continue
|
|
136
|
+
if inside:
|
|
137
|
+
shadow[i] = l; found = True
|
|
138
|
+
open(out, 'w', encoding='utf-8').write('\n'.join(shadow))
|
|
139
|
+
sys.exit(0 if found else 3)
|
|
140
|
+
PYEOF
|
|
141
|
+
rc=$?
|
|
142
|
+
case $rc in
|
|
143
|
+
0) ;; # 추출 성공
|
|
144
|
+
3) return 1 ;; # 그 언어의 펜스가 이 파일에 없다 — 정상 상태
|
|
145
|
+
*) return 2 ;; # 읽기 실패·인코딩·인터프리터 오류 — 못 쟀다. 「펜스 없음」과 같은 값 금지
|
|
146
|
+
esac
|
|
147
|
+
printf '%s' "$out"
|
|
148
|
+
}
|
|
149
|
+
# 한 md 에서 sh·py 두 shadow 를 모두 수집한다. 어느 쪽도 안 나오면 그때만 UNSCANNABLE/UNMEASURED.
|
|
150
|
+
# 반환: 0 = 하나 이상 수집됨, 1 = 그 파일에 스캔 대상 펜스 없음, 2 = 추출 자체가 불가(못 쟀다)
|
|
151
|
+
_md_collect() {
|
|
152
|
+
local src="$1" lang shp got=0 err=0 rc
|
|
153
|
+
for lang in sh py; do
|
|
154
|
+
shp=$(_md_shadow "$src" "$lang"); rc=$?
|
|
155
|
+
case $rc in
|
|
156
|
+
0) [ -n "$shp" ] && { FILES+=("$shp"); MD_ORIGIN+=("$shp=$src"); got=1; } ;;
|
|
157
|
+
1) ;; # 이 언어 펜스 없음 — 정상
|
|
158
|
+
*) err=1 ;; # 추출 실패 — 이 파일은 못 쟀다
|
|
159
|
+
esac
|
|
160
|
+
done
|
|
161
|
+
# 실패가 하나라도 있으면 «못 쟀다»가 이긴다. 다른 언어에서 뭔가 건졌다는 사실이
|
|
162
|
+
# 못 잰 언어를 덮으면 부분 커버리지가 완전 커버리지로 렌더된다.
|
|
163
|
+
[ "$err" = 1 ] && return 2
|
|
164
|
+
[ "$got" = 1 ] && return 0
|
|
165
|
+
return 1
|
|
166
|
+
}
|
|
44
167
|
for t in "${TARGETS[@]}"; do
|
|
168
|
+
# HYPHEN-LEADING TARGETS are normalized ONCE, here, for every branch (2026-08-12, round 6).
|
|
169
|
+
# The first fix did this inside the directory branch only, so a direct FILE target named
|
|
170
|
+
# `-bad.sh` was collected verbatim and every later `grep "$f"` parsed it as an option cluster —
|
|
171
|
+
# producing "no smells in 1 scanned file, exit 0" for a file carrying a known-positive. Fixing
|
|
172
|
+
# one branch of a two-branch dispatch is the half-fix shape this repo already has a scan for.
|
|
173
|
+
case "$t" in
|
|
174
|
+
-*) t="./$t" ;;
|
|
175
|
+
esac
|
|
45
176
|
if [ -d "$t" ]; then
|
|
46
|
-
|
|
177
|
+
# SYMLINKED TARGET (2026-08-12, cross-family gpt-5.5): `[ -d ]` follows a symlink, `find` does
|
|
178
|
+
# not — `find /path/to/symlink -type f` enumerates NOTHING and the run printed "no scannable
|
|
179
|
+
# (py/sh) target files" and exited 0 for a directory full of known-positives. `-H` makes find
|
|
180
|
+
# follow the command-line argument only (not links found during the walk, which is what would
|
|
181
|
+
# invite cycles). Measured: symlink target → 0 files without it, 1 with it.
|
|
182
|
+
# (`$t` is already `./`-normalized above, so no per-branch hyphen handling is needed here.)
|
|
183
|
+
_T=(-H "$t")
|
|
184
|
+
# DISCOVERY ERRORS (2026-08-12, cross-family gpt-5.5): the `find` preflight above proves the
|
|
185
|
+
# BINARY exists; it says nothing about whether this particular traversal worked. Every discovery
|
|
186
|
+
# call below redirects stderr and reads its output through a process substitution, which throws
|
|
187
|
+
# the exit status away — so an unreadable target directory produced an empty file list and the
|
|
188
|
+
# run printed "no scannable (py/sh) target files" and exited 0 = CLEAN, having looked at nothing.
|
|
189
|
+
# Reproduced with a mode-000 directory containing a known-positive `bad.sh`.
|
|
190
|
+
# One extra traversal per target is the price; a silent zero is not an acceptable alternative.
|
|
191
|
+
_derr=$(find "${_T[@]}" -type f 2>&1 >/dev/null); _drc=$?
|
|
192
|
+
if [ "$_drc" -ne 0 ] || [ -n "$_derr" ]; then
|
|
193
|
+
DISCOVERY_ERR+=("$t — ${_derr:-find exited $_drc}")
|
|
194
|
+
fi
|
|
195
|
+
while IFS= read -r f; do FILES+=("$f"); done < <(find "${_T[@]}" -type f \( -name '*.py' -o -name '*.sh' \) 2>/dev/null)
|
|
47
196
|
# Shebang pass — this is what makes git hooks visible at all. Measured 2026-07-28:
|
|
48
197
|
# `templates/.git-hooks` (files named `pre-push`, no extension, under a dotted directory) —
|
|
49
198
|
# FH's own mechanical floor — reported "no scannable (py/sh) target files", exit 0.
|
|
@@ -60,16 +209,48 @@ for t in "${TARGETS[@]}"; do
|
|
|
60
209
|
*.md|*.json|*.yaml|*.yml|*.txt|*.lock|*.png|*.jpg|*.svg|*.pdf|*.zip) continue ;;
|
|
61
210
|
esac
|
|
62
211
|
head -n1 "$f" 2>/dev/null | grep -qE '^#!.*\b(ba|z|k)?sh\b' && FILES+=("$f")
|
|
63
|
-
done < <(find "$
|
|
212
|
+
done < <(find "${_T[@]}" -type f 2>/dev/null)
|
|
213
|
+
while IFS= read -r f; do
|
|
214
|
+
_md_collect "$f"
|
|
215
|
+
case $? in
|
|
216
|
+
0) ;; # sh/py 펜스에서 하나 이상 수집됨
|
|
217
|
+
1) UNSCANNABLE+=("$f") ;; # 펜스가 없다 — 측정 대상이 아님(정상)
|
|
218
|
+
*) UNMEASURED+=("$f") ;; # 추출 자체가 불가 — 못 쟀다(CLEAN 으로 접히면 안 된다)
|
|
219
|
+
esac
|
|
220
|
+
done < <(find "${_T[@]}" -type f -name '*.md' 2>/dev/null)
|
|
64
221
|
elif [ -f "$t" ]; then
|
|
65
222
|
tb="${t##*/}"
|
|
66
223
|
case "$tb" in
|
|
67
224
|
*.py|*.sh) FILES+=("$t") ;;
|
|
225
|
+
*.md)
|
|
226
|
+
_md_collect "$t"
|
|
227
|
+
case $? in
|
|
228
|
+
0) ;; # at least one sh/py fence extracted
|
|
229
|
+
1) UNSCANNABLE+=("$t") ;; # no scannable fence — not a measurement target
|
|
230
|
+
*) UNMEASURED+=("$t") ;; # extraction impossible — could not measure
|
|
231
|
+
esac ;;
|
|
68
232
|
*.*) UNSCANNABLE+=("$t") ;;
|
|
69
233
|
*) if head -n1 "$t" 2>/dev/null | grep -qE '^#!.*\b(ba|z|k)?sh\b'; then FILES+=("$t"); else UNSCANNABLE+=("$t"); fi ;;
|
|
70
234
|
esac
|
|
71
235
|
fi
|
|
72
236
|
done
|
|
237
|
+
# 탐색 자체가 실패했으면 그 타깃은 **못 쟀다**. 파일 목록이 비어 나오는 것과 구분이 안 되므로
|
|
238
|
+
# 여기서 명시적으로 비-clean 을 건다 — 이 블록이 없으면 「읽을 수 없는 디렉토리」가
|
|
239
|
+
# 「스캔 대상 없음 · exit 0」으로 렌더된다(같은 파일이 고치는 그 클래스, 한 층 위).
|
|
240
|
+
if [ ${#DISCOVERY_ERR[@]} -gt 0 ]; then
|
|
241
|
+
echo "degrade-scan: ${#DISCOVERY_ERR[@]} target(s) COULD NOT BE MEASURED (directory traversal failed):"
|
|
242
|
+
printf ' (discovery-error) %s\n' "${DISCOVERY_ERR[@]}"
|
|
243
|
+
echo "This is NOT 'clean' — part of the requested surface was never enumerated."
|
|
244
|
+
_FORCE_NONCLEAN=2
|
|
245
|
+
fi
|
|
246
|
+
# 못 잰 파일이 있으면 hits 와 무관하게 비-clean 이다. 이 블록이 FILES 비었을 때만 도는 구조가
|
|
247
|
+
# S 의 정체였다 — 스캔 가능한 파일이 하나라도 있으면 「못 쟀다」가 note 로 강등되고 rc=0 이 나갔다.
|
|
248
|
+
if [ ${#UNMEASURED[@]} -gt 0 ]; then
|
|
249
|
+
echo "degrade-scan: ${#UNMEASURED[@]} file(s) COULD NOT BE MEASURED (markdown fence extraction unavailable — python3/cksum missing):"
|
|
250
|
+
printf ' (unmeasured) %s\n' "${UNMEASURED[@]}"
|
|
251
|
+
echo "This is NOT 'clean' — the fence surface was not scanned at all. Install python3+cksum or scan those files elsewhere."
|
|
252
|
+
_FORCE_NONCLEAN=2
|
|
253
|
+
fi
|
|
73
254
|
if [ ${#FILES[@]} -eq 0 ]; then
|
|
74
255
|
if [ ${#UNSCANNABLE[@]} -gt 0 ]; then
|
|
75
256
|
echo "degrade-scan: ${#UNSCANNABLE[@]} changed file(s) are OUTSIDE the scannable set (py/sh) — NOT scanned, NOT 'clean':"
|
|
@@ -77,11 +258,28 @@ if [ ${#FILES[@]} -eq 0 ]; then
|
|
|
77
258
|
echo "A load-bearing surface in another language must go straight to cross-family review."
|
|
78
259
|
exit 2 # advisory non-clean — an orchestrator keying on exit code must not read this as clean
|
|
79
260
|
fi
|
|
80
|
-
echo "degrade-scan: no scannable (py/sh) target files"; exit 0
|
|
261
|
+
echo "degrade-scan: no scannable (py/sh) target files"; exit "${_FORCE_NONCLEAN:-0}"
|
|
81
262
|
fi
|
|
82
263
|
|
|
83
264
|
hits=0
|
|
84
|
-
|
|
265
|
+
# A markdown fence is scanned through a shadow .sh (see §Markdown fence extraction). The finding
|
|
266
|
+
# must point at the file a human can open, so emit translates the shadow path back to its origin —
|
|
267
|
+
# line numbers need no adjustment because the shadow preserves them by construction. A finding that
|
|
268
|
+
# names a path under /tmp is unactionable, which would make the new coverage decorative.
|
|
269
|
+
_origin() {
|
|
270
|
+
local sh="$1" pair lbl
|
|
271
|
+
# 라벨은 shadow 확장자에서 나온다 — 하드코딩하면 python 펜스 결함이 「bash fence」로 보고돼
|
|
272
|
+
# 사람이 그 줄을 열었을 때 안 맞는 코드를 보게 된다(리포트가 거짓말을 하는 형태).
|
|
273
|
+
case "$sh" in
|
|
274
|
+
*.py) lbl='```python fence' ;;
|
|
275
|
+
*) lbl='```bash fence' ;;
|
|
276
|
+
esac
|
|
277
|
+
for pair in "${MD_ORIGIN[@]:-}"; do
|
|
278
|
+
[ "${pair%%=*}" = "$sh" ] && { printf '%s (%s)' "${pair#*=}" "$lbl"; return; }
|
|
279
|
+
done
|
|
280
|
+
printf '%s' "$sh"
|
|
281
|
+
}
|
|
282
|
+
emit() { printf ' %s:%s\n [%s] %s\n' "$(_origin "$1")" "$2" "$3" "$4"; hits=$((hits+1)); }
|
|
85
283
|
|
|
86
284
|
for f in "${FILES[@]}"; do
|
|
87
285
|
# ---- Shell-shaped probes (S*) -------------------------------------------------------------
|
|
@@ -201,6 +399,78 @@ for f in "${FILES[@]}"; do
|
|
|
201
399
|
done < <(grep -nE '(([^|]\|[[:space:]]*([a-z]+[[:space:]]+)*wc[^|]*)|(grep([[:space:]]+-[A-Za-z]*c[A-Za-z]*|[[:space:]]+--count)[^|]*))([^|]*\|[^|]*)*\|\|[[:space:]]*echo[[:space:]]+[\"'"'"']?0' "$f" 2>/dev/null \
|
|
202
400
|
| grep -vE '^[0-9]+:[[:space:]]*#' \
|
|
203
401
|
| grep -vE '#[[:space:]]*noqa[:[:space:]]*degrade')
|
|
402
|
+
|
|
403
|
+
# S6 — SHELL-DEPENDENT word split: `for x in $VAR` / `cmd -- $LIST` on an unquoted PARAMETER
|
|
404
|
+
# expansion. bash splits it on IFS; **zsh does not** (SH_WORD_SPLIT is off by default), so the
|
|
405
|
+
# loop body runs ONCE with the entire newline-joined blob as a single word. Every per-item test
|
|
406
|
+
# inside then fails against a filename that does not exist — and the loop's counters stay 0,
|
|
407
|
+
# which the surrounding code reports as "nothing found". Same family as the rest of this file:
|
|
408
|
+
# an unmeasured surface rendered as a zero. The default macOS login shell is zsh, and this
|
|
409
|
+
# corpus's bash lives in SKILL.md fences that a human may paste into that shell.
|
|
410
|
+
#
|
|
411
|
+
# SCOPE, and the discriminator this probe would be wrong without: `for f in $(cmd)` is NOT
|
|
412
|
+
# flagged. Command substitution DOES word-split in zsh — SH_WORD_SPLIT governs *parameter*
|
|
413
|
+
# expansion only. Flagging `$(...)` would have made this probe fire on the idiomatic form that
|
|
414
|
+
# behaves identically in both shells, i.e. pure noise. Verified in both shells before shipping.
|
|
415
|
+
# `"$@"` / `${arr[@]}` are excluded for the same reason: those are array expansions, which
|
|
416
|
+
# split by element in zsh too.
|
|
417
|
+
#
|
|
418
|
+
# Measured N=3 (2026-08-11..12): a scan whose `grep` returned rc=2 read as "clean" until it was
|
|
419
|
+
# re-measured; a repair session hit the same shape; and `harness-doctor` L5-A ships it live —
|
|
420
|
+
# `for f in $recent_sessions` (SKILL_detail.md:201/207), where under zsh every skill in the
|
|
421
|
+
# harness reports INACTIVE_90D. Hand-found, not scanner-found: that is what this probe closes.
|
|
422
|
+
# APPLICABILITY — S6 only fires where the execution shell is NOT pinned to a splitting shell.
|
|
423
|
+
# Measured 2026-08-12 on 28 hits, and the split is clean at 100%: all 7 hits inside markdown
|
|
424
|
+
# fences were REAL, and all 21 hits in `.sh` files carrying a `#!…bash` shebang were FALSE —
|
|
425
|
+
# bash runs those files, so the splitting is the intended semantics there, not a defect. A
|
|
426
|
+
# probe that is 75% noise trains authors to dismiss it, which is how the one hit that matters
|
|
427
|
+
# gets waved through (this file already paid that price once at 9/9 FP on S5).
|
|
428
|
+
# So: markdown-fence shadows (a human may paste them into zsh — the macOS login shell) and
|
|
429
|
+
# files whose shebang IS zsh. Nothing else.
|
|
430
|
+
# The test is INVERTED (2026-08-12, round-4 cross-family): the first draft asked "is this a
|
|
431
|
+
# markdown shadow or a zsh script?" and therefore excluded a `.sh` file with NO shebang —
|
|
432
|
+
# which is not pinned to anything either, so the defect is live there too. Ask the question
|
|
433
|
+
# that actually decides it: **does a shebang pin a splitting shell?** Unknown shell → the
|
|
434
|
+
# probe applies, which is the fail-closed direction for a detector.
|
|
435
|
+
_s6_applies=1
|
|
436
|
+
# `ksh[0-9]*` — `#!/usr/bin/env ksh93` was classified as unpinned because `\bksh\b` cannot match
|
|
437
|
+
# inside `ksh93` (a digit is a word character). ksh93 splits `$VAR` like bash, so it IS pinned.
|
|
438
|
+
head -n1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|ksh[0-9]*|dash|ash)\b|^#!.*[/ ]sh([[:space:]]|$)' && _s6_applies=""
|
|
439
|
+
if [ -n "$_s6_applies" ]; then
|
|
440
|
+
# PATTERN (widened 2026-08-12, round 5). The first form only matched a bare `$VAR` / `${VAR}`,
|
|
441
|
+
# so `for f in ${FILES:-}` — same defect, one operator away — was a FALSE NEGATIVE.
|
|
442
|
+
# The braced alternative now admits the expansion operators (`:-` `:=` `:?` `:+` `#` `%` `^` `,`)
|
|
443
|
+
# but **excludes `[` by construction** (`[^}[]*`). That exclusion is doing real work: widening
|
|
444
|
+
# the pattern is exactly what would make `${arr[@]}` match, and an array expansion splits by
|
|
445
|
+
# element in zsh too, so it is not this defect. Encoding it in the pattern rather than as a
|
|
446
|
+
# downstream `grep -v` is deliberate — the line-level filter version of this rule was deleted
|
|
447
|
+
# earlier today precisely because it killed whole lines, including real hits.
|
|
448
|
+
# `/` and `@` joined the operator class 2026-08-12 (round 6): `${FILES//old/new}` is a
|
|
449
|
+
# substitution whose RESULT still splits in bash and not in zsh — the same defect — and it was
|
|
450
|
+
# invisible because `/` was missing from the set. Measured on that construct: bash 2 words,
|
|
451
|
+
# zsh 1. `[` stays excluded so array expansions remain unmatched.
|
|
452
|
+
S6_PAT='(for[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]+in|--)[[:space:]]+(\$[A-Za-z_][A-Za-z0-9_]*|\$\{[A-Za-z_][A-Za-z0-9_]*([:#%^,!/@][^}[]*)?\})([[:space:];]|$)'
|
|
453
|
+
while IFS= read -r m; do
|
|
454
|
+
emit "$f" "${m%%:*}" "S6:wordsplit-shell-dep(sh)" "unquoted parameter expansion in a word-split position (\`for x in \$VAR\` / \`-- \$LIST\`) — bash splits on IFS but zsh does NOT, so under zsh the loop runs once over the whole blob and per-item results silently come back empty; iterate explicitly instead: \`printf '%s\\n' \"\$VAR\" | while IFS= read -r x; do …\` (or use an array). Command substitution \`\$(cmd)\` splits in both shells and is NOT this defect"
|
|
455
|
+
# NO COMMENT STRIPPING, and no array-form exclusion — both were removed 2026-08-12 after two
|
|
456
|
+
# cross-family rounds, and the reasoning is worth keeping because the removal LOOKS like a
|
|
457
|
+
# weakening and is not:
|
|
458
|
+
# · the `grep -vE '\[[@*]\]'` array exclusion was DEAD CODE. Measured against all six
|
|
459
|
+
# spellings: `$S6_PAT` requires `$name` followed by space/;/EOL, so `${arr[@]}` and
|
|
460
|
+
# `"${arr[@]}"` never match it in the first place. The filter excluded nothing real — but
|
|
461
|
+
# it matched the whole LINE, so a genuine defect whose trailing comment mentioned the safe
|
|
462
|
+
# form (`git add -- $FILES # use "${arr[@]}" later`) was silently dropped. A dead filter
|
|
463
|
+
# that only ever fires on false grounds is worse than no filter.
|
|
464
|
+
# · the `sed 's/#.*$//'` that was added to work around it then introduced its own false
|
|
465
|
+
# negative: it cannot tell a comment from a `#` inside a quoted string, so
|
|
466
|
+
# `printf "#"; git add -- $FILES` lost its real hit.
|
|
467
|
+
# Accepted trade, stated rather than hidden: a match living ONLY in a trailing comment is now
|
|
468
|
+
# reported. Full-line comments are still excluded above. Recall over precision is the right
|
|
469
|
+
# direction for an advisory detector — a missed defect is silent, a noisy line is not.
|
|
470
|
+
done < <(grep -nE "$S6_PAT" "$f" 2>/dev/null \
|
|
471
|
+
| grep -vE '^[0-9]+:[[:space:]]*#' \
|
|
472
|
+
| grep -vE '#[[:space:]]*noqa[:[:space:]]*degrade')
|
|
473
|
+
fi
|
|
204
474
|
fi
|
|
205
475
|
|
|
206
476
|
# Probe A — except/else/finally block returning a permissive value within 2 lines.
|
|
@@ -266,4 +536,4 @@ fi
|
|
|
266
536
|
# it does NOT assert the changed load-bearing surface is safe (other languages, non-code surfaces,
|
|
267
537
|
# and the lint's own recall gaps are out of scope). The load-bearing check is the cross-family review.
|
|
268
538
|
echo "degrade-scan: no default-toward-PASS smells in ${#FILES[@]} scanned py/sh file(s) — does NOT cover other languages / non-code surfaces / the cross-family check (advisory)."
|
|
269
|
-
exit 0
|
|
539
|
+
exit "${_FORCE_NONCLEAN:-0}" # 못 잰 파일이 있으면 0 이 아니다 — "안 쟀다"는 "깨끗하다"가 아니다
|
|
@@ -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
|