@chrono-meta/fh-gate 2.0.0 → 2.1.0
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/rules/fh_4axis_gate.md +77 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +25 -3
- package/CHEATSHEET.md +17 -1
- package/CLAUDE.md +241 -5
- 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/capability_composition_contract.md +68 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +122 -5
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +115 -3
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +83 -1
- package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +255 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +234 -11
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +128 -0
- package/knowledge/shared/rules/sister_asset_protocol.md +12 -0
- package/package.json +12 -3
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/agents/quench-challenger.md +6 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +60 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +14 -2
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +13 -0
- package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +34 -1
- 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 +271 -20
- package/scripts/chamber_run.sh +24 -3
- package/scripts/chamber_witness.sh +25 -0
- package/scripts/cluster_capability_scan.sh +629 -0
- package/scripts/fh-gate.sh +16 -1
- package/scripts/fh_session_load.sh +70 -0
- package/scripts/package_coverage_check.sh +10 -0
- package/scripts/portability_lint.sh +257 -0
- package/scripts/prepublish_scope_note.sh +139 -0
- package/scripts/relay_channel.sh +70 -7
- package/scripts/selfcheck.sh +167 -5
- package/scripts/test_adapter_lanes.sh +296 -0
- package/scripts/test_capability_entrypoint_shipping.sh +8 -1
- package/scripts/test_fh_gate_regressions.sh +21 -9
- package/scripts/test_marker_axes_run_lanes.sh +87 -3
- package/scripts/test_marker_crossfamily_lanes.sh +31 -1
- package/scripts/test_relay_channel_lanes.sh +76 -0
- package/templates/.git-hooks/pre-commit +239 -12
- package/templates/subagent-tally-hook.json +2 -2
|
@@ -51,7 +51,10 @@ set -f # noglob — 선언 파일의 값은 데이터지 파일 패턴이 아
|
|
|
51
51
|
|
|
52
52
|
RC_OK=0; RC_REJECT=1; RC_HARNESS=10
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# `summary`/`tags` 는 **추천 전용**(2026-08-16, cluster-wizard). 판정에는 안 쓰이고
|
|
55
|
+
# `cluster_capability_scan.sh recommend` 의 어휘 매칭에만 쓰인다. 닫힌 목록에 넣는 이유는
|
|
56
|
+
# 이 목록의 목적이 «오타 축 무음 드롭 방지» 이기 때문이다 — 안 넣으면 정당한 키가 SCHEMA 로 막힌다.
|
|
57
|
+
CLOSED_KEYS="id entry requires_cwd summary tags 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 calibration_positive_stdin calibration_negative_stdin"
|
|
55
58
|
|
|
56
59
|
# 「안 돌았다」를 뜻하는 이름들 — 추가조항(§ⓑ.4 B1)이 요구하는 구분항
|
|
57
60
|
DIDNOTRUN_NAMES="DID_NOT_RUN DIDNOTRUN NOT_RUN NO_TARGET SKIPPED UNMEASURED NOT_CONFIGURED HARNESS_ERROR"
|
|
@@ -67,7 +70,13 @@ _parse() {
|
|
|
67
70
|
CAP_id=""; CAP_entry=""; CAP_requires_cwd=""; CAP_verdict_channel=""
|
|
68
71
|
CAP_verdict_enum=""; CAP_verdict_stdout_key=""; CAP_judge=""; CAP_writes=""
|
|
69
72
|
CAP_cal_pos_args=""; CAP_cal_pos_expect=""; CAP_cal_neg_args=""; CAP_cal_neg_expect=""
|
|
70
|
-
|
|
73
|
+
# 다중 capfile 실행에서 앞 파일의 선언이 뒤 파일로 새지 않게 한다 — 초기화 누락은
|
|
74
|
+
# "뒤 파일이 선언하지 않은 stdin 으로 돌았다" 를 만들고, 그건 조용히 통과한다.
|
|
75
|
+
CAP_cal_pos_stdin=""; CAP_cal_neg_stdin=""
|
|
76
|
+
UNKNOWN_KEYS=""; CAP_requires_cwd_was_self=0
|
|
77
|
+
# 「선언했는데 값이 비었다」와 「선언 자체가 없다」는 다른 사실이다 — 관측 범위 블록이
|
|
78
|
+
# 둘을 구분해 적으려면 파서가 그 구분을 보존해야 한다(빈 문자열 하나로는 못 나눈다).
|
|
79
|
+
CAP_cal_pos_args_seen=0; CAP_cal_neg_args_seen=0
|
|
71
80
|
local line key val
|
|
72
81
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
73
82
|
case "$line" in ''|'#'*) continue ;; esac
|
|
@@ -84,9 +93,11 @@ _parse() {
|
|
|
84
93
|
requires_cwd) CAP_requires_cwd="$val" ;; verdict_channel) CAP_verdict_channel="$val" ;;
|
|
85
94
|
verdict_enum) CAP_verdict_enum="$val" ;; verdict_stdout_key) CAP_verdict_stdout_key="$val" ;;
|
|
86
95
|
judge) CAP_judge="$val" ;; writes) CAP_writes="$val" ;;
|
|
87
|
-
calibration_positive_args) CAP_cal_pos_args="$val" ;;
|
|
96
|
+
calibration_positive_args) CAP_cal_pos_args="$val"; CAP_cal_pos_args_seen=1 ;;
|
|
88
97
|
calibration_positive_expect) CAP_cal_pos_expect="$val" ;;
|
|
89
|
-
calibration_negative_args) CAP_cal_neg_args="$val" ;;
|
|
98
|
+
calibration_negative_args) CAP_cal_neg_args="$val"; CAP_cal_neg_args_seen=1 ;;
|
|
99
|
+
calibration_positive_stdin) CAP_cal_pos_stdin="$val" ;;
|
|
100
|
+
calibration_negative_stdin) CAP_cal_neg_stdin="$val" ;;
|
|
90
101
|
calibration_negative_expect) CAP_cal_neg_expect="$val" ;;
|
|
91
102
|
esac
|
|
92
103
|
done < "$1"
|
|
@@ -113,23 +124,129 @@ _validate_arm_args() { # $1=args → 셸 메타문자/상위경로 탈출을
|
|
|
113
124
|
return 0
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
|
|
127
|
+
# stdin 파일 검증 — args 와 같은 규율(메타문자·상위경로 탈출 거부) + 실재 확인.
|
|
128
|
+
_validate_arm_stdin() { # $1=선언된 경로(빈 값 허용)
|
|
129
|
+
[ -n "$1" ] || return 0
|
|
130
|
+
case "$1" in
|
|
131
|
+
*'|'*|*';'*|*'&'*|*'>'*|*'<'*|*'`'*|*'$('*|*$'\n'*)
|
|
132
|
+
_fail "M4" "캘리브레이션 stdin 경로에 셸 메타문자가 있다: $1"; return 1 ;;
|
|
133
|
+
*'../'*|/*)
|
|
134
|
+
_fail "M4" "캘리브레이션 stdin 경로가 레포 밖을 가리킨다(상대경로만 허용): $1"; return 1 ;;
|
|
135
|
+
esac
|
|
136
|
+
[ -f "$CAP_requires_cwd/$1" ] || {
|
|
137
|
+
_fail "M4" "캘리브레이션 stdin 파일이 없다: $1 (선언은 실재하는 픽스처를 가리켜야 한다)"; return 1; }
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_run_arm() { # $1=extra args · $2=stdin 파일(선택) → ARM_RC / ARM_NAME. 파이프로 읽지 않는다(PIPE-VERDICT).
|
|
142
|
+
# ── STDIN 은 항상 정의된다. 상속하지 않는다. ────────────────────────────────
|
|
143
|
+
# 실측 2026-08-16 (자기선언 캠페인, the-bible arm): 이 함수는 `> /dev/null 2>&1` 로
|
|
144
|
+
# stdout/stderr 만 막고 **stdin 은 검사기의 것을 물려받았다.** 결과가 두 가지로 나빴다.
|
|
145
|
+
# ⓐ **비결정**: 같은 선언이 터미널·파이프·CI 어디서 돌리느냐에 따라 arm 의 입력이 달라진다.
|
|
146
|
+
# 판정 계기 안의 비결정은 그 자체로 결함이다 — 초록의 이유가 실행 환경에 달리게 된다.
|
|
147
|
+
# ⓑ **표현 불가**: 대상을 stdin 으로 받는 진입점(JSON 브리지 등)은 양·음 arm 이 **둘 다 빈
|
|
148
|
+
# 입력**으로 돌아 같은 코드를 내고, 판별 실패로 REJECT 된다. 능력이 없어서가 아니라
|
|
149
|
+
# **스키마에 채널이 없어서** 막힌 것이다. 실제로 4방향으로 갈리는 게이트가 그렇게 막혔다.
|
|
150
|
+
# ⇒ 선언된 파일이 있으면 그걸 먹이고, 없으면 **명시적으로 /dev/null** 을 먹인다. 후자가
|
|
151
|
+
# 중요하다 — "선언 안 했으니 상속"이 아니라 "선언 안 했으면 빈 입력"이라고 **정해야** ⓐ 가 닫힌다.
|
|
152
|
+
# 🟥 `set --` 는 위치 인자를 **덮어쓴다** — 그 뒤의 `$2` 는 stdin 파일이 아니라 entry 의 두 번째
|
|
153
|
+
# 토큰이다. 초판이 그걸 밟았고, 그 결과 두 arm 이 **진입점 스크립트 자신을 stdin 으로 먹었다.**
|
|
154
|
+
# 발견 경위가 이 수정의 요점이다: 그때 **음성 arm 만 빨개졌고 양성 arm 은 통과했다** — 스크립트
|
|
155
|
+
# 소스에 마침 양성 픽스처와 같은 토큰이 들어 있어서다. 즉 **둘 다 틀린 입력을 먹었는데 하나가
|
|
156
|
+
# 우연히 기대값과 맞았다.** 한쪽만 보고 있었으면 «양성은 되는데 음성이 이상하다»로 오진했다.
|
|
157
|
+
# ⇒ set -- 앞에서 이름 있는 변수로 잡는다.
|
|
158
|
+
local _stdin="${2:-}"
|
|
117
159
|
( cd "$CAP_requires_cwd" 2>/dev/null || exit 127
|
|
118
160
|
# shellcheck disable=SC2086 # argv 토큰 분리는 의도 (noglob 로 확장은 막혀 있다)
|
|
119
|
-
set -- $CAP_entry $1
|
|
161
|
+
set -- $CAP_entry $1
|
|
162
|
+
if [ -n "$_stdin" ]; then "$@" < "$_stdin"; else "$@" < /dev/null; fi
|
|
163
|
+
) > /dev/null 2>&1
|
|
120
164
|
ARM_RC=$?
|
|
121
165
|
ARM_NAME="$(_enum_name_of "$ARM_RC" "$CAP_verdict_enum")"
|
|
122
166
|
}
|
|
123
167
|
|
|
168
|
+
# ── 관측 범위 블록 (2026-08-16) — «판정» 이 아니라 «채널» ────────────────────────
|
|
169
|
+
#
|
|
170
|
+
# 🟥 이 블록은 **exit code 에 아무 영향을 주지 않는다.** `_fail` 을 부르지 않고, 어떤 축도
|
|
171
|
+
# 새로 판정하지 않는다. 순수 정보 추가다 — 이 레포의 규율("기계는 비가역 경계와 채널에만,
|
|
172
|
+
# 판단은 안 짓는다")대로, 여기서 하는 일은 **무엇을 관측했고 무엇을 안 했는지 적는 것**뿐이다.
|
|
173
|
+
#
|
|
174
|
+
# 왜 필요한가 — 2026-08-16 실측 두 건이 같은 얼굴을 하고 있었다:
|
|
175
|
+
# ① **M4 는 enum↔구현 일치를 구조적으로 안 본다.** 같은 stale 선언이 캘리브레이션 쌍에
|
|
176
|
+
# 따라 판정이 갈린다 — 쌍이 우연히 «선언 밖 exit» 을 밟으면 REJECTED, 구 enum 안에서만
|
|
177
|
+
# 갈리면 M1–M6 전부 통과. 즉 M4 는 «자기가 부른 두 번의 실행에서 본 것» 만 판정한다.
|
|
178
|
+
# ② **M6 도 선언된 arm 만 관측한다.** `pmh-dev:merge-noop-check` 는 분기 입력에서 git 객체를
|
|
179
|
+
# 쓰지만(격리 클론 실측: `.git/objects` 2145 → 2146), 선언된 두 arm 이 그 경로를 안 지나므로
|
|
180
|
+
# `writes: read-only` 로 **거짓 선언해도 통과한다**(실측 확인).
|
|
181
|
+
#
|
|
182
|
+
# 둘 다 «판정이 틀렸다» 가 아니라 **«판정의 관측 범위가 출력에 안 적혀 있다»** 이다. 그래서
|
|
183
|
+
# 고치는 방향은 새 판정 축이 아니라 이 블록이다: 읽는 사람이 초록을 «선언 전체가 참이다» 로
|
|
184
|
+
# 읽지 않게, 이 실행이 실제로 밟은 곳을 이름으로 적는다.
|
|
185
|
+
# ★ 「없음」도 **반드시 찍는다.** 침묵은 «관측되지 않은 값이 없다» 와 «계산하지 않았다» 를
|
|
186
|
+
# 구분하지 못한다 — 미측정을 0 으로 렌더하지 않는다([[feedback_not_found_is_not_zero_family]]).
|
|
187
|
+
_observation_scope() {
|
|
188
|
+
printf ' ── 관측 범위 (이 판정이 «무엇을 안 봤는지») ──\n'
|
|
189
|
+
printf ' M4/M6 는 **선언된 캘리브레이션 arm 의 실행에서 본 것만** 판정한다.\n'
|
|
190
|
+
|
|
191
|
+
local _pa _na
|
|
192
|
+
if [ "${CAP_cal_pos_args_seen:-0}" = "1" ]; then
|
|
193
|
+
[ -n "$CAP_cal_pos_args" ] && _pa="$CAP_cal_pos_args" || _pa='<빈 값 — 인자 없이 실행>'
|
|
194
|
+
else _pa='<미선언>'; fi
|
|
195
|
+
if [ "${CAP_cal_neg_args_seen:-0}" = "1" ]; then
|
|
196
|
+
[ -n "$CAP_cal_neg_args" ] && _na="$CAP_cal_neg_args" || _na='<빈 값 — 인자 없이 실행>'
|
|
197
|
+
else _na='<미선언>'; fi
|
|
198
|
+
printf ' 실행된 arm: 양성 «%s» / 음성 «%s»\n' "$_pa" "$_na"
|
|
199
|
+
|
|
200
|
+
if [ "${OBS_STATE:-none}" = "run" ]; then
|
|
201
|
+
printf ' 관측된 exit: 양성 rc=%s(%s)%s / 음성 rc=%s(%s)\n' \
|
|
202
|
+
"$OBS_POS_RC" "${OBS_POS_NAME:-<enum 밖>}" \
|
|
203
|
+
"$( [ "$OBS_POS_RC" = "$OBS_POS_RC2" ] && printf ' ×2회 동일' || printf ' ×2회째 rc=%s(갈림)' "$OBS_POS_RC2" )" \
|
|
204
|
+
"$OBS_NEG_RC" "${OBS_NEG_NAME:-<enum 밖>}"
|
|
205
|
+
else
|
|
206
|
+
printf ' 관측된 exit: 없음 — arm 을 실행하지 않았다(%s)\n' "${OBS_WHY:-사유 미기록}"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# 미관측 enum = 선언 enum − 이 실행에서 실제로 나온 rc. **계산해서** 적는다.
|
|
210
|
+
if [ -z "$CAP_verdict_enum" ]; then
|
|
211
|
+
printf ' ⚠️ 선언된 enum 이 없어 「관측되지 않은 값」을 계산할 수 없다(미측정이지 0 이 아니다 — M2 가 이미 실패로 기록했다)\n'
|
|
212
|
+
else
|
|
213
|
+
local _pair _code _unobs="" _seenrc=""
|
|
214
|
+
[ "${OBS_STATE:-none}" = "run" ] && _seenrc=" $OBS_POS_RC $OBS_POS_RC2 $OBS_NEG_RC "
|
|
215
|
+
for _pair in $CAP_verdict_enum; do
|
|
216
|
+
_code="${_pair%%=*}"
|
|
217
|
+
case "$_seenrc" in *" $_code "*) ;; *) _unobs="$_unobs $_pair" ;; esac
|
|
218
|
+
done
|
|
219
|
+
if [ -n "$_unobs" ]; then
|
|
220
|
+
printf ' ⚠️ 선언된 enum 중 이 실행에서 **관측되지 않은** 값:%s\n' "$_unobs"
|
|
221
|
+
else
|
|
222
|
+
printf ' · 선언된 enum 중 이 실행에서 관측되지 않은 값: 없음 (선언된 전 값이 이 실행에서 관측됐다)\n'
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
printf ' ⚠️ 이 실행이 지나지 않은 코드 경로의 부작용(M6 의 `writes:` 축 포함)은 관측 대상이 아니다.\n'
|
|
226
|
+
}
|
|
227
|
+
|
|
124
228
|
_check_one() {
|
|
125
229
|
local f="$1"
|
|
126
230
|
[ -r "$f" ] || _die "capfile 도달 불가: $f"
|
|
127
231
|
_parse "$f"
|
|
128
232
|
FILE_FAILED=0
|
|
233
|
+
# 관측 원장 — 「안 돌았다」가 디폴트다. 실행한 arm 만 이 값들을 채운다.
|
|
234
|
+
OBS_STATE=none; OBS_WHY=""; OBS_POS_RC=""; OBS_POS_RC2=""; OBS_NEG_RC=""
|
|
235
|
+
OBS_POS_NAME=""; OBS_NEG_NAME=""
|
|
129
236
|
printf '\n── %s (%s)\n' "${CAP_id:-<id 미선언>}" "$f"
|
|
130
237
|
|
|
131
238
|
[ -n "$UNKNOWN_KEYS" ] && _fail "SCHEMA" "닫힌 키 목록 밖:$UNKNOWN_KEYS (오타 축 무음드롭 방지 — 무시하지 않는다)"
|
|
132
239
|
|
|
240
|
+
# ── `requires_cwd: SELF` 를 **M1 보다 먼저** 푼다 (2026-08-16 순서 수리) ────
|
|
241
|
+
# 초판은 M5 에서 풀었는데, M1 이 그보다 먼저 돌면서 **상대 entry 를 현재 cwd 기준으로** 찾았다.
|
|
242
|
+
# FH 로컬 등록부 시절엔 entry 가 늘 절대경로라 안 터졌다 — 하네스 자기 선언(상대 entry +
|
|
243
|
+
# SELF)이 그 잠재 가정을 처음 실행에 노출시킨 것이다([[feedback_wiring_surfaces_hidden_failures]]).
|
|
244
|
+
# 해석 실패는 여기서 조용히 넘기고 M5 가 판정한다(한 결함을 두 축에서 두 번 세지 않는다).
|
|
245
|
+
if [ "$CAP_requires_cwd" = "SELF" ]; then
|
|
246
|
+
local _sr; _sr="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
|
|
247
|
+
[ -n "$_sr" ] && [ -d "$_sr" ] && CAP_requires_cwd="$_sr" && CAP_requires_cwd_was_self=1
|
|
248
|
+
fi
|
|
249
|
+
|
|
133
250
|
# ── M1 실행 가능한 진입점 ──────────────────────────────────────────────────
|
|
134
251
|
local first second
|
|
135
252
|
first="$(printf '%s' "$CAP_entry" | awk '{print $1}')"
|
|
@@ -139,8 +256,15 @@ _check_one() {
|
|
|
139
256
|
_fail "M1" "entry 가 argv 가 아니라 셸 문자열이다(파이프/리다이렉트/치환 포함) — 인젝션 표면" ;;
|
|
140
257
|
'') _fail "M1" "entry 미선언" ;;
|
|
141
258
|
*)
|
|
259
|
+
# 상대 경로는 **선언된 cwd 기준**으로도 본다. 선언은 자기 레포를 기준으로 쓰는 것이
|
|
260
|
+
# 자연스럽고(그래야 이식된다), 검사기가 자기 cwd 로만 보면 정당한 선언을 못 읽는다.
|
|
261
|
+
local _second_abs="$second"
|
|
262
|
+
case "$second" in
|
|
263
|
+
/*) ;;
|
|
264
|
+
*) [ -n "$CAP_requires_cwd" ] && [ -r "$CAP_requires_cwd/$second" ] && _second_abs="$CAP_requires_cwd/$second" ;;
|
|
265
|
+
esac
|
|
142
266
|
if [ -x "$first" ] 2>/dev/null; then _ok "M1" "실행 가능: $first"
|
|
143
|
-
elif command -v "$first" >/dev/null 2>&1 && [ -r "$
|
|
267
|
+
elif command -v "$first" >/dev/null 2>&1 && [ -r "$_second_abs" ]; then
|
|
144
268
|
_ok "M1" "선언된 인터프리터($first) + 읽을 수 있는 스크립트: $second"
|
|
145
269
|
else _fail "M1" "entry 를 셸이 모델 없이 실행할 수 없다: $CAP_entry"; fi ;;
|
|
146
270
|
esac
|
|
@@ -170,11 +294,35 @@ _check_one() {
|
|
|
170
294
|
# ── M3 모델 독립성 (선언 검사 + 명백한 모순만) ─────────────────────────────
|
|
171
295
|
case "$CAP_judge" in
|
|
172
296
|
mechanical)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
297
|
+
# 토큰의 BASENAME 으로 본다 — 문자열 전체 부분매칭이 아니다.
|
|
298
|
+
#
|
|
299
|
+
# 실측 2026-08-16 (자기선언 캠페인, qasp-dev arm 이 지목): 이 검사는 `*claude*` 처럼
|
|
300
|
+
# entry **문자열 전체**에 부분매칭했다. 그래서 진입점이 모델을 전혀 안 부르는데도
|
|
301
|
+
# **경로에 그 단어가 있다는 이유만으로** REJECT 났다 — 실제 사례는 스크래치패드 경로
|
|
302
|
+
# `/private/tmp/claude-501/...` 였고, 경로만 바꾸니 같은 선언이 PASS 했다.
|
|
303
|
+
# 구조적으로 더 나쁜 경우가 둘 있다: ⓐ 진입점이 `.claude/` 아래 사는 하네스는 **전부**
|
|
304
|
+
# 막힌다(FH 자기 선언이 `.claude/capabilities/` 에 사는 걸 생각하면 남 얘기가 아니다)
|
|
305
|
+
# ⓑ `scripts/claude_md_lint.sh` 처럼 이름에 그 단어가 든 순수 기계 스크립트도 막힌다.
|
|
306
|
+
#
|
|
307
|
+
# 과차단은 «안전한 방향»이 아니다 — 정본이 명시하듯 override 를 습관화시켜 같은 훅의
|
|
308
|
+
# 다른 게이트까지 무장해제시킨다. 그리고 이 오탐은 **무음**이었다: 거부 사유가
|
|
309
|
+
# "모델 CLI 를 부른다" 로 찍히므로, 읽는 사람은 자기 진입점을 의심하지 경로를 의심하지 않는다.
|
|
310
|
+
#
|
|
311
|
+
# 이 검사가 실제로 답할 수 있는 질문은 «entry 줄이 모델 CLI 를 **직접 이름으로** 부르는가»
|
|
312
|
+
# 뿐이다. 래퍼 안에서 부르는 건 선언 층에서 원래 못 본다(M6 효과 프로브의 몫).
|
|
313
|
+
# 그러니 각 토큰의 basename 을 보고, 확장자를 떼고, **정확히 그 이름일 때만** 막는다.
|
|
314
|
+
_m3_bad=""
|
|
315
|
+
for _tok in $CAP_entry; do
|
|
316
|
+
_base="${_tok##*/}"; _base="${_base%.sh}"; _base="${_base%.js}"; _base="${_base%.py}"
|
|
317
|
+
case "$_base" in
|
|
318
|
+
claude|codex|gemini|copilot|ollama|llm) _m3_bad="$_base" ;;
|
|
319
|
+
esac
|
|
320
|
+
done
|
|
321
|
+
if [ -n "$_m3_bad" ]; then
|
|
322
|
+
_fail "M3" "judge: mechanical 선언인데 entry 가 모델 CLI 를 직접 부른다: $_m3_bad ($CAP_entry)"
|
|
323
|
+
else
|
|
324
|
+
_ok "M3" "judge: mechanical (entry 토큰에 모델 CLI 없음 — basename 기준)"
|
|
325
|
+
fi ;;
|
|
178
326
|
model) _ok "M3" "judge: model — 선언됨(합법). 조합에서 이 PASS 는 NON_CLEARING 이다" ;;
|
|
179
327
|
'') _fail "M3" "judge 축 미선언 — 모델 개입 여부가 불명이면 조합이 계산될 수 없다" ;;
|
|
180
328
|
*) _fail "M3" "judge 값이 {mechanical|model} 밖: '$CAP_judge'" ;;
|
|
@@ -182,25 +330,65 @@ _check_one() {
|
|
|
182
330
|
|
|
183
331
|
# ── M5 선언된 cwd (M4 를 그 자리에서 돌리므로 먼저) ────────────────────────
|
|
184
332
|
case "$CAP_requires_cwd" in
|
|
185
|
-
/*)
|
|
186
|
-
|
|
333
|
+
/*)
|
|
334
|
+
# `SELF` 는 위(M1 앞)에서 이미 레포 루트로 풀렸다. 그 사실을 판정문에 남긴다 —
|
|
335
|
+
# 안 남기면 독자가 선언 원문(`SELF`)과 검사 결과(절대경로)를 연결하지 못한다.
|
|
336
|
+
if [ -d "$CAP_requires_cwd" ]; then
|
|
337
|
+
if [ "${CAP_requires_cwd_was_self:-0}" = "1" ]; then
|
|
338
|
+
_ok "M5" "requires_cwd: SELF → 선언을 품은 레포 루트: $CAP_requires_cwd"
|
|
339
|
+
else
|
|
340
|
+
_ok "M5" "requires_cwd 실재: $CAP_requires_cwd"
|
|
341
|
+
fi
|
|
342
|
+
else
|
|
343
|
+
_fail "M5" "requires_cwd 가 절대경로지만 존재하지 않는다: $CAP_requires_cwd"
|
|
344
|
+
fi ;;
|
|
345
|
+
SELF)
|
|
346
|
+
# ★`SELF` = «이 선언을 품은 레포» (2026-08-16 신설, 정체성 ①-(a) cluster-wizard).
|
|
347
|
+
#
|
|
348
|
+
# 왜 필요했나: 이 스키마는 원래 **FH 로컬 등록부**(gitignored)를 위한 것이라 절대경로가
|
|
349
|
+
# 문제없었다. 그런데 (c) 를 닫으려면 선언이 **그 하네스 자신의 레포에 tracked 로** 살아야
|
|
350
|
+
# 하고, 그 순간 절대경로는 두 가지로 깨진다:
|
|
351
|
+
# ① 이식 불가 — 다른 머신·다른 체크아웃에서 그 경로는 없다
|
|
352
|
+
# ② **공개표면 위반** — 운영자 홈 경로가 tracked 파일에 실린다
|
|
353
|
+
# 그래서 «자기 자신» 을 가리키는 이식 가능한 표현이 필요했다.
|
|
354
|
+
#
|
|
355
|
+
# 해석은 **위치 가정이 아니라 git 레포 루트**다. `../../..` 같은 상대 오프셋으로 풀면
|
|
356
|
+
# `.claude/capabilities/` 배치를 하드코딩하게 되고, 배치가 바뀌는 순간 조용히 엉뚱한
|
|
357
|
+
# 디렉토리를 가리킨다. 레포 루트는 그 파일이 어디 놓이든 같은 답을 준다.
|
|
358
|
+
local _self_root
|
|
359
|
+
_self_root="$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null)"
|
|
360
|
+
if [ -n "$_self_root" ] && [ -d "$_self_root" ]; then
|
|
361
|
+
CAP_requires_cwd="$_self_root"
|
|
362
|
+
_ok "M5" "requires_cwd: SELF → 선언을 품은 레포 루트로 해석: $_self_root"
|
|
363
|
+
else
|
|
364
|
+
# 🟥 fail-closed. git 레포 밖의 선언은 «자기» 가 무엇인지 정의되지 않는다 —
|
|
365
|
+
# 추측해서 cwd 를 정하면 M4 가 **엉뚱한 트리에서** 진입점을 돌린다.
|
|
366
|
+
_fail "M5" "requires_cwd: SELF 인데 이 선언이 git 레포 안에 없다 — «자기» 를 해석할 수 없다"
|
|
367
|
+
fi ;;
|
|
187
368
|
'') _fail "M5" "requires_cwd 미선언 — 콕핏이 어디서 부를지 알 수 없다" ;;
|
|
188
|
-
*) _fail "M5" "requires_cwd 가
|
|
369
|
+
*) _fail "M5" "requires_cwd 가 절대경로도 SELF 도 아니다: $CAP_requires_cwd" ;;
|
|
189
370
|
esac
|
|
190
371
|
|
|
191
372
|
# ── M4 캘리브레이션 쌍 — 선언 + **실행** ──────────────────────────────────
|
|
192
373
|
if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
|
|
193
374
|
_fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
|
|
375
|
+
OBS_WHY="캘리브레이션 쌍 미선언"
|
|
194
376
|
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
|
|
195
377
|
printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
196
|
-
|
|
378
|
+
OBS_WHY="앞선 축 실패로 M4 생략"
|
|
379
|
+
elif ! _validate_arm_args "$CAP_cal_pos_args" || ! _validate_arm_args "$CAP_cal_neg_args" \
|
|
380
|
+
|| ! _validate_arm_stdin "${CAP_cal_pos_stdin:-}" || ! _validate_arm_stdin "${CAP_cal_neg_stdin:-}"; then
|
|
197
381
|
printf ' ⏭ M4 — args 검문 실패로 arm 을 실행하지 않았다(SKIPPED, PASS 아님)\n'
|
|
382
|
+
OBS_WHY="args 검문 실패"
|
|
198
383
|
else
|
|
199
|
-
local pos_name neg_name pos_rc neg_rc pos_name2
|
|
200
|
-
_run_arm "$CAP_cal_pos_args"; pos_rc="$ARM_RC"; pos_name="$ARM_NAME"
|
|
384
|
+
local pos_name neg_name pos_rc neg_rc pos_name2 pos_rc2
|
|
385
|
+
_run_arm "$CAP_cal_pos_args" "${CAP_cal_pos_stdin:-}"; pos_rc="$ARM_RC"; pos_name="$ARM_NAME"
|
|
201
386
|
[ "$pos_rc" = "127" ] && _fail "M4" "requires_cwd 로 진입 실패 — arm 을 돌릴 수 없다"
|
|
202
|
-
_run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME"
|
|
203
|
-
_run_arm "$CAP_cal_neg_args"; neg_rc="$ARM_RC"; neg_name="$ARM_NAME"
|
|
387
|
+
_run_arm "$CAP_cal_pos_args" "${CAP_cal_pos_stdin:-}"; pos_name2="$ARM_NAME"; pos_rc2="$ARM_RC" # reps=2 (M3 부분 방어)
|
|
388
|
+
_run_arm "$CAP_cal_neg_args" "${CAP_cal_neg_stdin:-}"; neg_rc="$ARM_RC"; neg_name="$ARM_NAME"
|
|
389
|
+
# 관측 원장에 남긴다 — 판정과 무관하게, 이 실행이 **실제로 밟은 exit** 만 적는다.
|
|
390
|
+
OBS_STATE=run; OBS_POS_RC="$pos_rc"; OBS_POS_RC2="$pos_rc2"; OBS_NEG_RC="$neg_rc"
|
|
391
|
+
OBS_POS_NAME="$pos_name"; OBS_NEG_NAME="$neg_name"
|
|
204
392
|
|
|
205
393
|
if [ -z "$pos_name" ]; then
|
|
206
394
|
_fail "M4" "양성 arm 의 exit $pos_rc 가 선언된 enum 밖 — enum 밖 값은 HARNESS_ERROR 이지 PASS 가 아니다"
|
|
@@ -254,6 +442,7 @@ _check_one() {
|
|
|
254
442
|
_hops=$((_hops+1))
|
|
255
443
|
if [ "$_hops" -gt 40 ]; then
|
|
256
444
|
_fail "M6" "심링크가 40회를 넘어간다 — 순환으로 본다. 프로브 경로를 못 정한다"
|
|
445
|
+
_observation_scope # 이 이른 return 에서도 관측 범위는 찍힌다(조건부로 찍히면 채널이 아니다)
|
|
257
446
|
return 0 # _fail 이 이미 기록했다. 판정 불가를 통과로 렌더하지 않는다
|
|
258
447
|
fi
|
|
259
448
|
_dir=$(cd -P "$(dirname "$_src")" >/dev/null 2>&1 && pwd) || break
|
|
@@ -279,6 +468,10 @@ _check_one() {
|
|
|
279
468
|
esac
|
|
280
469
|
fi
|
|
281
470
|
fi
|
|
471
|
+
|
|
472
|
+
# ★무조건 찍는다 — PASS 든 REJECTED 든, arm 을 돌렸든 안 돌렸든. 관측 범위가 조건부로
|
|
473
|
+
# 찍히면 그건 채널이 아니라 또 하나의 판정이 된다.
|
|
474
|
+
_observation_scope
|
|
282
475
|
}
|
|
283
476
|
|
|
284
477
|
# ── self-test (known-pair: 통과해야 할 선언 1 · 막혀야 할 선언 6) ─────────────
|
|
@@ -324,6 +517,23 @@ EOF
|
|
|
324
517
|
sed 's|^requires_cwd: .*|requires_cwd: relative/path|' "$T/good.cap" > "$T/relcwd.cap"
|
|
325
518
|
_t "M5: requires_cwd 상대경로" 1 "$T/relcwd.cap"
|
|
326
519
|
|
|
520
|
+
# ── M5 `SELF` (2026-08-16) — 하네스가 **자기 레포에 tracked 로** 선언할 수 있게 하는 값 ──
|
|
521
|
+
# known-pair 를 **양쪽 다** 세운다. 양성만 세우면 「SELF 를 그냥 통과시킨다」와
|
|
522
|
+
# 「SELF 를 옳게 해석한다」가 구분되지 않는다.
|
|
523
|
+
local SR="$T/selfrepo"
|
|
524
|
+
mkdir -p "$SR/.claude/capabilities"
|
|
525
|
+
( cd "$SR" && git init -q . && git config user.email t@t && git config user.name t \
|
|
526
|
+
&& echo x > seed.txt && git add seed.txt && git commit -qm init ) >/dev/null 2>&1
|
|
527
|
+
cp "$T/probe.sh" "$SR/probe.sh" 2>/dev/null || true
|
|
528
|
+
sed -e 's|^requires_cwd: .*|requires_cwd: SELF|' \
|
|
529
|
+
-e "s|^entry: .*|entry: bash $SR/probe.sh|" "$T/good.cap" > "$SR/.claude/capabilities/self.cap"
|
|
530
|
+
_t "M5: SELF 는 레포 루트로 해석된다" 0 "$SR/.claude/capabilities/self.cap"
|
|
531
|
+
|
|
532
|
+
# known-negative(=fail-closed 방향): git 레포 **밖**의 SELF 는 해석 불가여야 한다.
|
|
533
|
+
# 이 레인이 없으면 「SELF 면 무조건 통과」가 되고, 그건 M5 를 없앤 것과 같다.
|
|
534
|
+
sed -e 's|^requires_cwd: .*|requires_cwd: SELF|' "$T/good.cap" > "$T/self_outside.cap"
|
|
535
|
+
_t "M5: git 밖의 SELF 는 fail-closed" 1 "$T/self_outside.cap"
|
|
536
|
+
|
|
327
537
|
# ── M6: 선언 진위 (2026-08-16) — 2026-08-11 실사고 형태를 그대로 재현한다 ──
|
|
328
538
|
# `writes: read-only` 를 선언하고 진입점이 `rm -rf scripts` 를 한다. 그날 이 검사기는
|
|
329
539
|
# M1–M5 를 전부 통과시켰다. 이제 M6 가 막는다.
|
|
@@ -368,6 +578,47 @@ LIAR
|
|
|
368
578
|
else
|
|
369
579
|
fail=$((fail+1)); printf ' ❌ %-34s — rc 는 1 인데 M6 가 막은 게 아니다(다른 축이 먼저 걸렸다)\n' "M6: 차단 귀속"
|
|
370
580
|
fi
|
|
581
|
+
# ── 관측 범위 블록 (2026-08-16) — 「무엇을 안 봤는지」가 출력에 남는가 ───────────
|
|
582
|
+
# 이 레인들은 **판정을 재지 않는다**(rc 는 위 레인들이 이미 고정한다). 재는 것은
|
|
583
|
+
# «채널이 켜져 있나 · 계산이 맞나 · 조건부로 꺼지지 않나» 셋이다.
|
|
584
|
+
_tg() { # $1=이름 $2=기대 rc $3=capfile $4=반드시 포함할 문자열
|
|
585
|
+
local o r hit; o="$(bash "$0" "$3" 2>&1)"; r=$?
|
|
586
|
+
if printf '%s' "$o" | grep -qF "$4"; then hit=1; else hit=0; fi
|
|
587
|
+
if [ "$r" = "$2" ] && [ "$hit" = "1" ]; then
|
|
588
|
+
pass=$((pass+1)); printf ' ✅ %-34s rc=%s\n' "$1" "$r"
|
|
589
|
+
else
|
|
590
|
+
fail=$((fail+1))
|
|
591
|
+
printf ' ❌ %-34s rc=%s (기대 %s) · 부분문자열 «%s» %s\n%s\n' \
|
|
592
|
+
"$1" "$r" "$2" "$4" "$( [ "$hit" = 1 ] && printf 있음 || printf 없음 )" "$o"
|
|
593
|
+
fi
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# ⓐ 부분 관측: 선언 enum 4값 · 이 실행이 밟는 건 2값 → **나머지 2값을 이름으로** 적어야 한다.
|
|
597
|
+
# (`good.cap` 의 probe 는 ok→0 / 그 외→1 만 낸다. 3·7 은 이 실행이 갈 수 없는 값이다.)
|
|
598
|
+
sed 's/^verdict_enum: .*/verdict_enum: 0=PASS 1=FAIL 3=DID_NOT_RUN 7=NO_TARGET/' \
|
|
599
|
+
"$T/good.cap" > "$T/obs_partial.cap"
|
|
600
|
+
_tg "관측범위: 미관측 enum 을 이름으로" 0 "$T/obs_partial.cap" \
|
|
601
|
+
"관측되지 않은** 값: 3=DID_NOT_RUN 7=NO_TARGET"
|
|
602
|
+
|
|
603
|
+
# ⓑ 🟥**컨트롤** — 전부 관측되면 「없음」을 찍어야 한다. 이게 없으면 「늘 경고하는 계기」와
|
|
604
|
+
# 「실제로 계산하는 계기」가 구분되지 않는다(경고만 하는 계기는 판별력이 0 이다).
|
|
605
|
+
# 선언 전 값이 관측되려면 음성 arm 이 「안 돌았다」 코드로 떨어져야 한다(M2+ 가 그 값을
|
|
606
|
+
# 요구하므로) — 그래서 전용 probe 를 쓴다.
|
|
607
|
+
printf '#!/bin/sh\n[ "$1" = ok ] && exit 0\nexit 3\n' > "$T/probe_allobs.sh"
|
|
608
|
+
sed -e 's/^verdict_enum: .*/verdict_enum: 0=PASS 3=DID_NOT_RUN/' \
|
|
609
|
+
-e "s|^entry: .*|entry: bash $T/probe_allobs.sh|" \
|
|
610
|
+
-e 's/^calibration_negative_expect: .*/calibration_negative_expect: DID_NOT_RUN/' \
|
|
611
|
+
"$T/good.cap" > "$T/obs_all.cap"
|
|
612
|
+
_tg "관측범위: 전부 관측되면 「없음」" 0 "$T/obs_all.cap" \
|
|
613
|
+
"관측되지 않은 값: 없음"
|
|
614
|
+
|
|
615
|
+
# ⓒ REJECTED 에서도 블록이 찍힌다 + arm 을 안 돌렸다는 사실과 사유가 적힌다.
|
|
616
|
+
# (`nocal.cap` = 캘리브레이션 키 전체 제거 → args 는 「미선언」, 관측 exit 은 「없음」.)
|
|
617
|
+
_tg "관측범위: REJECTED 에서도 찍힌다" 1 "$T/nocal.cap" \
|
|
618
|
+
"관측된 exit: 없음 — arm 을 실행하지 않았다(캘리브레이션 쌍 미선언)"
|
|
619
|
+
_tg "관측범위: 미선언 args 를 미선언이라 적는다" 1 "$T/nocal.cap" \
|
|
620
|
+
"실행된 arm: 양성 «<미선언>» / 음성 «<미선언>»"
|
|
621
|
+
|
|
371
622
|
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
372
623
|
rm -rf "$T"
|
|
373
624
|
[ "$fail" -eq 0 ] || return 1
|
package/scripts/chamber_run.sh
CHANGED
|
@@ -125,8 +125,11 @@ if [ ! -f "$WS/BUDGET.md" ]; then
|
|
|
125
125
|
|
|
126
126
|
# Route through goal-quench's budget gate for an expensive run, then record here.
|
|
127
127
|
# Demo-scale runs may self-cap — but an ESTIMATE line is mandatory (this is the entry cap).
|
|
128
|
+
#
|
|
129
|
+
# 🟥 ACTUAL 은 이 파일에 적지 마라 — step 6 이 별도 ACTUAL.md 를 쓴다.
|
|
130
|
+
# 이유: 이 파일의 판정-전 해시가 순서 증인(ship_readiness_gate §② P1)이다. 사후에
|
|
131
|
+
# 이 파일을 고치면 증인이 TAMPERED 로 죽는다 (2026-08-17 런 #11 실측).
|
|
128
132
|
ESTIMATE: <e.g. ~3 persona dispatches, demo-scale, self-capped | or a token budget>
|
|
129
|
-
ACTUAL: <filled at step 6>
|
|
130
133
|
EOF
|
|
131
134
|
echo " ⛔ step 3 BLOCKED: record an ESTIMATE in $WS/BUDGET.md (budget-entry gate G2), then re-run."; exit 1
|
|
132
135
|
fi
|
|
@@ -203,8 +206,26 @@ if [ -f "$_WITNESS" ]; then
|
|
|
203
206
|
fi
|
|
204
207
|
|
|
205
208
|
# STEP 6 — actual cost / carry-forward record.
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
# 🟥 별도 파일이다. BUDGET.md 가 아니다 — 그리고 그 분리가 이 게이트의 핵심이다.
|
|
210
|
+
# 2026-08-17 런 #11(첫 형식 완주)이 실측한 결함: step 3 이 BUDGET.md 의 해시를 순서 증인으로
|
|
211
|
+
# 기록하는데 step 6 이 같은 파일에 ACTUAL 을 요구하며 하드 차단했다 → 완주하면 반드시
|
|
212
|
+
# 증인 아티팩트가 사후 변경되고 verify 가 TAMPERED 를 낸다 → **완주한 런은 구조적으로
|
|
213
|
+
# ② 승급 근거가 될 수 없었다.** 대상 선정 실수가 아니라 한 파일에 두 역할(①불변 요구 =
|
|
214
|
+
# 증인 · ②변경 요구 = 사후 캘리브레이션)이 겹친 것이고, 개별로는 둘 다 옳아서 어느 쪽
|
|
215
|
+
# 코드를 읽어도 안 보였다. 그래서 스펙(P1 이 BUDGET 을 증인으로 지목)은 안 건드리고
|
|
216
|
+
# **변경 요구만** 빼낸다. ACTUAL.md 는 정의상 판정 후 산물이므로 **증인에 기록하지 않는다.**
|
|
217
|
+
if [ ! -f "$WS/ACTUAL.md" ]; then
|
|
218
|
+
cat > "$WS/ACTUAL.md" <<EOF
|
|
219
|
+
# ACTUAL — $SLUG (chamber run, 판정 후 기록)
|
|
220
|
+
#
|
|
221
|
+
# 이 파일은 **순서 증인 대상이 아니다** — 판정 후에 쓰이는 것이 정상이므로 해시를 걸지 않는다.
|
|
222
|
+
# ESTIMATE 는 BUDGET.md 에 있고 그 파일은 판정 전에 얼어 있다. 대조는 사람이 한다.
|
|
223
|
+
ACTUAL: <number/summary — dispatch 수 · 토큰 · 예산 대비 편차>
|
|
224
|
+
EOF
|
|
225
|
+
fi
|
|
226
|
+
if grep -qE '^ACTUAL:[[:space:]]*<' "$WS/ACTUAL.md" || ! grep -qE '^ACTUAL:[[:space:]]*\S' "$WS/ACTUAL.md"; then
|
|
227
|
+
echo " ⛔ step 6 BLOCKED: record ACTUAL cost in $WS/ACTUAL.md (actual-vs-estimate calibration)."
|
|
228
|
+
echo " 🟥 BUDGET.md 가 아니다 — 그 파일은 판정 전 증인이라 사후 변경하면 증인이 죽는다."
|
|
208
229
|
echo " Expected — a LINE PREFIX, not a heading:"
|
|
209
230
|
echo " ACTUAL: <number/summary> ← this form is checked"
|
|
210
231
|
echo " ## ACTUAL ← a heading does NOT satisfy it"
|
|
@@ -140,6 +140,31 @@ HDR
|
|
|
140
140
|
echo "❌ 원장 헤더 쓰기 실패: $LEDGER" >&2; return 10
|
|
141
141
|
fi
|
|
142
142
|
fi
|
|
143
|
+
# ── 멱등: 같은 (run, artifact, sha) 삼중항이 이미 있으면 다시 안 적는다 ──────────
|
|
144
|
+
# 기원 2026-08-17 런 #11(첫 형식 완주): 이 함수가 **무조건 append** 라서 러너를 advance
|
|
145
|
+
# 할 때마다 step 2~5 가 재실행되며 같은 해시가 다시 쌓였다 — 4스텝 런에 **엔트리 11개**.
|
|
146
|
+
# 증인 판정 자체는 안 틀리지만(같은 해시는 같은 결론) 공개 tracked 파일이 부풀고,
|
|
147
|
+
# verify 출력이 같은 줄을 여러 번 뱉어 **읽는 사람이 「몇 건이 문제인가」를 오독**한다.
|
|
148
|
+
# 🟥 내용이 바뀐 경우는 **여전히 새 엔트리로 남는다** — 그게 TAMPERED 를 성립시키는
|
|
149
|
+
# 증거이므로 여기서 접으면 안 된다. 접는 것은 «완전히 동일한 재기록»뿐이다.
|
|
150
|
+
# 🟥 오프셋 주의 — `- run: ` 는 **7자**라 값은 8부터다. 초판이 9로 썼고(cross-family
|
|
151
|
+
# gpt-5.5 적발, A급) 그러면 슬러그 첫 글자가 잘려 **어떤 정상 엔트리와도 매칭되지 않는다**
|
|
152
|
+
# → 가드가 조용히 무력화되고 append 가 계속된다. 재현: `printf -- '- run: g2\n' |
|
|
153
|
+
# awk '{print substr($0,9)}'` → `2`. **레인이 이걸 못 잡았다**(아래 L14 신설로 닫음).
|
|
154
|
+
# 인접성도 강제한다: run → artifact → sha256 이 **연속**일 때만 인정. 안 그러면 깨진
|
|
155
|
+
# YAML 에서 stale run/art 가 무관한 sha 와 짝지어 **필요한 append 를 눌러버린다**(같은 리뷰 B급).
|
|
156
|
+
if awk -v r="$slug" -v a="$base" -v s="$h" '
|
|
157
|
+
/^- run: / { run=substr($0,8); art=""; prev="run"; next }
|
|
158
|
+
/^ artifact: / { if (prev=="run") { art=substr($0,13); prev="art" }
|
|
159
|
+
else { art=""; prev="" } ; next }
|
|
160
|
+
/^ sha256: / { if (prev=="art" && run==r && art==a && substr($0,11)==s) { found=1; exit }
|
|
161
|
+
prev=""; next }
|
|
162
|
+
{ prev="" }
|
|
163
|
+
END { exit(found?0:1) }
|
|
164
|
+
' "$LEDGER" 2>/dev/null; then
|
|
165
|
+
echo "witnessed(already): $slug/$base — 동일 해시가 이미 원장에 있다(재기록 안 함)"
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
143
168
|
if ! printf -- '- run: %s\n artifact: %s\n sha256: %s\n recorded: %s\n' \
|
|
144
169
|
"$slug" "$base" "$h" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LEDGER" 2>/dev/null; then
|
|
145
170
|
echo "❌ 원장 append 실패: $LEDGER" >&2; return 10
|