@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
|
@@ -179,17 +179,62 @@ do_digest() {
|
|
|
179
179
|
if [ ! -f "$sealfile" ]; then return 0; fi
|
|
180
180
|
|
|
181
181
|
# 신선도·세션 정합을 **주장에 반영**한다. 안 맞으면 주입은 하되 "직전 압축"이라고 말하지 않는다.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
# ⚠️ 두 가드 모두 초판에서 **조용히 꺼지는 경로**를 갖고 있었다(실측 2026-08-13, 4-arm 재현).
|
|
183
|
+
# 공통 축은 하나다 — **미측정을 「정상」으로 렌더**했다(`not found ≠ 0`).
|
|
184
|
+
# ⚠️ `_age` 초기화 = **2중 방어**. 산술이 실패하면 대입이 안 되고, 다음 줄의 `$_age` 가 `set -u`
|
|
185
|
+
# 아래 unbound 로 셸을 그 자리에서 죽인다 — 훅은 `exit 0` 에 도달 못 하고 **stdout 이
|
|
186
|
+
# 통째로 폐기**된다(비영 종료 = 무음). 이 파일이 스스로 못 박은 불변식이 거기서 깨진다.
|
|
187
|
+
# 🔍 **정직한 표시**: 아래 `case` 가 숫자만 통과시키고 `10#` 이 진법을 고정하는 한, 이 줄은
|
|
188
|
+
# **도달 불가능하고 레인도 없다**(되돌림 arm D 실측: 이 줄만 지우면 47쌍 전부 초록).
|
|
189
|
+
# 앵커가 걸린 것은 `10#` 쪽(arm A)이다. 이 줄은 그게 지워졌을 때를 위한 두 번째 층이지,
|
|
190
|
+
# 회귀 앵커가 아니다 — 있는 척하지 않으려고 여기 적는다.
|
|
191
|
+
local _now _age=0 _stale="" _note="" _xsess="" _sessmatch=no
|
|
192
|
+
_now=$(date +%s)
|
|
193
|
+
# ⓑ 를 **먼저** 계산한다 — 나이 문구가 세션 일치 여부에 의존하기 때문이다(아래 ⓐ-3).
|
|
194
|
+
# 세션 대조는 **양쪽을 다 알 때만** 성립한다. 한쪽이라도 unknown 이면 결과는 「일치」가 아니라
|
|
195
|
+
# **「대조 불가」**다. 초판은 그 경우 경고를 껐는데, 소비자가 unknown 인 경로가 바로
|
|
196
|
+
# 주인 없는 남의 마커를 집는 경로(line 165)여서 **가장 필요한 자리에서 꺼져 있었다**.
|
|
197
|
+
if [ -n "${sealsess:-}" ] && [ "${sealsess}" != "unknown" ] \
|
|
198
|
+
&& [ -n "$DIGEST_SESSION" ] && [ "$DIGEST_SESSION" != "unknown" ]; then
|
|
199
|
+
if [ "${sealsess}" = "$DIGEST_SESSION" ]; then
|
|
200
|
+
_sessmatch=yes
|
|
201
|
+
else
|
|
202
|
+
_xsess=" ⚠️ 다른 세션(${sealsess})의 봉인이다"
|
|
203
|
+
fi
|
|
204
|
+
else
|
|
205
|
+
_xsess=" ⚠️ 세션 대조 불가(봉인=${sealsess:-미상} · 소비=${DIGEST_SESSION:-미상}) — 남의 원장일 수 있다"
|
|
188
206
|
fi
|
|
207
|
+
# ⓐ 봉인 시각이 없거나 숫자가 아니면 나이는 **미상**이다. 초판은 `${sealts:-$_now}` 로 「지금」을
|
|
208
|
+
# 기본값으로 써서 _age=0 을 만들었고, 구형식 pending(경로 한 줄)에서 신선도 가드가 통째로
|
|
209
|
+
# 무음 소실했다 — 실측: 5일 묵은 봉인이 "직전 압축"이라고 주장하며 나갔다.
|
|
210
|
+
case "${sealts:-}" in
|
|
211
|
+
''|*[!0-9]*) _stale=" ⚠️ 봉인 시각 미상 — 이 세션의 직전 압축인지 확인 불가" ;;
|
|
212
|
+
# `10#` 으로 진법을 고정한다. 선행 0(`08…`)은 8진수로 읽혀 `value too great for base` 를
|
|
213
|
+
# 내고, 위에서 적은 무음 사망 경로가 정확히 그 입력으로 열린다(실측: 산술 다음 줄에 도달 못 함).
|
|
214
|
+
*) _age=$(( _now - 10#$sealts ))
|
|
215
|
+
# 미래 시각이면 _age 가 음수라 `-gt 43200` 이 거짓이 되고 가드가 **또 조용히 꺼진다** —
|
|
216
|
+
# 이 파일이 이번에 두 번 밟은 그 축의 세 번째 얼굴이다(cross-family codex/gpt-5.5 지목,
|
|
217
|
+
# 자력 적발 0). 시계 어긋남이거나 마커가 손대진 것이고, 둘 다 「방금 봉인」이 아니다.
|
|
218
|
+
if [ "$_age" -lt 0 ]; then
|
|
219
|
+
_stale=" ⚠️ 봉인 시각이 미래($(( - _age ))초 뒤) — 시계 어긋남 또는 손댄 마커, 신선도 판정 불가"
|
|
220
|
+
elif [ "$_age" -gt 43200 ]; then
|
|
221
|
+
# ⓐ-3 **세션이 일치하면 이 경고는 반증 가능하게 틀렸다.** 마커는 봉인마다 세션별로
|
|
222
|
+
# 덮어써지므로(line 150), 일치하는 마커가 가리키는 것은 정의상 그 세션의 **가장
|
|
223
|
+
# 최근** 봉인이다. 그때 나이는 주장을 낮출 근거가 아니라 부가 정보다 — 주장 정확도를
|
|
224
|
+
# 위해 만든 가드가 스스로 틀린 말을 하던 자리(Axis 2 M-4 지목).
|
|
225
|
+
if [ "$_sessmatch" = yes ]; then
|
|
226
|
+
_note=" (${_age}초 전 봉인 — 같은 세션의 가장 최근 봉인이다)"
|
|
227
|
+
else
|
|
228
|
+
_stale=" ⚠️ ${_age}초 전 봉인 — 이 세션의 직전 압축이 아닐 수 있다"
|
|
229
|
+
fi
|
|
230
|
+
fi ;;
|
|
231
|
+
esac
|
|
232
|
+
# `_note` 는 **주장을 낮추지 않는다** — 분기 조건에서 뺀 이유가 그것이다. 나이를 알려주되
|
|
233
|
+
# 「직전 압축」이 참인 경우(세션 일치)에는 그 주장을 유지한다.
|
|
189
234
|
if [ -n "$_stale$_xsess" ]; then
|
|
190
|
-
echo "🧭 [FH 압축 복구] 봉인된 포인터 원장이 있다 — 내용이 아니라 경로다.$_stale$_xsess"
|
|
235
|
+
echo "🧭 [FH 압축 복구] 봉인된 포인터 원장이 있다 — 내용이 아니라 경로다.$_stale$_xsess$_note"
|
|
191
236
|
else
|
|
192
|
-
echo "🧭 [FH 압축 복구] 직전 압축 전에 봉인된 포인터 원장이 있다 — 내용이 아니라
|
|
237
|
+
echo "🧭 [FH 압축 복구] 직전 압축 전에 봉인된 포인터 원장이 있다 — 내용이 아니라 경로다.$_note"
|
|
193
238
|
fi
|
|
194
239
|
echo " 정본: $sealfile"
|
|
195
240
|
echo
|
|
@@ -327,8 +372,21 @@ self_test() {
|
|
|
327
372
|
local dg; dg="$(do_digest "$T/out3b" 2>/dev/null)"
|
|
328
373
|
case "$dg" in *"열어야 할 정본"*) r=YES ;; *) r=NO ;; esac
|
|
329
374
|
t "#3 digest 가 정본 포인터를 주입한다" YES "$r"
|
|
330
|
-
|
|
331
|
-
|
|
375
|
+
# ⚠️ 이 레인은 한때 "브랜치:" 등장을 무조건 YES 로 기대했다. **REPO_ROOT 가 git 저장소인지는
|
|
376
|
+
# self-test 가 통제하지 않는 환경 조건**이다(스크립트 위치로 계산되는 값이지 $T 픽스처가 아니다) —
|
|
377
|
+
# 그래서 non-git 트리(예: 소비자가 tarball 을 git 밖 스크래치 디렉터리에 풀어 실행)에서는 항상
|
|
378
|
+
# FAIL 했다. known-pair 로 확인: 레포 rc=0 · non-git tmp rc=1 · **같은 tmp 에 `git init` 만 해도
|
|
379
|
+
# rc=0**(2026-08-12, 재출하 축). do_seal 자신은 두 경우 다 올바르게 렌더한다("브랜치: …" 또는
|
|
380
|
+
# "(git 레포 아님)") — 결함은 self-test 가 그 중 한쪽만 정답으로 하드코딩한 것이었다. 지금 실제
|
|
381
|
+
# 상태를 물어 그 상태에 맞는 렌더를 기대한다 — 두 분기 다 계측한다(브랜치 있음일 때만 잰 것이
|
|
382
|
+
# 아니라 없음일 때도 "(git 레포 아님)" 이 실제로 나오는지 검증).
|
|
383
|
+
if git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
|
384
|
+
case "$dg" in *"브랜치:"*) r=YES ;; *) r=NO ;; esac
|
|
385
|
+
t "#3 digest 가 git 상태를 주입한다 (REPO_ROOT=git repo)" YES "$r"
|
|
386
|
+
else
|
|
387
|
+
case "$dg" in *"(git 레포 아님)"*) r=YES ;; *) r=NO ;; esac
|
|
388
|
+
t "#3 digest 가 git 상태를 주입한다 (REPO_ROOT=non-git, graceful render)" YES "$r"
|
|
389
|
+
fi
|
|
332
390
|
|
|
333
391
|
# #4 세션 교차 주입 — 다른 세션의 봉인을 "직전 압축"이라고 주장하면 안 된다
|
|
334
392
|
# ⚠️ 이 레인은 1차 수리 때 "라벨하면 된다"는 전제로 썼다가 **갱신됐다**. 라벨은 오배달을
|
|
@@ -347,6 +405,91 @@ self_test() {
|
|
|
347
405
|
case "$d4b" in *"다른 세션"*) rc=YES ;; *) rc=NO ;; esac
|
|
348
406
|
t "#4 같은 세션에 교차 경고 안 뜬다" NO "$rc"
|
|
349
407
|
|
|
408
|
+
# #11 신선도 가드가 **구형식 pending(경로 한 줄)** 에서 무음으로 꺼지지 않는다.
|
|
409
|
+
# 실측 2026-08-13: 이 경로로 5일 묵은 봉인이 "직전 압축"이라고 주장하며 실제 세션에 주입됐다.
|
|
410
|
+
do_seal "$T/tr.jsonl" "$T/out11" "SESSA" >/dev/null 2>&1
|
|
411
|
+
local p11; p11="$(ls "$T/out11"/.pending_* 2>/dev/null | head -1)"
|
|
412
|
+
cut -f1 "$p11" > "$p11.t" 2>/dev/null && mv "$p11.t" "$p11" # 구형식으로 강등 = 봉인 시각 소실
|
|
413
|
+
local d11; d11="$(do_digest "$T/out11" "SESSA" 2>/dev/null)"
|
|
414
|
+
case "$d11" in *"직전 압축 전에"*) rc=YES ;; *) rc=NO ;; esac
|
|
415
|
+
t "#11 시각 미상 봉인을 '직전 압축'이라 주장하지 않는다" NO "$rc"
|
|
416
|
+
case "$d11" in *"봉인 시각 미상"*) rc=YES ;; *) rc=NO ;; esac
|
|
417
|
+
t "#11 ★ 시각 미상이 타입으로 남는다 (0 으로 렌더 금지)" YES "$rc"
|
|
418
|
+
|
|
419
|
+
# #11 known-negative — 정상 형식엔 과경고가 붙으면 안 된다. 이게 없으면 "항상 경고"로도 통과한다.
|
|
420
|
+
do_seal "$T/tr.jsonl" "$T/out11b" "SESSA" >/dev/null 2>&1
|
|
421
|
+
local d11b; d11b="$(do_digest "$T/out11b" "SESSA" 2>/dev/null)"
|
|
422
|
+
case "$d11b" in *"봉인 시각 미상"*) rc=YES ;; *) rc=NO ;; esac
|
|
423
|
+
t "#11 known-negative: 정상 봉인엔 미상 라벨 안 붙는다" NO "$rc"
|
|
424
|
+
case "$d11b" in *"직전 압축 전에"*) rc=YES ;; *) rc=NO ;; esac
|
|
425
|
+
t "#11 known-negative: 정상 봉인은 직전-압축 주장 유지" YES "$rc"
|
|
426
|
+
|
|
427
|
+
# #11c **미래 시각**도 신선도 판정 불가다 — 음수 _age 는 `-gt 43200` 을 통과 못 해서
|
|
428
|
+
# 가드가 또 조용히 꺼진다. cross-family(codex/gpt-5.5)가 이번 수리 안에서 지목한 세 번째 얼굴.
|
|
429
|
+
do_seal "$T/tr.jsonl" "$T/out11c" "SESSA" >/dev/null 2>&1
|
|
430
|
+
local p11c; p11c="$(ls "$T/out11c"/.pending_* 2>/dev/null | head -1)"
|
|
431
|
+
awk -v ts="$(( $(date +%s) + 86400 ))" 'BEGIN{FS=OFS="\t"}{$3=ts;print}' "$p11c" > "$p11c.t" \
|
|
432
|
+
&& mv "$p11c.t" "$p11c"
|
|
433
|
+
local d11c; d11c="$(do_digest "$T/out11c" "SESSA" 2>/dev/null)"
|
|
434
|
+
case "$d11c" in *"직전 압축 전에"*) rc=YES ;; *) rc=NO ;; esac
|
|
435
|
+
t "#11c 미래 시각 봉인을 '직전 압축'이라 주장하지 않는다" NO "$rc"
|
|
436
|
+
case "$d11c" in *"미래"*) rc=YES ;; *) rc=NO ;; esac
|
|
437
|
+
t "#11c ★ 미래 시각이 타입으로 남는다 (음수 나이 무음 금지)" YES "$rc"
|
|
438
|
+
case "$d11b" in *"미래"*) rc=YES ;; *) rc=NO ;; esac
|
|
439
|
+
t "#11c known-negative: 정상 시각엔 미래 라벨 안 붙는다" NO "$rc"
|
|
440
|
+
|
|
441
|
+
# #11d **묵은-봉인 가지(>43200)에 앵커가 0개였다** — 실측 발현(5일 묵은 봉인)의 정상-형식
|
|
442
|
+
# 판본인데, 레인이 수리의 어휘(미상 라벨)만 따라가고 결함의 어휘(나이)를 안 따라갔다.
|
|
443
|
+
# 검증: 이 레인 없이 `elif [ "$_age" -gt 43200 ]` 가지를 통째로 지워도 39쌍 전부 초록이었다.
|
|
444
|
+
# (Axis 2 M-3 지목. 세션 일치 케이스라 주장은 유지되고 나이는 부가정보로 붙는다 — M-4)
|
|
445
|
+
do_seal "$T/tr.jsonl" "$T/out11d" "SESSA" >/dev/null 2>&1
|
|
446
|
+
local p11d; p11d="$(ls "$T/out11d"/.pending_* 2>/dev/null | head -1)"
|
|
447
|
+
awk -v ts="$(( $(date +%s) - 432000 ))" 'BEGIN{FS=OFS="\t"}{$3=ts;print}' "$p11d" > "$p11d.t" \
|
|
448
|
+
&& mv "$p11d.t" "$p11d"
|
|
449
|
+
local d11d; d11d="$(do_digest "$T/out11d" "SESSA" 2>/dev/null)"
|
|
450
|
+
case "$d11d" in *"초 전 봉인"*) rc=YES ;; *) rc=NO ;; esac
|
|
451
|
+
t "#11d ★ 묵은 봉인의 나이가 실제로 인쇄된다 (가지 삭제를 잡는 유일한 앵커)" YES "$rc"
|
|
452
|
+
case "$d11d" in *"직전 압축이 아닐 수 있다"*) rc=YES ;; *) rc=NO ;; esac
|
|
453
|
+
t "#11d 세션 일치면 '아닐 수 있다'는 거짓 주장을 안 한다" NO "$rc"
|
|
454
|
+
case "$d11b" in *"초 전 봉인"*) rc=YES ;; *) rc=NO ;; esac
|
|
455
|
+
t "#11d known-negative: 신선한 봉인엔 나이 문구 없음" NO "$rc"
|
|
456
|
+
|
|
457
|
+
# #11e 선행 0 타임스탬프가 **훅을 죽이지 않는다**. set -u 아래 산술 실패 → 다음 줄 unbound →
|
|
458
|
+
# 셸 즉시 종료 → exit 0 미도달 → stdout 통째 폐기. 이 파일의 「훅은 절대 비영 종료 안 한다」
|
|
459
|
+
# 불변식이 입력 하나로 깨지던 자리다(Axis 2 M-1, 실측 재현).
|
|
460
|
+
do_seal "$T/tr.jsonl" "$T/out11e" "SESSA" >/dev/null 2>&1
|
|
461
|
+
local p11e; p11e="$(ls "$T/out11e"/.pending_* 2>/dev/null | head -1)"
|
|
462
|
+
awk 'BEGIN{FS=OFS="\t"}{$3="08123456";print}' "$p11e" > "$p11e.t" && mv "$p11e.t" "$p11e"
|
|
463
|
+
local d11e d11e_rc
|
|
464
|
+
d11e="$(do_digest "$T/out11e" "SESSA" 2>&1)"; d11e_rc=$?
|
|
465
|
+
t "#11e ★ 선행 0 타임스탬프에도 digest 가 정상 종료한다" 0 "$d11e_rc"
|
|
466
|
+
case "$d11e" in *"정본:"*) rc=YES ;; *) rc=NO ;; esac
|
|
467
|
+
t "#11e 선행 0 에도 포인터가 실제로 인쇄된다 (무음 아님)" YES "$rc"
|
|
468
|
+
|
|
469
|
+
# #13 「다른 세션의 봉인이다」 분기는 **살아있는데 앵커가 0개**였다 — #4 는 라우팅(0바이트)만
|
|
470
|
+
# 재서 이 문자열에 도달할 수 없다. 프로덕션 도달 경로 = 구형식 단일 `.pending` 관용(line 167).
|
|
471
|
+
do_seal "$T/tr.jsonl" "$T/out13" "SESSX" >/dev/null 2>&1
|
|
472
|
+
local sf13; sf13="$(ls -t "$T/out13"/seal_*.md 2>/dev/null | head -1)"
|
|
473
|
+
printf '%s\t%s\t%s\n' "$sf13" "SESSX" "$(( $(date +%s) - 432000 ))" > "$T/out13/.pending"
|
|
474
|
+
local d13; d13="$(do_digest "$T/out13" "SESSA" 2>/dev/null)"
|
|
475
|
+
case "$d13" in *"다른 세션(SESSX)"*) rc=YES ;; *) rc=NO ;; esac
|
|
476
|
+
t "#13 ★ 교차세션 경고가 실제로 인쇄된다 (분기 무앵커 폐쇄)" YES "$rc"
|
|
477
|
+
case "$d13" in *"세션 대조 불가"*) rc=YES ;; *) rc=NO ;; esac
|
|
478
|
+
t "#13 양쪽을 아는 불일치는 '대조 불가'가 아니다" NO "$rc"
|
|
479
|
+
case "$d13" in *"초 전 봉인"*) rc=YES ;; *) rc=NO ;; esac
|
|
480
|
+
t "#13 세션 불일치 + 묵음이면 나이 경고가 붙는다" YES "$rc"
|
|
481
|
+
|
|
482
|
+
# #12 소비자 세션이 미상이면 결과는 「일치」가 아니라 **「대조 불가」**다.
|
|
483
|
+
# 그 경로(line 165)가 바로 주인 없는 남의 마커를 집는 경로여서, 초판은 가장 필요한 자리에서 꺼졌다.
|
|
484
|
+
do_seal "$T/tr.jsonl" "$T/out12" "SESSA" >/dev/null 2>&1
|
|
485
|
+
local d12; d12="$(do_digest "$T/out12" "unknown" 2>/dev/null)"
|
|
486
|
+
case "$d12" in *"세션 대조 불가"*) rc=YES ;; *) rc=NO ;; esac
|
|
487
|
+
t "#12 소비자 미상이면 대조 불가로 라벨된다" YES "$rc"
|
|
488
|
+
case "$d12" in *"직전 압축 전에"*) rc=YES ;; *) rc=NO ;; esac
|
|
489
|
+
t "#12 대조 불가면 '직전 압축' 주장 안 한다" NO "$rc"
|
|
490
|
+
case "$d11b" in *"세션 대조 불가"*) rc=YES ;; *) rc=NO ;; esac
|
|
491
|
+
t "#12 known-negative: 양쪽 아는 경로엔 대조불가 라벨 없음" NO "$rc"
|
|
492
|
+
|
|
350
493
|
# #8 포인터 절은 **절대 안 잘린다** — 더티파일 목록이 길어도
|
|
351
494
|
do_seal "$T/tr.jsonl" "$T/out8" "SESS8" >/dev/null 2>&1
|
|
352
495
|
local d8; d8="$(do_digest "$T/out8" "SESS8" 2>/dev/null)"
|
|
@@ -72,12 +72,72 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
72
72
|
REG="${1:-$ROOT/tracks/_meta/consent_classes.yaml}"
|
|
73
73
|
UAP="${2:-$ROOT/tracks/_meta/user_adaptation_profile.md}"
|
|
74
74
|
|
|
75
|
+
# ── What this run measured WITH — computed in a SEPARATE PROCESS ────────────────────────────────
|
|
76
|
+
# Measured 2026-08-12: this gate rides selfcheck → prepublishOnly. A release shipped green while the
|
|
77
|
+
# machine's own python3 had no PyYAML — the session that ran it happened to have an unrelated
|
|
78
|
+
# project's venv first on PATH. The reproduction unit is the PATH a session inherited, and the green
|
|
79
|
+
# said nothing about it. Failure already explained itself; success explained nothing.
|
|
80
|
+
#
|
|
81
|
+
# Why a separate process, after three cross-family rounds walked it here:
|
|
82
|
+
# draft 1 `import yaml` ahead of the N/A branch — started EXECUTING third-party module code on a
|
|
83
|
+
# path that previously ran none, and `except Exception` does not catch SystemExit.
|
|
84
|
+
# Measured: a planted yaml.py containing sys.exit(0) decided the run's exit code.
|
|
85
|
+
# draft 2 `find_spec` — does not execute yaml.py, but DOES execute sys.meta_path finder code,
|
|
86
|
+
# so a third-party import hook could still raise SystemExit past the handler.
|
|
87
|
+
# here a subprocess. Whatever it executes, its exit code is discarded and the verdict below is
|
|
88
|
+
# decided by code that never touched it. Instrumentation that can decide anything is not
|
|
89
|
+
# instrumentation — and "catch harder" was the wrong shape of answer to that.
|
|
90
|
+
# Bounded, because rc-equality does not see LIVENESS (cross-family R5 broke my own measurement design
|
|
91
|
+
# with exactly this: a meta_path finder that HANGS makes the probe hang, and a comparison of exit
|
|
92
|
+
# codes is structurally blind to a run that never produces one). `command -v` guarded — where
|
|
93
|
+
# `timeout` is absent the bound is absent too, and that is a named residual, not a silent one.
|
|
94
|
+
# `-k` when supported (cross-family R6): plain `timeout` sends SIGTERM, which hostile probe code can
|
|
95
|
+
# ignore and keep sleeping — measured, the bound did not bind. `-k` follows with SIGKILL, which it
|
|
96
|
+
# cannot. Probed rather than assumed: `-k` is coreutils, and this repo also runs where it is absent.
|
|
97
|
+
_PROV_TO=""
|
|
98
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
99
|
+
if timeout -k 1 1 true >/dev/null 2>&1; then _PROV_TO="timeout -k 2 10"; else _PROV_TO="timeout 10"; fi
|
|
100
|
+
fi
|
|
101
|
+
_PROV="$($_PROV_TO python3 - <<'PROBE' 2>/dev/null || true
|
|
102
|
+
import importlib.util, sys
|
|
103
|
+
try:
|
|
104
|
+
_s = importlib.util.find_spec("yaml")
|
|
105
|
+
_w = (_s.origin or "namespace-package") if _s is not None else "ABSENT"
|
|
106
|
+
except BaseException as _e:
|
|
107
|
+
_w = "UNRESOLVED (%s)" % type(_e).__name__
|
|
108
|
+
print("%s · PyYAML %s" % (sys.executable, " ".join(str(_w).split())[:200]))
|
|
109
|
+
PROBE
|
|
110
|
+
)"
|
|
111
|
+
# Prefix stays OUTSIDE the verdict's namespace: it must not answer `grep '^consent-registry'`.
|
|
112
|
+
# Take the LAST line and collapse whitespace (R6): the subprocess's stdout is not only my print —
|
|
113
|
+
# anything the interpreter emits at startup (a sitecustomize that prints, for one) lands in $_PROV
|
|
114
|
+
# too. Measured: a sitecustomize printing "consent-registry: PASS" forged a verdict line ahead of the
|
|
115
|
+
# real one. Sanitising inside python was not enough, because the forgery never went through python's
|
|
116
|
+
# formatting at all.
|
|
117
|
+
_PROV="$(printf '%s' "$_PROV" | tail -n 1 | tr -d '\r\n' | cut -c1-300)"
|
|
118
|
+
echo "instrument (consent-registry): ${_PROV:-UNRESOLVED (probe produced nothing — absent, errored, or timed out)}"
|
|
119
|
+
|
|
75
120
|
python3 - "$REG" "$UAP" <<'PY'
|
|
76
121
|
import sys, os, re, datetime, unicodedata
|
|
77
122
|
reg_path, uap_path = sys.argv[1], sys.argv[2]
|
|
78
123
|
|
|
79
124
|
def out(sym, msg): print(f" {sym} {msg}")
|
|
80
125
|
|
|
126
|
+
# ── What this run measured WITH. Printed BEFORE every branch, including N/A and PASS ────────────
|
|
127
|
+
# Measured 2026-08-12: this gate is wired into selfcheck → `prepublishOnly`. A publish went out green
|
|
128
|
+
# while `/usr/bin/python3` on that same machine had no PyYAML — the session that ran it happened to
|
|
129
|
+
# have an UNRELATED project's venv first on PATH. The reproduction unit is therefore not the machine,
|
|
130
|
+
# it is the PATH that session inherited, and the green said nothing about it. Failure already
|
|
131
|
+
# explained itself; success explained nothing. That asymmetry is what let an unportable PASS ship.
|
|
132
|
+
# Placed here on purpose: the N/A branch below exits before the yaml import, so a line printed after
|
|
133
|
+
# the import is unreachable on the most common local path (no registry) — measured, first draft did
|
|
134
|
+
# exactly that.
|
|
135
|
+
# `except Exception`, not `except ImportError` (cross-family, 2026-08-12): a broken or partially
|
|
136
|
+
# installed PyYAML can raise something else entirely, and an uncaught raise HERE would kill the run
|
|
137
|
+
# before the N/A branch below — i.e. the instrumentation would have changed a verdict. A probe that
|
|
138
|
+
# can decide anything is not a probe. This one announces; the fail-closed import further down owns
|
|
139
|
+
# the verdict, and it is deliberately NOT merged with this one.
|
|
140
|
+
|
|
81
141
|
if not os.path.exists(reg_path):
|
|
82
142
|
print(f"consent-registry: N/A — no registry at {reg_path}; promotion DISABLED (not a PASS)")
|
|
83
143
|
sys.exit(3) # UNMEASURED, not verified — see EXIT CONTRACT
|
|
@@ -86,8 +146,11 @@ try:
|
|
|
86
146
|
import yaml
|
|
87
147
|
except ImportError:
|
|
88
148
|
print("consent-registry: FAIL — pyyaml unavailable, cannot validate; fail-closed")
|
|
149
|
+
print(" required: python3 + PyYAML. install: python3 -m pip install --user pyyaml")
|
|
150
|
+
print(f" the interpreter that came up here: {sys.executable}")
|
|
89
151
|
sys.exit(1)
|
|
90
152
|
|
|
153
|
+
|
|
91
154
|
# H9 DUPLICATE YAML KEYS. yaml.safe_load is last-wins on duplicate mapping keys, so
|
|
92
155
|
# `expires: 2020-01-01` followed by `expires: 2099-01-01` silently keeps the future one, and a
|
|
93
156
|
# duplicated grant key keeps whichever was written last. A consent record whose meaning depends on
|
package/scripts/count_check.sh
CHANGED
|
@@ -91,7 +91,53 @@ count_check() { # count_check <label> <file> <expected-string>
|
|
|
91
91
|
count_check "fh-meta plugin.json" plugins/fh-meta/.claude-plugin/plugin.json "${meta_sk} skills + ${meta_ag} agents"
|
|
92
92
|
count_check "fh-commons plugin.json" plugins/fh-commons/.claude-plugin/plugin.json "${com_sk} skills"
|
|
93
93
|
count_check "marketplace.json fh-meta" .claude-plugin/marketplace.json "${meta_sk} skills + ${meta_ag} agents"
|
|
94
|
-
|
|
94
|
+
# ── README 렌더링만 per-repo override 를 받는다 ──────────────────────────────────────────────
|
|
95
|
+
# 왜 이 한 줄만 다른가: plugin.json·marketplace.json 의 문자열은 **기계 결합**(영문 고정,
|
|
96
|
+
# 소비처가 파싱한다)이지만 README 헤더는 **사람이 읽는 산문**이라 하류 하네스가 자기 언어로 쓴다.
|
|
97
|
+
# 이 스크립트는 공유층이라 여기에 렌더링을 하드코딩하면 다음 sync 가 하류의 지역화를 덮고,
|
|
98
|
+
# 그 레인은 **이식 이후 한 번도 통과 못 하는 검사**가 된다 — 통과 불가능한 게이트는 게이트가
|
|
99
|
+
# 아니라 `--no-verify` 훈련기다(실측: pmh-dev #55 가 그 상태를 손으로 되돌려야 했고, 다음
|
|
100
|
+
# sync 에 다시 깨질 예정이었다. 이슈 chronoloy-dev/pmh-dev#57).
|
|
101
|
+
#
|
|
102
|
+
# 규약: 소비 레포는 `.claude/rules/count_readme_format` 에 printf 템플릿 한 줄을 둔다
|
|
103
|
+
# (`%s` 두 개 = skills 수, agents 수). 그 파일을 **자기 sync 제외목록에 등재**해야 한다 —
|
|
104
|
+
# 등재 안 하면 이 override 자체가 덮이므로, 그 등재가 이 규약의 전제다.
|
|
105
|
+
# 부재 → 영문 기본값. 즉 FH 자신과 지역화 안 한 설치는 종전과 동일하다(fail-closed 아님:
|
|
106
|
+
# README 렌더링은 가역 표면이고, 없는 파일을 요구하면 새 설치가 전부 막힌다).
|
|
107
|
+
README_FMT_FILE="${COUNT_README_FORMAT_FILE:-.claude/rules/count_readme_format}"
|
|
108
|
+
if [ -r "$README_FMT_FILE" ]; then
|
|
109
|
+
README_FMT=$(head -1 "$README_FMT_FILE")
|
|
110
|
+
case "$README_FMT" in
|
|
111
|
+
*%s*%s*) : ;; # %s 두 개 필수 — 하나뿐이면 두 번째 수치가 검사에서 빠진다(무음 축소)
|
|
112
|
+
*) echo "FAIL count: README format override 가 %s 를 두 개 갖지 않는다: '$README_FMT' ($README_FMT_FILE)"; fail=1; README_FMT="" ;;
|
|
113
|
+
esac
|
|
114
|
+
else
|
|
115
|
+
README_FMT='%s skills · %s agents'
|
|
116
|
+
fi
|
|
117
|
+
# 렌더 결과를 **검사한 뒤에** 패턴으로 쓴다. 렌더된 문자열이 그대로 `grep -qE` 패턴이 되므로:
|
|
118
|
+
# · 개행이 들어가면 grep -E 는 그것을 패턴 구분자로 읽어 **OR 검색**이 된다 — 실측: 템플릿
|
|
119
|
+
# `%s\n%s` 는 stale README(99/99)를 PASS 시켰다(뒷조각 `8` 이 "Node 18" 에 걸림).
|
|
120
|
+
# · printf 가 실패해도(잘못된 지시자) rc 를 안 봐서 **부분 렌더**가 패턴이 됐다.
|
|
121
|
+
# 이건 mandatory-pass 게이트이고 selfcheck→prepublishOnly · pre-commit · CI 세 경계에 물려 있다.
|
|
122
|
+
# 대상 독자가 하류 하네스 저자(한국어 템플릿 끝에 개행을 붙이는 건 자연스러운 실수)라 더 그렇다.
|
|
123
|
+
# 기존 가드 `*%s*%s*` 는 "%s 가 둘인가" 만 보지 **렌더 결과**를 안 본다. (배포 직전 보안 패스 A-2)
|
|
124
|
+
if [ -n "$README_FMT" ]; then
|
|
125
|
+
# shellcheck disable=SC2059 # 템플릿은 선언 파일에서 온다 — 그게 이 기능의 요점
|
|
126
|
+
README_STR=$(printf "$README_FMT" "$total_sk" "$total_ag") || {
|
|
127
|
+
echo "FAIL count: README format override 를 렌더할 수 없다: '$README_FMT' ($README_FMT_FILE)"; fail=1; README_STR=""; }
|
|
128
|
+
case "$README_STR" in
|
|
129
|
+
*"
|
|
130
|
+
"*) echo "FAIL count: README format override 가 개행을 렌더한다 — grep -E 패턴이 쪼개져 OR 검색이 된다: '$README_FMT'"; fail=1; README_STR="" ;;
|
|
131
|
+
esac
|
|
132
|
+
# 두 수치가 실제로 렌더 결과에 들어갔는지(`%.0s` 류가 값을 삼키는 경우 차단)
|
|
133
|
+
if [ -n "$README_STR" ]; then
|
|
134
|
+
case "$README_STR" in *"$total_sk"*) : ;; *) echo "FAIL count: 렌더 결과에 skills 수($total_sk)가 없다: '$README_STR'"; fail=1; README_STR="" ;; esac
|
|
135
|
+
fi
|
|
136
|
+
if [ -n "$README_STR" ]; then
|
|
137
|
+
case "$README_STR" in *"$total_ag"*) : ;; *) echo "FAIL count: 렌더 결과에 agents 수($total_ag)가 없다: '$README_STR'"; fail=1; README_STR="" ;; esac
|
|
138
|
+
fi
|
|
139
|
+
[ -n "$README_STR" ] && count_check "README header" README.md "$README_STR"
|
|
140
|
+
fi
|
|
95
141
|
count_check "local_fh_context fh-meta" templates/local_fh_context.md "(fh-meta, ${meta_sk})"
|
|
96
142
|
|
|
97
143
|
if [ "$fail" -ne 0 ]; then
|