@chrono-meta/fh-gate 2.0.0 → 2.0.1
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/capabilities/adapters/gstack-content-safety.cap +110 -0
- package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
- package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
- package/.claude/capabilities/degrade-direction-scan.cap +24 -0
- package/.claude/capabilities/public-surface-scan.cap +24 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +9 -3
- package/CHEATSHEET.md +17 -1
- package/CLAUDE.md +2 -2
- package/README.ja.md +55 -20
- package/README.ko.md +86 -20
- package/README.md +57 -20
- package/README.zh.md +49 -19
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +96 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +1 -1
- package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +214 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +29 -10
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +62 -0
- package/package.json +10 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +17 -0
- package/scripts/adapters/gstack_content_safety.sh +186 -0
- package/scripts/adapters/mate_agent_boundary.sh +120 -0
- package/scripts/adapters/peer_resolve.sh +99 -0
- package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
- package/scripts/branch_claim.sh +14 -0
- package/scripts/capability_effect_probe.sh +340 -20
- package/scripts/capability_registry_check.sh +198 -10
- package/scripts/cluster_capability_scan.sh +629 -0
- package/scripts/fh_session_load.sh +70 -0
- package/scripts/portability_lint.sh +257 -0
- package/scripts/relay_channel.sh +70 -7
- package/scripts/selfcheck.sh +138 -1
- package/scripts/test_adapter_lanes.sh +296 -0
- package/scripts/test_capability_entrypoint_shipping.sh +8 -1
- package/scripts/test_relay_channel_lanes.sh +76 -0
- package/templates/.git-hooks/pre-commit +14 -2
|
@@ -200,6 +200,76 @@ if [ -d "$_AUDIT_DIR" ] && fh_cadence_due "$_FH_HOOK_SRC"; then
|
|
|
200
200
|
fi
|
|
201
201
|
fi
|
|
202
202
|
|
|
203
|
+
# 1-c) harness-doctor cadence — 위 1-b 와 같은 이유로, 같은 훅에서, 한 블록 더.
|
|
204
|
+
# MEASURED 2026-08-16 (weekly_audit_2026-08-16.md 🟥-3): 직전 감사(07-31)가 «산문 캐던스 미이행»을
|
|
205
|
+
# N=2 로 집계하면서 **주간감사(50일)와 harness-doctor(당시 ~49일) 둘을 함께** 지목했다. 그 뒤 16일
|
|
206
|
+
# 동안 주간감사는 50→16일로 움직였고 — 그건 1-b 가 기계화돼 있기 때문이다 — harness-doctor 는
|
|
207
|
+
# **한 번도 돌지 않아 65일**이 됐다(캐던스 30). 즉 같은 세션이 같은 날 같은 문서에서 지목한 두
|
|
208
|
+
# 캐던스가, 하나는 훅이고 하나는 산문이라는 차이만으로 정반대로 갔다. 그것이 N=3 이고,
|
|
209
|
+
# `operations.md §Recurrence escalation` 은 그때 «새 습관 규칙이 아니라 계기»를 만들라고 한다.
|
|
210
|
+
# 1-b 와 동일하게 **advisory** 다: 밀린 진단은 비가역 표면이 아니므로 표면화하고 절대 막지 않는다.
|
|
211
|
+
#
|
|
212
|
+
# 🟥 not-found ≠ 0 — 이 블록이 1-b 와 다른 유일한 지점이다. 1-b 는 파일이 없으면 조용하고, 그건
|
|
213
|
+
# 갓 클론한 설치를 나그하지 않기 위해 옳다. 그러나 harness-doctor 는 **한 번도 안 돌린 상태가
|
|
214
|
+
# 실재하는 결함**이고(이 레포가 2026-06-12 이후 정확히 그 상태였다), 파일 부재를 침묵으로 렌더하면
|
|
215
|
+
# 「없음」과 「0일 경과」가 같은 출력이 된다 — 이 레포가 반복해 배운 그 클래스다. 그래서 부재를
|
|
216
|
+
# **다른 문장으로** 말하되, «이미 굴러가는 설치인가»를 `fh_completed_*.md` 존재로 판별해 갓
|
|
217
|
+
# 클론한 설치에는 여전히 침묵한다. 그 판별은 1-b 의 audit-dir 존재 검사와 같은 역할이다.
|
|
218
|
+
#
|
|
219
|
+
# 🟥 스코프와 정렬, 둘 다 위 1-b 를 그대로 베끼면 틀린다 — 적대검증 2026-08-16 이 둘 다 잡았다.
|
|
220
|
+
#
|
|
221
|
+
# (1) **노드-로컬만 보면 안 된다.** `tracks/` 는 gitignored 라 노드 간에 안 따라오고, 진단은
|
|
222
|
+
# `sync-to-be.sh` 로 `$BE/$TM` 에 실린다. 프로에서 돌리고 에어에서 세션을 열면 에어엔
|
|
223
|
+
# `fh_completed_*.md` 는 있고 진단 리포트만 없어서 **「기록 0건」이라는 거짓 단언**이 난다.
|
|
224
|
+
# 그리고 이건 1-b 보다 나쁘다: 1-b 는 부재를 침묵으로 렌더해 «틀린 말을 안 하는» 쪽으로
|
|
225
|
+
# degrade 하는데, 이 블록은 부재를 **큰 소리 단언으로 승격**시켰기 때문이다. 스코프가 틀린
|
|
226
|
+
# 채로 소리를 키우면 오보 비용도 같이 커진다. 바로 위 `_fd_ready()`(:129)가 이미 같은 버그를
|
|
227
|
+
# 한 번 겪고 합집합으로 고쳤다 — **같은 형태를 쓴다**(divergent-leniency 금지).
|
|
228
|
+
#
|
|
229
|
+
# (2) **`sort | tail -1` 은 사전순이지 시간순이 아니다.** 1-b 의 글롭 `weekly_audit_*.md` 는
|
|
230
|
+
# 접두어가 고정이라 사전순≈시간순이지만, 여기 글롭은 `*harness_doctor*.md` 로 **양쪽
|
|
231
|
+
# 와일드카드**다. `reference_harness_doctor_notes.md` 가 `harness_doctor_2026-08-16.md` 를
|
|
232
|
+
# 사전순으로 이기고, 그 파일의 mtime 이 나이가 된다 → 거짓 나그 또는 거짓 침묵. **mtime
|
|
233
|
+
# 최대값으로 뽑는다.** (`ls -t` 는 이식성 때문에 안 쓴다 — 이 커밋이 짓는 린트의 P-클래스다.)
|
|
234
|
+
_hd_latest() { # stdout: 가장 최근 리포트의 mtime (없으면 빈 문자열)
|
|
235
|
+
_best=""; _bestm=0
|
|
236
|
+
for _d in "$FH/tracks/_meta" ${BE:+"$BE/$TM"}; do
|
|
237
|
+
[ -d "$_d" ] || continue
|
|
238
|
+
while IFS= read -r _f; do
|
|
239
|
+
[ -n "$_f" ] || continue
|
|
240
|
+
_m=$(_mtime "$_f")
|
|
241
|
+
[ "$_m" -gt "$_bestm" ] && { _bestm="$_m"; _best="$_f"; }
|
|
242
|
+
done < <(find "$_d" -maxdepth 1 -name '*harness_doctor*.md' -print 2>/dev/null)
|
|
243
|
+
done
|
|
244
|
+
[ -n "$_best" ] && printf '%s\t%s\n' "$_bestm" "$_best"
|
|
245
|
+
}
|
|
246
|
+
_hd_has_history() { # «굴러가는 설치인가» — 이것도 합집합으로 본다
|
|
247
|
+
for _d in "$FH/tracks/_meta" ${BE:+"$BE/$TM"}; do
|
|
248
|
+
[ -d "$_d" ] || continue
|
|
249
|
+
find "$_d" -maxdepth 1 -name 'fh_completed_*.md' -print 2>/dev/null | grep -q . && return 0
|
|
250
|
+
done
|
|
251
|
+
return 1
|
|
252
|
+
}
|
|
253
|
+
_HD_DIR="$FH/tracks/_meta"
|
|
254
|
+
if [ -d "$_HD_DIR" ] && fh_cadence_due "$_FH_HOOK_SRC"; then
|
|
255
|
+
_HD_ROW="$(_hd_latest)"
|
|
256
|
+
_LATEST_HD="${_HD_ROW#* }"; _HD_MTIME="${_HD_ROW%% *}"
|
|
257
|
+
if [ -n "$_HD_ROW" ]; then
|
|
258
|
+
_HD_AGE_D=$(( ( $(date +%s) - _HD_MTIME ) / 86400 ))
|
|
259
|
+
if [ "$_HD_AGE_D" -ge 30 ]; then
|
|
260
|
+
echo "🩺 [harness-doctor] 마지막 구조 진단이 ${_HD_AGE_D}일 전이다($(basename "$_LATEST_HD")) — 캐던스는 30일."
|
|
261
|
+
echo " → /harness-doctor (구조·드리프트·끊긴 참조). --lint 는 언어 패턴까지."
|
|
262
|
+
fi
|
|
263
|
+
# `-print | grep -q` 이지 `-quit` 이 아니다: `-quit` 은 GNU/BSD 양쪽에 있으나 버전 편차가 있고,
|
|
264
|
+
# 이 파일이 고치는 결함군에 BSD/GNU 이식성이 들어 있다(감사 🟧-3). 최적화 한 톨을 위해 그 클래스를
|
|
265
|
+
# 새로 들이지 않는다 — 여기 모집단은 디렉터리 하나이므로 얻을 것도 없다.
|
|
266
|
+
elif _hd_has_history; then
|
|
267
|
+
# 세션 이력은 있는데 진단 기록이 0건 = «한 번도 안 돌렸다». 부재를 0 으로 읽지 않는다.
|
|
268
|
+
echo "🩺 [harness-doctor] 구조 진단 기록이 **0건**이다 — 캐던스 30일이 한 번도 발화한 적 없다."
|
|
269
|
+
echo " → /harness-doctor (첫 베이스라인). 부재는 «최신»이 아니다."
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
|
|
203
273
|
# ── branch-claim 자동 기록 (2026-08-09) ────────────────────────────────────────
|
|
204
274
|
# WHY HERE, ABOVE THE COMPANION-STORE EARLY EXIT: 공유 체크아웃 사고는 Mode D 와 무관하다.
|
|
205
275
|
# companion store 가 없는 설치에서도 병렬 세션은 돌고, 그때도 `.git/HEAD` 는 트리당 하나다.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# portability_lint.sh — 셸 이식성 · 조용한 즉사 · 레포전제 픽스처 린트 (advisory)
|
|
3
|
+
#
|
|
4
|
+
# 왜 이게 있나 (weekly_audit_2026-08-16 🟧-3):
|
|
5
|
+
# 이 레포의 재발 클래스 중 BSD/GNU · zsh/bash 이식성이 윈도우 16일에 ~12건인데
|
|
6
|
+
# **전용 계기가 없었고**, 잡힌 경로는 **전부 CI ubuntu 에서 사후**였다. 즉 저자 머신
|
|
7
|
+
# (macOS)에서는 구조적으로 초록이고, 로컬 초록 / CI 적색이 2026-08-16 하루에 2회 났다.
|
|
8
|
+
# 이 린트는 그 사각을 보라고 지어졌다.
|
|
9
|
+
#
|
|
10
|
+
# 🟥 배선 상태를 헤더가 정직하게 말한다 (적대검증 2026-08-16 이 초판 헤더를 «거짓말»로 지목):
|
|
11
|
+
# 초판 헤더는 "저자 머신에서 **커밋 전에** 한 번 본다" 고 적었는데, 실제 호출부는
|
|
12
|
+
# `selfcheck.sh` 의 `--self-test` **하나뿐**이다. 즉 **계기의 계기는 도는데 계기 자체는 안 돈다.**
|
|
13
|
+
# 제자리는 pre-commit 의 staged `*.sh` 한정 advisory 인데 아직 안 붙였다(별도 커밋 · 인계됨).
|
|
14
|
+
# 그때까지 본 실행은 **손으로 돌려야 한다**: `bash scripts/portability_lint.sh`.
|
|
15
|
+
# 이 문장을 «커밋 전에 본다»로 되돌리려면 **먼저 pre-commit 에 붙여라.** 헤더가 배선보다
|
|
16
|
+
# 앞서 나가는 것이 이 커밋이 고치고 있는 바로 그 결함이다.
|
|
17
|
+
#
|
|
18
|
+
# 무엇이 아닌가:
|
|
19
|
+
# · shellcheck 대체가 아니다. shellcheck 는 「성공하는 첫 명령」 패턴(P1)을 못 잡는다 —
|
|
20
|
+
# 문법이 아니라 **플랫폼별 의미 차이**라서다. 둘은 상보적이다.
|
|
21
|
+
# · 게이트가 아니다. **advisory** 다(exit 0 유지, `--strict` 로만 1). 이유는
|
|
22
|
+
# degrade_direction_scan 과 같다 — FP 를 감수하는 사전 선별기이고, 과차단은
|
|
23
|
+
# `--no-verify` 를 근육기억으로 만들어 같은 훅의 비가역 게이트까지 무장해제시킨다.
|
|
24
|
+
#
|
|
25
|
+
# Exit: 0 = 소견 없음 또는 advisory 모드 · 1 = --strict 에서 소견 有 · 10 = 하네스 오류
|
|
26
|
+
# Usage: bash scripts/portability_lint.sh [PATHS...] [--strict] [--quiet]
|
|
27
|
+
# bash scripts/portability_lint.sh --self-test
|
|
28
|
+
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
|
|
31
|
+
STRICT=0; QUIET=0; SELFTEST=0; TARGETS=()
|
|
32
|
+
for a in "$@"; do
|
|
33
|
+
case "$a" in
|
|
34
|
+
--strict) STRICT=1 ;;
|
|
35
|
+
--quiet) QUIET=1 ;;
|
|
36
|
+
--self-test) SELFTEST=1 ;;
|
|
37
|
+
-*) echo "unknown flag: $a" >&2; exit 10 ;;
|
|
38
|
+
*) TARGETS+=("$a") ;;
|
|
39
|
+
esac
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# ── 규칙표 ────────────────────────────────────────────────────────────────────
|
|
43
|
+
# 각 규칙 = ID§정규식§한 줄 사유§처방
|
|
44
|
+
#
|
|
45
|
+
# 🟥 구분자가 `|` 가 아니라 `§` 인 이유 — 이 린트의 self-test 가 초판에서 잡은 결함이다:
|
|
46
|
+
# `|` 를 필드 구분자로 쓰면 정규식 안의 **교대(alternation) `|`** 에서 잘린다.
|
|
47
|
+
# P1 이 `…=\$\((cat` 까지만 남아 known-positive 를 놓쳤다. 계기가 자기 문법에 걸린 것이고,
|
|
48
|
+
# known-pair 가 없었으면 «소견 0건»이 그냥 초록으로 보였을 것이다.
|
|
49
|
+
# 전부 **오늘 실물에서 나온 것**이거나 이 레포의 기록된 재발이다. 추측 규칙은 넣지 않는다 —
|
|
50
|
+
# FP 가 늘면 이 린트가 무시되고, 무시되는 린트는 없는 린트보다 나쁘다(경고 하나가 0건보다
|
|
51
|
+
# 나쁠 수 있다: 0건은 «아무도 안 봤다»로 남지만 1건은 «봤고 이것만 나왔다»로 접힌다).
|
|
52
|
+
RULES=(
|
|
53
|
+
'P1§^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=\$\((cat|sed|awk|grep|head|tail|python3?|node|jq)[^)]*\)§[후보] set -e 아래 명령치환 ASSIGNMENT — 파일 부재 시 그 줄에서 즉사. set -e 판별이 파일 스코프라 확정 아님§`|| true` 를 붙여라. 2>/dev/null 도 ${VAR:-기본값} 도 이걸 못 막는다(할당문 자체가 죽으므로). [ -f ] 가드도 present-but-unreadable arm 을 못 막는다 — 2026-08-16 4방 매트릭스가 반증' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
54
|
+
'P2§stat -f §`stat -f` 가 먼저 오면 GNU 에서 **성공한다** — 파일시스템 정보를 내고 폴백이 영영 안 돈다§stat -c %Y … 2>/dev/null || stat -f %m … 2>/dev/null || echo 0 순서로. GNU-first 가 맞다' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
55
|
+
'P3§sed -i[[:space:]]+[^-.]§`sed -i` 는 BSD 가 백업 접미사를 요구한다 — GNU 폼은 macOS 에서 다음 토큰을 파일명으로 먹는다§sed -i.bak 후 삭제, 또는 sed … > tmp && mv tmp f' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
56
|
+
'P4§grep -P§`grep -P` 는 BSD/macOS 기본 grep 에 없다§grep -E 로 낮추거나 perl -ne 로' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
57
|
+
'P5§\$\{PIPESTATUS§`${PIPESTATUS[…]}` 는 zsh 에서 빈 문자열이다 — 판정이 "" 로 접힌다§bash 전용이면 shebang 확인. 더 나은 형태: 파이프를 빼고 out=$(cmd); rc=$?' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
58
|
+
'P6§readlink -f§`readlink -f` 는 구형 BSD 에 없다§cd "$(dirname "$f")" && pwd -P 조합으로' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
59
|
+
'P7§date -d §`date -d` 는 GNU 전용 — BSD 는 date -v§epoch 로 계산하거나 두 폼을 || 로 잇되 GNU-first' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
60
|
+
'P9§"(package\.json|bin/)§테스트가 **레포 고유 파일**을 픽스처로 참조한다 — 이식된 자산에서 전량 FAIL 한다§자기 픽스처를 mktemp 로 만들어 써라. 2026-08-16 PMH 이식에서 selfcheck 35 FAIL 의 원인' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_hits=0
|
|
64
|
+
_emit() { [ "$QUIET" = 1 ] || printf '%s\n' "$*"; }
|
|
65
|
+
|
|
66
|
+
lint_file() {
|
|
67
|
+
local f="$1" rule id re why fix line n
|
|
68
|
+
# 🟥 끊어진 심링크는 «소견 없음»이 아니라 **미측정**이다 (cross-family 2026-08-16).
|
|
69
|
+
if [ -L "$f" ] && [ ! -e "$f" ]; then
|
|
70
|
+
_emit " [UNMEASURED] $f — 끊어진 심링크. 읽지 못했다(초록이 아니다)."
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
# 🟥 **로케일 fail-open — cross-family 가 양쪽 로케일을 실제로 돌려서 잡았다.**
|
|
74
|
+
# UTF-8 로케일에서 대상 줄에 비-UTF8 바이트가 있으면 `grep -nE` 가 **매치하지 않고**, 그
|
|
75
|
+
# 실패가 삼켜져 최종 「소견 없음」으로 접힌다. 같은 입력이 `LC_ALL=C` 에서는 검출됐다.
|
|
76
|
+
# 즉 «무매치»와 «해석 실패»가 구분되지 않는 fail-open 이고, 이 린트가 잡으라고 만들어진
|
|
77
|
+
# 클래스 그 자체다. 이 함수의 모든 매칭을 **바이트 기반으로 고정**한다.
|
|
78
|
+
local LC_ALL=C LANG=C
|
|
79
|
+
# P1 은 «set -e 계열이 켜져 있는 파일»에서만 의미가 있다. 아니면 FP 폭발.
|
|
80
|
+
# ⚠️ 이 판별은 **파일 스코프**라 정확하지 않다 (cross-family 실측): 호출되지 않는 함수 안의
|
|
81
|
+
# `set -e` 로도 켜지고, 뒤따르는 `set +e` 로 꺼지지 않으며, 부모가 `set -e` 상태로 source 하는
|
|
82
|
+
# 파일은 **놓친다**. 그래서 P1 은 «확정 결함»이 아니라 **후보**다 — 출력 문구도 그렇게 낮춘다.
|
|
83
|
+
local has_sete=0
|
|
84
|
+
grep -qE '^[[:space:]]*set -[a-z]*e' "$f" 2>/dev/null && has_sete=1
|
|
85
|
+
for rule in "${RULES[@]}"; do
|
|
86
|
+
id="${rule%%§*}"; rest="${rule#*§}"
|
|
87
|
+
re="${rest%%§*}"; rest="${rest#*§}"
|
|
88
|
+
why="${rest%%§*}"; fix="${rest#*§}"
|
|
89
|
+
case "$id" in P1) [ "$has_sete" = 1 ] || continue ;; esac
|
|
90
|
+
while IFS= read -r line; do
|
|
91
|
+
n="${line%%:*}"; body="${line#*:}"
|
|
92
|
+
# noqa 이탈구: 알고 쓴 경우를 위한 문서화된 억제(전부 차단하면 우회를 훈련시킨다)
|
|
93
|
+
# noqa 는 **사유가 있어야** 억제한다. 초판은 문자열 존재만 봐서 사유 없이도, 심지어
|
|
94
|
+
# 문자열 리터럴 안에서도 억제됐다 — advisory 인 지금은 경미하나 `--strict` 를 게이트로
|
|
95
|
+
# 승격하는 순간 무료 우회로가 된다.
|
|
96
|
+
printf '%s' "$body" | grep -qE '#[[:space:]]*portability-noqa:[[:space:]]*[^[:space:]]' && continue
|
|
97
|
+
# 주석 전용 줄은 소견이 아니다. 이 레포는 결함을 **주석으로 길게 설명하는** 관습이 있어서
|
|
98
|
+
# (그 설명이 문제 패턴을 그대로 인용한다) 이걸 안 빼면 FP 가 구조적으로 쌓인다.
|
|
99
|
+
# 손검증 2026-08-16: 초판 50건 중 8건이 정확히 이 형태였다.
|
|
100
|
+
case "$(printf '%s' "$body" | sed 's/^[[:space:]]*//')" in '#'*) continue ;; esac
|
|
101
|
+
# P2 는 **순서**가 결함이지 `stat -f` 의 존재가 아니다. GNU-first(`stat -c … || stat -f …`)는
|
|
102
|
+
# 이 레포의 올바른 관용구이고, 초판은 그걸 12건 중 대다수로 지적했다 — 즉 규칙이 처방을
|
|
103
|
+
# 위반으로 렌더했다. 손검증 한 건이 이걸 잡았다(`_mtime()` 두 곳).
|
|
104
|
+
case "$id" in
|
|
105
|
+
P2) case "$body" in *'stat -c'*'stat -f'*) continue ;; esac ;;
|
|
106
|
+
# P7 도 같은 형태다 — `date -d … || date -v…` 는 GNU-first 폴백이라 **올바른 관용구**다.
|
|
107
|
+
# P2 에서 한 번 겪고도 새 규칙에 같은 오류를 심었다(손검증이 두 번째로 잡음).
|
|
108
|
+
P7) case "$body" in *'date -d'*'date -v'*) continue ;; esac ;;
|
|
109
|
+
esac
|
|
110
|
+
# P1 의 처방이 이미 적용된 줄은 소견이 아니다. 이 이탈구가 없으면 린트가 **자기 처방을
|
|
111
|
+
# 따른 코드를 계속 지적**하고, 그러면 사람이 린트를 끄게 된다 — self-test 가 잡았다.
|
|
112
|
+
# ⚠️ `2>/dev/null` 이나 `${VAR:-기본값}` 은 이탈 사유가 **아니다**: 둘 다 할당문의
|
|
113
|
+
# 즉사를 막지 못한다(2026-08-16 4방 매트릭스). 여기서 인정하는 건 `|| true` 뿐이다.
|
|
114
|
+
case "$id" in
|
|
115
|
+
P1) case "$body" in *'|| true'*|*'||true'*) continue ;; esac ;;
|
|
116
|
+
esac
|
|
117
|
+
_emit " [$id] $f:$n"
|
|
118
|
+
_emit " $why"
|
|
119
|
+
_emit " → $fix"
|
|
120
|
+
_hits=$((_hits+1))
|
|
121
|
+
done < <(grep -nE "$re" "$f" 2>/dev/null)
|
|
122
|
+
done
|
|
123
|
+
|
|
124
|
+
# P8 — 가드 없는 glob 루프. 규칙표에 못 넣는다: **판정이 다음 줄에 있기 때문**이다.
|
|
125
|
+
# 줄 단위 정규식은 `for f in *.sh; do` 만 보고 바로 아래의 `[ -e "$f" ] || continue` 를
|
|
126
|
+
# 볼 수 없어서, 초판 self-test 가 known-negative 에서 오탐을 냈다(hits=1, expected 0).
|
|
127
|
+
# 계기의 시야가 한 줄이면 두 줄짜리 관용구를 영영 오판한다 — 그래서 여기만 창을 넓힌다.
|
|
128
|
+
while IFS= read -r line; do
|
|
129
|
+
n="${line%%:*}"; body="${line#*:}"
|
|
130
|
+
printf '%s' "$body" | grep -qE '#[[:space:]]*portability-noqa:[[:space:]]*[^[:space:]]' && continue
|
|
131
|
+
# 주석 전용 줄 제외 — RULES 루프엔 넣고 여기엔 안 넣어서, 이 블록이 **자기 자신을 설명하는
|
|
132
|
+
# 주석**을 소견으로 냈다(손검증이 잡음). 같은 이탈구는 두 루프에 **같이** 있어야 한다.
|
|
133
|
+
case "$(printf '%s' "$body" | sed 's/^[[:space:]]*//')" in '#'*) continue ;; esac
|
|
134
|
+
# 다음 2줄 안에 가드가 있으면 관용구를 지킨 것이다
|
|
135
|
+
if sed -n "$((n+1)),$((n+2))p" "$f" 2>/dev/null \
|
|
136
|
+
| grep -qE '\[ *-[efd] |\|\| *continue|nullglob'; then continue; fi
|
|
137
|
+
# 파일 어딘가에 nullglob 이 켜져 있으면 전역적으로 해결된 것이다
|
|
138
|
+
grep -qE '^[[:space:]]*shopt -s [a-z ]*nullglob' "$f" 2>/dev/null && continue
|
|
139
|
+
_emit " [P8] $f:$n"
|
|
140
|
+
_emit " glob 이 하나도 안 맞으면 **리터럴 문자열 자체**가 루프에 들어온다"
|
|
141
|
+
_emit " → 루프 첫 줄에 \`[ -e \"\$f\" ] || continue\`, 또는 shopt -s nullglob"
|
|
142
|
+
_hits=$((_hits+1))
|
|
143
|
+
done < <(grep -nE 'for [A-Za-z_][A-Za-z0-9_]* in [^;]*\*[^;)]*; *do' "$f" 2>/dev/null)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# ── self-test (known-pair) ────────────────────────────────────────────────────
|
|
147
|
+
# 이 린트가 «무엇을 못 잡는지»까지 고정한다. 컨트롤이 없으면 초록의 이유를 모른다.
|
|
148
|
+
if [ "$SELFTEST" = 1 ]; then
|
|
149
|
+
t=$(mktemp -d) || { echo "harness-error: mktemp"; exit 10; }
|
|
150
|
+
trap 'rm -rf "$t"' EXIT
|
|
151
|
+
pass=0; failn=0
|
|
152
|
+
_ck(){ # _ck <label> <expect-hit:0|1> <file>
|
|
153
|
+
_hits=0; QUIET=1; lint_file "$3"; QUIET=0
|
|
154
|
+
if { [ "$2" = 1 ] && [ "$_hits" -gt 0 ]; } || { [ "$2" = 0 ] && [ "$_hits" -eq 0 ]; }; then
|
|
155
|
+
echo " ✅ $1"; pass=$((pass+1))
|
|
156
|
+
else echo " ❌ $1 (hits=$_hits, expected-hit=$2)"; failn=$((failn+1)); fi
|
|
157
|
+
}
|
|
158
|
+
# P1 known-positive — 오늘 `scripts/fh-gate.sh` 의 VERSION 할당이 정확히 이 형태였다.
|
|
159
|
+
# ⚠️ 행번호를 적지 않는다: 초판은 `:37` 이라 적었는데 **같은 세션의 수리가 그걸 :52 로 밀었고**,
|
|
160
|
+
# Axis 3 이 그 stale 참조를 잡았다. 이 커밋이 지적하는 결함 클래스의 자기재현이라 이름으로만 건다.
|
|
161
|
+
printf 'set -euo pipefail\nV=$(sed -n s/x/y/p package.json 2>/dev/null | head -1)\n' > "$t/p1_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
162
|
+
_ck "P1 known-positive (set -e + 명령치환 할당)" 1 "$t/p1_pos.sh"
|
|
163
|
+
# P1 known-negative — || true 가 붙으면 안 잡아야 한다
|
|
164
|
+
printf 'set -euo pipefail\nV=$(sed -n s/x/y/p f 2>/dev/null | head -1 || true)\n' > "$t/p1_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
165
|
+
_ck "P1 known-negative (|| true 있음)" 0 "$t/p1_neg.sh"
|
|
166
|
+
# P1 컨트롤 — set -e 가 없으면 애초에 해당 없음
|
|
167
|
+
printf 'V=$(cat package.json)\n' > "$t/p1_nosete.sh"
|
|
168
|
+
_ck "P1 컨트롤 (set -e 없음 → 해당없음)" 0 "$t/p1_nosete.sh"
|
|
169
|
+
# P2 stat BSD-first
|
|
170
|
+
printf 'm=$(stat -f %%m "$1" || stat -c %%Y "$1")\n' > "$t/p2_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
171
|
+
_ck "P2 known-positive (stat -f 선행)" 1 "$t/p2_pos.sh"
|
|
172
|
+
printf 'm=$(stat -c %%Y "$1" 2>/dev/null || echo 0)\n' > "$t/p2_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
173
|
+
_ck "P2 known-negative (GNU-first)" 0 "$t/p2_neg.sh"
|
|
174
|
+
# P5 PIPESTATUS
|
|
175
|
+
printf 'cmd | tail -1\nrc=${PIPESTATUS[0]}\n' > "$t/p5_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
176
|
+
_ck "P5 known-positive (PIPESTATUS)" 1 "$t/p5_pos.sh"
|
|
177
|
+
# P8 unguarded glob
|
|
178
|
+
printf 'for f in scripts/*.sh; do\n echo "$f"\ndone\n' > "$t/p8_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
179
|
+
_ck "P8 known-positive (가드 없는 glob 루프)" 1 "$t/p8_pos.sh"
|
|
180
|
+
printf 'for f in scripts/*.sh; do\n [ -e "$f" ] || continue\ndone\n' > "$t/p8_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
181
|
+
_ck "P8 known-negative (continue 가드)" 0 "$t/p8_neg.sh"
|
|
182
|
+
# noqa 이탈구가 실제로 억제하나
|
|
183
|
+
printf 'set -e\nV=$(cat package.json) # portability-noqa: 이 레포 전용 도구다\n' > "$t/noqa.sh"
|
|
184
|
+
_ck "noqa 억제 동작" 0 "$t/noqa.sh"
|
|
185
|
+
# 🟥 P3·P4·P6·P7·P9 는 초판에 레인이 **하나도 없었다** — 적대검증이 잡았다. 레인 없는 규칙은
|
|
186
|
+
# 정규식에 오타를 내도 self-test 가 `10 pass / 0 fail` 그대로라, 판별력이 미측정이다.
|
|
187
|
+
printf 'sed -i s/a/b/ f\n' > "$t/p3_pos.sh"; _ck "P3 known-positive (sed -i GNU폼)" 1 "$t/p3_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
188
|
+
printf 'sed -i.bak s/a/b/ f\n' > "$t/p3_neg.sh"; _ck "P3 known-negative (접미사 있음)" 0 "$t/p3_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
189
|
+
printf 'grep -P "\\d+" f\n' > "$t/p4_pos.sh"; _ck "P4 known-positive (grep -P)" 1 "$t/p4_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
190
|
+
printf 'grep -E "[0-9]+" f\n' > "$t/p4_neg.sh"; _ck "P4 known-negative (grep -E)" 0 "$t/p4_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
191
|
+
printf 'p=$(readlink -f "$1")\n' > "$t/p6_pos.sh"; _ck "P6 known-positive (readlink -f)" 1 "$t/p6_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
192
|
+
printf 'p=$(cd "$(dirname "$1")" && pwd -P)\n' > "$t/p6_neg.sh"; _ck "P6 known-negative (pwd -P)" 0 "$t/p6_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
193
|
+
printf 'd=$(date -d @123 +%%s)\n' > "$t/p7_pos.sh"; _ck "P7 known-positive (date -d)" 1 "$t/p7_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
194
|
+
printf 'd=$(date +%%s)\n' > "$t/p7_neg.sh"; _ck "P7 known-negative (date +%s)" 0 "$t/p7_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
195
|
+
printf 'run_review "package.json"\n' > "$t/p9_pos.sh"; _ck "P9 known-positive (레포고유 픽스처)" 1 "$t/p9_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
196
|
+
printf 'run_review "$t/fixture.json"\n' > "$t/p9_neg.sh"; _ck "P9 known-negative (자기 픽스처)" 0 "$t/p9_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
|
|
197
|
+
|
|
198
|
+
# 🟥 로케일 fail-open — cross-family 가 실측으로 잡은 것. 비-UTF8 바이트가 섞인 줄을
|
|
199
|
+
# UTF-8 로케일에서 돌려도 검출돼야 한다(lint_file 이 LC_ALL=C 로 고정하므로).
|
|
200
|
+
printf 'set -e\nV=$(cat package.json)\n\xff\n' > "$t/nonutf.sh"
|
|
201
|
+
( export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8; _ck "비-UTF8 바이트 (UTF-8 로케일에서도 검출)" 1 "$t/nonutf.sh" )
|
|
202
|
+
|
|
203
|
+
# 끊어진 심링크 — 조용한 «소견 없음» 이 아니라 UNMEASURED 로 나와야 한다
|
|
204
|
+
ln -s "$t/does_not_exist.sh" "$t/broken.sh" 2>/dev/null
|
|
205
|
+
_bo=$(QUIET=0; _hits=0; lint_file "$t/broken.sh"); case "$_bo" in
|
|
206
|
+
*UNMEASURED*) echo " ✅ 끊어진 심링크 → UNMEASURED"; pass=$((pass+1)) ;;
|
|
207
|
+
*) echo " ❌ 끊어진 심링크가 조용히 통과했다"; failn=$((failn+1)) ;;
|
|
208
|
+
esac
|
|
209
|
+
|
|
210
|
+
# 빈 파일 — 계기가 아무 데나 울리지 않는가
|
|
211
|
+
: > "$t/empty.sh"
|
|
212
|
+
_ck "빈 파일 (오탐 없음)" 0 "$t/empty.sh"
|
|
213
|
+
# 종결 verdict 한 줄 — selfcheck.sh 의 --self-test 루프가 이 문자열로 «실제로 돌았다»를 판정한다.
|
|
214
|
+
# 규약(그 루프의 주석): 이 줄은 **진짜 종결 판정**이어야 하고 한국어 테스트케이스 *제목*이면
|
|
215
|
+
# 안 된다. 제목이면 그 레인 이름 하나만 바꿔도 진짜 PASS 가 거짓 "dispatcher missing?" 으로
|
|
216
|
+
# 뒤집힌다(capability_registry_check 가 그 이유로 루프에서 빠졌다). 그래서 여기 딱 한 번,
|
|
217
|
+
# 요약 자리에서만 쓴다.
|
|
218
|
+
if [ "$failn" -eq 0 ]; then
|
|
219
|
+
echo "portability-lint 캘리브레이션 통과: $pass pass / 0 fail"
|
|
220
|
+
exit 0
|
|
221
|
+
fi
|
|
222
|
+
echo "portability-lint 캘리브레이션 실패: $pass pass / $failn fail"
|
|
223
|
+
exit 1
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# ── 본 실행 ───────────────────────────────────────────────────────────────────
|
|
227
|
+
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
|
228
|
+
while IFS= read -r f; do TARGETS+=("$f"); done < <(
|
|
229
|
+
{ git ls-files '*.sh' 'templates/.git-hooks/*' 'bin/*' 2>/dev/null \
|
|
230
|
+
|| find . \( -name '*.sh' -o -path './templates/.git-hooks/*' -o -path './bin/*' \) \
|
|
231
|
+
-not -path './.git/*' 2>/dev/null; } )
|
|
232
|
+
fi
|
|
233
|
+
[ "${#TARGETS[@]}" -gt 0 ] || { _emit "portability-lint: 대상 파일 0 — 잴 것이 없다(초록이 아니라 미측정)"; exit 0; }
|
|
234
|
+
|
|
235
|
+
_emit "── portability-lint (${#TARGETS[@]} files) ──"
|
|
236
|
+
for f in "${TARGETS[@]}"; do
|
|
237
|
+
[ -f "$f" ] || { [ -L "$f" ] && lint_file "$f"; continue; }
|
|
238
|
+
# 🟥 확장자 없는 셸 파일을 넣었으므로(templates/.git-hooks/pre-commit·pre-push, bin/fh-*)
|
|
239
|
+
# 비-셸 파일(.js 등)을 걸러야 한다. 판별은 확장자가 아니라 **shebang** 이다.
|
|
240
|
+
# 초판은 `git ls-files '*.sh'` 뿐이라 **이 레포에서 BSD/GNU 결함이 가장 비싼 두 파일**
|
|
241
|
+
# (pre-commit·pre-push)을 구조적으로 못 봤다 — 그리고 같은 커밋의 selfcheck 주석이
|
|
242
|
+
# «첫 스캔이 확장자 없는 훅을 놓쳤다»고 적어 놓고서 새 계기에 같은 스코프를 다시 심었다.
|
|
243
|
+
case "$f" in
|
|
244
|
+
*.sh) : ;;
|
|
245
|
+
*) head -1 "$f" 2>/dev/null | grep -qE '^#!.*\b(ba)?sh' || continue ;;
|
|
246
|
+
esac
|
|
247
|
+
lint_file "$f"
|
|
248
|
+
done
|
|
249
|
+
|
|
250
|
+
if [ "$_hits" -eq 0 ]; then
|
|
251
|
+
_emit " 소견 없음 (${#TARGETS[@]} files 스캔)"
|
|
252
|
+
exit 0
|
|
253
|
+
fi
|
|
254
|
+
_emit "── 소견 $_hits 건 ──"
|
|
255
|
+
_emit " advisory 다 — 이 린트만으로 막지 않는다. 알고 쓰는 줄엔 \`# portability-noqa: <사유>\`."
|
|
256
|
+
[ "$STRICT" = 1 ] && exit 1
|
|
257
|
+
exit 0
|
package/scripts/relay_channel.sh
CHANGED
|
@@ -97,7 +97,12 @@ AXES_SET="verdict_binding"
|
|
|
97
97
|
# 선언이 VIOLATION 으로 거부된다 — 2026-08-11 실측: registry_check 를 통과한 capfile 2개가
|
|
98
98
|
# relay 에서 8건 VIOLATION. 같은 스펙에 대해 두 계기가 서로 다른 스키마를 든
|
|
99
99
|
# divergent-normalizer 이고, 관대함이 갈리면 한쪽만 통과하는 입력이 생긴다.
|
|
100
|
-
|
|
100
|
+
# 🟥 **2026-08-16 재발.** 위 문단이 이 함정을 2026-08-11 사례로 적어놨는데, `summary`/`tags` 가
|
|
101
|
+
# `capability_registry_check.sh` 에만 추가되고 여기 안 들어와 **같은 파일이 같은 함정을 두 번**
|
|
102
|
+
# 밟았다 — 출하된 `.claude/capabilities/*.cap` 2개가 `✅ REGISTRABLE` 인데 relay 에서
|
|
103
|
+
# `⛔ COMPOSITION_VIOLATION` 이었다(등록되지만 **부를 수 없는** 선언). 아래 교차 레인이
|
|
104
|
+
# 그 재발을 잡는 앵커다 — 산문 경고는 이미 있었고 막지 못했다.
|
|
105
|
+
META_KEYS="id entry requires_cwd summary tags verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
|
|
101
106
|
|
|
102
107
|
_die() { printf '❌ %s\n' "$*" >&2; exit "$RC_HARNESS"; }
|
|
103
108
|
_violation() { printf '⛔ COMPOSITION_VIOLATION — %s\n' "$*" >&2; VIOLATED=1; }
|
|
@@ -378,7 +383,7 @@ _sha() { # stdin → sha256 hex (macOS/linux 양쪽)
|
|
|
378
383
|
# 반환: 전역 NODE_VERDICT / NODE_EXIT / NODE_OUT_FILE
|
|
379
384
|
# 파이프로 verdict 를 읽지 않는다(D6) — 출력은 파일로 받고 exit status 를 직접 취한다.
|
|
380
385
|
_invoke_node() {
|
|
381
|
-
local f="$1" task="$2" upstream="$3" outdir="$4" n="$5"
|
|
386
|
+
local f="$1" task="$2" upstream="$3" outdir="$4" n="$5" node_args="${6:-}"
|
|
382
387
|
local entry cwd chan enum key argv_extra rc line code name
|
|
383
388
|
entry=$(_cap_get "$f" entry)
|
|
384
389
|
cwd=$(_cap_get "$f" requires_cwd)
|
|
@@ -387,6 +392,19 @@ _invoke_node() {
|
|
|
387
392
|
key=$(_cap_get "$f" verdict_stdout_key); [ -z "$key" ] && key="FH_GATE_VERDICT"
|
|
388
393
|
|
|
389
394
|
[ -n "$entry" ] || { NODE_VERDICT="HARNESS_ERROR"; NODE_EXIT=$RC_HARNESS; return 0; }
|
|
395
|
+
# ★`requires_cwd: SELF` = «이 선언을 품은 레포». 해석은 `capability_registry_check.sh` ·
|
|
396
|
+
# `capability_effect_probe.sh` 와 **같은 규칙**(git 레포 루트)이어야 한다 — 규칙이 갈리면
|
|
397
|
+
# 두 계기가 서로 다른 트리를 재고, 그건 이 파일이 §divergent-normalizer 로 이름 붙인 결함이다.
|
|
398
|
+
# 🟥 이 값이 없으면 **tracked 선언이 구조적으로 uncallable** 하다: `SELF` 는 공개표면에
|
|
399
|
+
# 운영자 홈 절대경로를 안 싣기 위해 도입된 형태라, 하네스 자기선언은 전부 이 형태를 쓴다.
|
|
400
|
+
# 실측(2026-08-16): SELF 두 노드 → `NOT_CALLABLE … HARNESS_ERROR` · 절대경로 두 노드 → 진짜 판정.
|
|
401
|
+
if [ "$cwd" = "SELF" ]; then
|
|
402
|
+
cwd="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
|
|
403
|
+
if [ -z "$cwd" ]; then
|
|
404
|
+
printf 'ℹ️ NOT_CALLABLE: requires_cwd: SELF 인데 선언이 git 레포 안에 없다 → dispatch 폴백\n' >&2
|
|
405
|
+
NODE_VERDICT="HARNESS_ERROR"; NODE_EXIT=$RC_HARNESS; return 0
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
390
408
|
if [ -n "$cwd" ] && [ ! -d "$cwd" ]; then
|
|
391
409
|
printf 'ℹ️ NOT_CALLABLE: requires_cwd 부재(%s) → dispatch 폴백 대상\n' "$cwd" >&2
|
|
392
410
|
NODE_VERDICT="HARNESS_ERROR"; NODE_EXIT=$RC_HARNESS; return 0
|
|
@@ -406,8 +424,10 @@ _invoke_node() {
|
|
|
406
424
|
argv_extra="--upstream-verdict $upstream"
|
|
407
425
|
fi
|
|
408
426
|
|
|
427
|
+
# 순서: entry → **노드 인자** → 결박 플래그. 노드 인자를 결박 플래그 뒤에 붙이면
|
|
428
|
+
# 위치 인자를 읽는 진입점이 `--upstream-verdict` 를 자기 인자로 삼는다.
|
|
409
429
|
( [ -n "$cwd" ] && cd "$cwd"; RELAY_TASK="$task" RELAY_UPSTREAM_VERDICT="$upstream" \
|
|
410
|
-
$entry $argv_extra ) > "$NODE_OUT_FILE" 2>&1
|
|
430
|
+
$entry $node_args $argv_extra ) > "$NODE_OUT_FILE" 2>&1
|
|
411
431
|
rc=$?
|
|
412
432
|
NODE_EXIT=$rc
|
|
413
433
|
|
|
@@ -489,11 +509,54 @@ _invoke_node() {
|
|
|
489
509
|
|
|
490
510
|
# ── run ───────────────────────────────────────────────────────────────────────
|
|
491
511
|
do_run() {
|
|
492
|
-
local task="" recdir="" ack=0 n=0 upstream="" rc final=$RC_PASS
|
|
493
|
-
CAPS=()
|
|
512
|
+
local task="" recdir="" ack=0 n=0 upstream="" rc final=$RC_PASS _ci
|
|
513
|
+
CAPS=(); CAPARGS=(); CAPARGS_SET=()
|
|
494
514
|
while [ $# -gt 0 ]; do
|
|
495
515
|
case "$1" in
|
|
496
|
-
--cap) shift; CAPS+=("$1") ;;
|
|
516
|
+
--cap) shift; CAPS+=("$1"); CAPARGS+=("") ;;
|
|
517
|
+
# ── 노드별 호출 인자 채널 ────────────────────────────────────────────────
|
|
518
|
+
# 🟥 **왜 이게 필요한가(실측 2026-08-16)**: 호출 시점 인자를 요구하는 능력은
|
|
519
|
+
# relay 를 통과할 방법이 아예 없었다. pmh-dev 의 `merge-noop-check` 는
|
|
520
|
+
# «무엇을 무엇에 반입하나» 가 인자인데, 채널이 없어 **항상 ARGS→HARNESS_ERROR
|
|
521
|
+
# →BLOCKED** 로 떨어졌다. 방향은 fail-closed 로 옳았지만 **신호가 0** 이었고,
|
|
522
|
+
# 그건 정체성 ①의 «재사용이 실재하고 호출 가능한가» 를 구조적으로 못 만족시킨다.
|
|
523
|
+
# ⚠️ **결박은 «가장 최근 --cap»** 이다. 위치 인덱스로 짝지으면 순서가 어긋났을 때
|
|
524
|
+
# 조용히 다른 노드에 붙는다 — 그건 계기가 대상을 바꾸는 형태다.
|
|
525
|
+
# ⚠️ `--cap` 없이 먼저 오면 **fail-closed**: 어디 붙일지 모르는 인자를 버리지 않는다.
|
|
526
|
+
--cap-args)
|
|
527
|
+
shift
|
|
528
|
+
[ "${#CAPS[@]}" -gt 0 ] || _die "--cap-args 는 --cap 뒤에 와야 한다(어느 노드의 인자인지 결박 불가)"
|
|
529
|
+
_ci=$(( ${#CAPS[@]} - 1 ))
|
|
530
|
+
# 🟥 **중복 지정은 fail-closed**(cross-family 지목 + 재현, 2026-08-16). 초판은 같은
|
|
531
|
+
# `--cap`에 `--cap-args` 를 두 번 주면 **앞 값이 조용히 사라지고** 정상 실행됐다
|
|
532
|
+
# (`--cap A --cap-args FIRST --cap-args SECOND` → 노드가 `ARGS=[SECOND]`).
|
|
533
|
+
# 무음 폐기는 이 레포가 반복해서 이름 붙인 결함이다 — 의미가 append 인지 replace 인지
|
|
534
|
+
# **정하지 않은 채 하나를 버리느니** 막는다.
|
|
535
|
+
[ -z "${CAPARGS_SET[$_ci]:-}" ] || _die "같은 --cap 에 --cap-args 가 두 번 왔다 — 무음 폐기 대신 막는다(의미가 append 인지 replace 인지 정의되지 않았다)"
|
|
536
|
+
# 🟥 **결박 플래그와의 충돌은 fail-closed**(cross-family 지목 + 재현). 진입점은
|
|
537
|
+
# `$entry $node_args $argv_extra` 순으로 단어분해 실행되므로, `upstream_argv: yes` 를
|
|
538
|
+
# 선언한 노드에 `--cap-args --` 를 주면 그 노드의 옵션 파서가 `--` 에서 멈추고
|
|
539
|
+
# 뒤따르는 `--upstream-verdict <v>` 를 **위치 인자로 삼는다** = 인과 결박이 조용히 죽는다.
|
|
540
|
+
# 순서를 뒤집으면 위치 인자를 읽는 진입점이 깨지므로, 안전한 순서가 없다.
|
|
541
|
+
# ⇒ 두 계약을 **동시에 요구하지 않게** 한다. 결박은 `RELAY_UPSTREAM_VERDICT` env 로
|
|
542
|
+
# 항상 전달되므로 이 노드도 결박 자체를 잃지는 않는다.
|
|
543
|
+
if [ "$(_cap_get "${CAPS[$_ci]}" upstream_argv 2>/dev/null)" = "yes" ]; then
|
|
544
|
+
_die "이 노드는 upstream_argv: yes 다 — argv 결박과 --cap-args 를 같이 쓸 수 없다(둘의 인자 순서가 정의되지 않았다). env RELAY_UPSTREAM_VERDICT 로 결박을 받아라"
|
|
545
|
+
fi
|
|
546
|
+
# 셸 메타문자·제어문자 거부. ⚠️ **이건 «명령 실행 우회» 방어가 아니다** — 진입점은
|
|
547
|
+
# `eval` 이 아니라 단어분해로 돌고 전역 `set -f` 라 `;` `|` `$()` 글롭 틸드 중괄호는
|
|
548
|
+
# 연산자/확장으로 재해석되지 않는다. 이 가드가 막는 것은 **인자 경계의 조용한 변형**이다.
|
|
549
|
+
# 초판 주석은 "프로브와 같은 규칙" 이라고 적었는데 **과잉주장이었다**: 개행이 통과해
|
|
550
|
+
# 한 인자가 두 인자로 쪼개졌다(`$'GO\nSECOND'` → 노드가 `ARGC=2`). 개행/탭/제어문자를
|
|
551
|
+
# 추가로 막고, 문구를 사실에 맞춘다.
|
|
552
|
+
case "$1" in
|
|
553
|
+
*[\;\|\&\`\$\(\)\<\>]*|*'..'/*) _die "--cap-args 에 셸 메타문자/상위경로: $1" ;;
|
|
554
|
+
esac
|
|
555
|
+
case "$1" in
|
|
556
|
+
*[$'\n\t\r\v\f']*) _die "--cap-args 에 개행/탭/제어문자 — 인자 경계가 조용히 바뀐다" ;;
|
|
557
|
+
esac
|
|
558
|
+
CAPARGS[$_ci]="$1"; CAPARGS_SET[$_ci]=1
|
|
559
|
+
;;
|
|
497
560
|
--task) shift; task="$1" ;;
|
|
498
561
|
--record) shift; recdir="$1" ;;
|
|
499
562
|
--ack) ack=1 ;;
|
|
@@ -539,7 +602,7 @@ do_run() {
|
|
|
539
602
|
|
|
540
603
|
for f in "${CAPS[@]}"; do
|
|
541
604
|
n=$((n+1))
|
|
542
|
-
_invoke_node "$f" "$task" "$upstream" "$recdir" "$n"
|
|
605
|
+
_invoke_node "$f" "$task" "$upstream" "$recdir" "$n" "${CAPARGS[$((n-1))]:-}"
|
|
543
606
|
printf 'FH_NODE%d_ID: %s\n' "$n" "$(_cap_get "$f" id)"
|
|
544
607
|
printf 'FH_NODE%d_VERDICT: %s\n' "$n" "$NODE_VERDICT"
|
|
545
608
|
printf 'FH_NODE%d_EXIT: %s\n' "$n" "$NODE_EXIT"
|