@chrono-meta/fh-gate 1.4.96 → 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/CLAUDE.md +11 -4
- package/README.md +19 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +70 -0
- package/package.json +5 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/compaction_probe.sh +153 -10
- package/scripts/consent_registry_check.sh +63 -0
- package/scripts/lane_runner_check.sh +425 -0
- package/scripts/package_coverage_check.sh +182 -35
- package/scripts/psa_scan_lib.sh +164 -0
- package/scripts/public_surface_scan_files.sh +30 -0
- package/scripts/selfcheck.sh +332 -29
- package/scripts/test_consent_registry.sh +54 -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/templates/goal-quench-settings-merged.json +30 -0
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "fh-meta",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.97",
|
|
15
15
|
"description": "Hub meta-operations toolkit — 35 skills + 7 agents. New in 1.4.53: `fh-codex-doctor` (npm bin) — Codex adapter drift scanner; reads the documented M1/M2/M3 skill tier map + skill/agent source and reports codex-native/adapter-required/claude-native/unclassified per unit, wired into `npm test`/`prepublishOnly` (fail-closed on unclassified Claude-native primitives). New in 1.4.49: steel-quench gains Step 0.6 Verdict-Invariance Probe (groundedness axis — a load-bearing judged gate's verdict must track behavior, not rubric phrasing; measured flip-count over cross-family paraphrases; arXiv:2605.06161 Policy Invariance anchor); multi_model_sidecar_strategy §Vendor-native harness (a model is strongest in its own vendor CLI — Claude/CC, GPT/codex, Gemini/Antigravity; a universal router degrades all of them, so it stays an autocomplete/QA sidecar, never orchestration); predelete_check.sh fail-closed rewrite; memory-hygiene A-TMA anchor. New in 1.4.48: phantom-quench + steel-quench gain external frontier anchors (arXiv:2607.02052 package-hallucination; arXiv:2607.02057 prompt-coverage-adequacy); README model-flat claim reframed from a per-release point-curve to structural invariants (operation flattens across tiers; depth tier-order fixed within a generation). New in 1.4.47: onboarding step ① surfaces the Mode D companion-store session-start load in the auto-read salience anchor (previously only in the local binding + rules, so a greeting could skip the load). New in 1.4.46: context-doctor command-output axis (route to rtk/proxy for verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce envs). New in 1.4.41: context-doctor 2026 trigger vocab (context engineering/rot/collapse) + phantom-citation hardening; hub measurement-integrity-checklist (cross-model measurement pre-flight: display-name pin/reps≥3/discriminating probe). New in 1.4.40: install-wizard queryable-wiki scaffold (INDEX + session-start read + R/W/C ingest). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment) + video-ingest (capability-routed video ingestion). New in 1.4.x: verify-axis check-class taxonomy (mandatory-pass/measured/judged), no-reinvention Tier-0 inventory, 7-class failure taxonomy, Destructive-Op Gate, Wave-T (Temper), tier-floor governance, Mode D Model Notice, FC consent lane, default-Sonnet guidance. New in 1.3.0: public-surface-audit, field-harvest Mode B auto-trigger, 4-axis gate scope ext. Validated cross-CLI: Claude Code, Codex, Gemini.",
|
|
16
16
|
"source": "./plugins/fh-meta"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fh-commons",
|
|
20
|
-
"version": "1.4.
|
|
20
|
+
"version": "1.4.97",
|
|
21
21
|
"description": "Project-agnostic utility skills — 5 skills (convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate · ko-tech-writer) + 1 agent (quench-challenger). Domain-independent utilities transplantable into any project.",
|
|
22
22
|
"source": "./plugins/fh-commons"
|
|
23
23
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -254,9 +254,16 @@ into muscle memory and disarm it.
|
|
|
254
254
|
> `required_pull_request_reviews` present with `enforce_admins: true` — not something
|
|
255
255
|
> `required_status_checks` says anything about.
|
|
256
256
|
> ⚠️ A *different* residual on `main` is still real and must not be folded into the one just
|
|
257
|
-
> retracted
|
|
258
|
-
>
|
|
259
|
-
>
|
|
257
|
+
> retracted — but the residual's own description was itself stale and needed correction on
|
|
258
|
+
> 2026-08-12 (live re-check, `[[reference_github_protection_two_layers]]`): legacy
|
|
259
|
+
> `required_status_checks.contexts` is **`["validate"]`**, not `[]` — a green `validate` check IS
|
|
260
|
+
> required before a PR can merge, and `GET /rules/branches/main` carries no competing
|
|
261
|
+
> `required_status_checks` rule, so the legacy field is the effective one here. `validate`
|
|
262
|
+
> (`.github/workflows/validate.yml`) is a **separate job from Axis 1** (`regression-guard.yml`) —
|
|
263
|
+
> Axis 1 is still not required, see the 4-axis section below. The gap on `validate` is
|
|
264
|
+
> `strict: false`: that check re-runs on every push to the PR branch, but nothing re-forces it
|
|
265
|
+
> against a **moving** main after it last ran — so a check that passed can still land behind
|
|
266
|
+
> concurrent merges it never saw.
|
|
260
267
|
|
|
261
268
|
## Permission-Denial Guidance (When Auto-Mode Blocks an Action)
|
|
262
269
|
|
|
@@ -324,7 +331,7 @@ Every new `SKILL.md` must clear a **6-item bar** (role-duplication via `/asset-p
|
|
|
324
331
|
|
|
325
332
|
**FH 자산을 수정하면**(SKILL.md · **SKILL_detail.md** · `.claude/rules/*.md` · `knowledge/shared/rules/*.md` · `templates/` · `CLAUDE.md` · substantive `knowledge/`·`docs/*.md` · `AGENTS.md` · **`scripts/**/*.sh`** · **에이전트 정의**(`plugins/*/agents/**/*.md` · `.claude/agents/**/*.md`)) **4축 검증 체인이 그 세션 첫 커밋 전에 자동 실행된다.** 사용자 요청 불요 — 제안이 아니라 의무 단계다.
|
|
326
333
|
|
|
327
|
-
**기계층 — 무엇이 실제로 막는지 나눠서 말한다(2026-08-04 실측).** `git commit` 은 `templates/.git-hooks/pre-commit` 이 **하드 차단**한다: 축이 전부 PASS 할 때까지 커밋이 안 되고, 아래 상세가 로드되지 않아도 훅이 막는다 — 이 산문은 훅 위의 살리언스 층이지 유일 floor 가 아니다. 다만 **훅은 클라이언트측이고 `--no-verify` 로 우회된다**(§Integration branch 가 이미 그렇게 말한다 — 두 곳이 어긋나 보이던 것을 여기서 맞춘다). **그리고
|
|
334
|
+
**기계층 — 무엇이 실제로 막는지 나눠서 말한다(2026-08-04 실측).** `git commit` 은 `templates/.git-hooks/pre-commit` 이 **하드 차단**한다: 축이 전부 PASS 할 때까지 커밋이 안 되고, 아래 상세가 로드되지 않아도 훅이 막는다 — 이 산문은 훅 위의 살리언스 층이지 유일 floor 가 아니다. 다만 **훅은 클라이언트측이고 `--no-verify` 로 우회된다**(§Integration branch 가 이미 그렇게 말한다 — 두 곳이 어긋나 보이던 것을 여기서 맞춘다). **그리고 서버측 검증엔 남은 잔여가 있다(2026-08-12 재확인 — `contexts=[]` 서술은 stale, 정정됨)**: `main` 은 `enforce_admins: true` 로 **푸시 경로**(PR 경유)를 강제하고, legacy `required_status_checks.contexts` 는 **`["validate"]`** — `validate` 잡(`.github/workflows/validate.yml`, 메타데이터·`selfcheck.sh` 배선 레인)이 실제 **필수 체크**다. ⚠️ **`validate` 는 Axis 1 이 아니다** — Axis 1(`regression-guard.yml` → `templates/regression_guard.sh`)은 여전히 필수 체크가 **아니고**, 그 워크플로의 `paths:` 필터가 `SKILL.md`·`.claude/rules/*.md`·`CLAUDE.md`·`templates/*.md` 만 보므로 이 절이 4축 대상으로 나열한 `knowledge/shared/rules/*.md`·`docs/*.md`·`AGENTS.md`·`scripts/**/*.sh`·에이전트 정의·`SKILL_detail.md` 만 바뀐 PR 에는 **Axis 1 자체가 돌지도 않는다**. `validate` 쪽 남은 갭은 `strict: false`: 그 체크는 PR 브랜치에 푸시할 때마다 재실행되지만(오픈 시점 한정이 아니다), 그 뒤 main 이 움직여도 재검증을 강제하지 않으므로 **초록으로 남아 있는 체크가 실제로 병합되는 최신 트리를 본 적이 없을 수 있다.** 즉 서버가 강제하는 건 *체크가 초록인가*지 *그 체크가 지금의 main 을 봤는가*가 아니다. Axes 2–3(마커)·Axis 4(매니페스트)는 그 파일들이 `tracks/**` 로 gitignored 라 CI 가 **구조적으로 볼 수조차 없다**. 정직한 표현은 "하드 차단"이 아니라 **"가용한 가장 강한 층"**이다. **미해결 잔여**: `strict` 를 켜는 것도, Axis 1 을 필수 체크로 걸거나 그 `paths:` 를 넓히는 것도 운영자 결정이다(막 flaky 레인을 하나 기록한 참이라, 과차단이 override 를 습관화시키는 쪽으로 기울 수 있다).
|
|
328
335
|
|
|
329
336
|
> **상세 정본**: `.claude/rules/fh_4axis_gate.md` — 4축 정의·마커 필수 필드·경량 예외·substantive carve-out·target-tier sim 게이트·Mode D 모델 공지·cross-family 보완. **`paths:` 로 *일부* FH 자산 경로에 스코핑돼 있어 그 파일들을 *읽을 때* 자동 로드된다 — 로드 조건이지 게이트 적용 범위가 아니다** (공식 트리거는 read — `code.claude.com/docs/en/memory.md` §Path-specific rules).
|
|
330
337
|
> (2026-07-20 분리. **파일 char 실측**: 이 절 자체가 76,706자 중 **10,331자(13.5%)**로 단일 최대였다. 그 분리 + 같은 세션의 중복 3건 제거 + New-Skill 게이트 편입까지 **합산**해 파일은 **76,706 → 67,611 (순감 9,095자, 11.9%)** — 합산치이지 이 절 하나의 성과가 아니다 — 이건 파일 크기지 `/context` 상주 실측이 아니다(계기≠대상, [[feedback_resident_memory_measured_fresh_toplevel]]: 상주는 톱레벨 새 세션 `/context` 로만 잰다 — 미측정). 트리거가 *파일*이고 *기계 백스톱*이 있어 1순위 후보였다. 같은 이유로 **비가역 게이트 3종은 이동 불가** — 의도 트리거라 경로 스코핑하면 fail-open 이 된다.)
|
package/README.md
CHANGED
|
@@ -56,6 +56,25 @@
|
|
|
56
56
|
|
|
57
57
|
**Prerequisite**: Claude Code CLI — verify with `claude --version`
|
|
58
58
|
|
|
59
|
+
<details><summary><b>Optional: one gate needs Python + PyYAML</b> — <code>npm test</code> is red without it</summary>
|
|
60
|
+
|
|
61
|
+
The consent-registry gate parses YAML, and it **fails closed** when it cannot — correctly, since an
|
|
62
|
+
unvalidated consent record must not read as a clean one. But that fail-closed turns the whole of
|
|
63
|
+
`npm test` (and `prepublishOnly`) red on a machine without PyYAML, and until 2026-08-12 the
|
|
64
|
+
requirement was written down **nowhere**:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python3 -m pip install --user pyyaml # verify: python3 -c 'import yaml; print(yaml.__version__)'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Why this is called out rather than left implicit: a release once shipped green from a session whose
|
|
71
|
+
`python3` happened to resolve to an **unrelated project's virtualenv** that had PyYAML, while the
|
|
72
|
+
machine's own `python3` did not. The gate was never bypassed — it passed, and the pass simply was not
|
|
73
|
+
portable. Every verdict from that gate now prints the interpreter and PyYAML version it used, so a
|
|
74
|
+
green states what produced it instead of leaving the reader to assume.
|
|
75
|
+
|
|
76
|
+
</details>
|
|
77
|
+
|
|
59
78
|
```bash
|
|
60
79
|
# 1. Install the plugin
|
|
61
80
|
claude plugin marketplace add https://github.com/chrono-meta/forge-harness.git
|
|
@@ -1872,3 +1872,73 @@
|
|
|
1872
1872
|
outcome: accepted
|
|
1873
1873
|
evidence: "R1(diff 축) 1S·2A·5B → 전부 **재현 후** 수리 6건, B2 만 근거 명시 수용. 주장 축 10개 전부 TRUE·FALSE 0(명령+컨트롤 동반). 전량 2956 passed/43 skipped · 라이브 known-pair 유지(생성 TC 가 손 미션과 **grounds 바이트 동일**). PR qasp-dev #156"
|
|
1874
1874
|
notes: "★**Explore 축이 인수인계의 전제를 반증했다** — 11c §4 「page↔area 를 선언 채널에 얹으면 NO_MATCH 6건이 열린다」가 틀렸다. 페이지 토큰은 매칭 키가 아니고(surface_inventory 에 `page` 0회, control `route` 36회), 같은 «p3 영역» 토큰인 TC_MT_005 는 매칭 성공·TC_MT_019 는 NO_MATCH 다. 나는 **그 위에 스키마를 이미 얹은 뒤** 실측했다 — 「남이 준 다음 할 일」도 전제부터 재야 하고, 특히 범위를 **줄이는** 방향일 때 소스 그라운딩이 제일 허술해진다([[feedback_scope_widening_needs_grounding]] 의 반대편 얼굴). 🟥 **선언 채널이 자기 실패모드를 맞았다** — 「선언 파일을 줬는데 선언이 없으면」 3가지 입력 전부 rc=0 으로 조용히 통과, cross-family 가 잡았고 자력 적발 0. 🟥 **되돌림 프로브가 내 테스트 하나를 장식으로 적발** — origin 을 spec_dictionary 로 위조해도 62건 전건 초록이었다(`anchors: []` 라 qualify_anchor 가 origin 을 보기도 전에 빠진다). 「자격이 안 나온다」를 확인했지 「origin 게이트가 산다」를 확인한 게 아니었다. 🟥 **계기가 세 번 거짓말했다**: `tail -1` 이 요약 대신 DeprecationWarning · 파이프 뒤 `$?` 가 필터 상태(훅이 잡아줌) · **pytest 색상 escape 때문에 `1 failed, 71 passed` 를 「71 passed」로 읽음**. 셋째는 실제 실패 1건을 숨겼다 — 전량 판정은 `--color=no` 필수. ★innovator 리포트의 B6(「처방 전사 검사 없음」)은 **오판** — 그 에이전트가 테스트 파일을 안 읽었다. 해당 레인은 존재하고 되돌림에서 2 적색. 남의 리포트도 대조 대상이다"
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
- date: 2026-08-12
|
|
1878
|
+
session: ifkakao-deck-notes-parallel
|
|
1879
|
+
agents_summary: "3 dispatches: general-purpose×3 — ⓐ 챔버 런 원장 1차기록 재측정(승격 후보) · ⓑ 창작 도메인 «죽인 것» 1차기록 대조(승격 후보) · ⓒ 창작 대체안 B 5건 커밋 diff 직독"
|
|
1880
|
+
dispatch_count: 3
|
|
1881
|
+
outcome: accepted
|
|
1882
|
+
evidence: "세 리포트의 load-bearing 주장을 거버너가 **전건 원문·컨트롤로 재검증**했다.
|
|
1883
|
+
ⓐ 런#10 판정문:31 «10런 중 9 KILL» 오기 확인 · 디렉터리 9 ↔ 판정파일 9 대응 ·
|
|
1884
|
+
`chamber_ordering_witness.yaml` 부재(컨트롤 = 같은 디렉터리 `subagent_invocations_log.yaml` 존재) ·
|
|
1885
|
+
#5 «5/5 false-positive on 111 real source files» 축자 확인.
|
|
1886
|
+
ⓑ `codex_decorrelation_audit_2026-07-25.md:14` 축자 · 신호 `:52` «독트린화는 여전히 보류» ·
|
|
1887
|
+
`tracks/noblewriter/` 대상 grep rc=1 + **컨트롤 rc=0(10파일)**.
|
|
1888
|
+
ⓒ `fh_completed_2026-08-01.md:76` 축자 · 캐논 검사기 부재(target rc=1, control rc=0) ·
|
|
1889
|
+
커밋 `7046798` diff 축자.
|
|
1890
|
+
산출: 원장 승격 1(인큐베이션) · 기각 2(창작 A·B) · 발표 출하 1장(S18-b)"
|
|
1891
|
+
notes: "★**ⓒ가 내 중계 오류를 반증했다 — 이 세션 최대 소득이고 자력 적발 0이다.**
|
|
1892
|
+
ⓑ의 요약을 받아 «typed 캐논이 저자를 잡은 3건»으로 운영자에게 **원문을 안 열고 중계**했는데,
|
|
1893
|
+
요약 로그 원문은 그중 **둘을 «운영자 캐치»로 명시**한다 — 게이트가 *놓친* 것이라 실적으로 세면
|
|
1894
|
+
논지가 뒤집힌다. 「대화에서 말한 것도 publish 다」(§Instrument-Calibration publish-order)가
|
|
1895
|
+
always-loaded 인데도 뚫렸고, 잡은 건 후속 격리 에이전트다.
|
|
1896
|
+
★**세 리포트 모두 «부재»를 주장할 때 컨트롤을 스스로 동반했다** — 브리프에 명시한 결과이고,
|
|
1897
|
+
ⓒ는 첫 컨트롤 실행이 파이프 오염($? 가 head 를 읽음)이었음을 **스스로 적발**해 rc 캡처로 재실행했다.
|
|
1898
|
+
★**ⓐ가 손 집계와 자동 grep 이 갈리는 지점을 지목**: `grep -c 'EMIT|KILL'` 은 27/43 을 뱉는데
|
|
1899
|
+
그건 파일 내 토큰 계수이지 런별 판정이 아니다(#2·#3 이 `KILL / PARTIAL-EMIT` 형태).
|
|
1900
|
+
★**분모가 셋 다 방어 가능하다는 것을 ⓐ가 먼저 말했다** — 원장 행 전체(10, #1 오염) vs
|
|
1901
|
+
완주 런(9, 권고) vs 아티팩트 완비(8). 어느 걸 쓰는지가 정직성 갈림길이라는 지적이 그대로
|
|
1902
|
+
발표 인용 형태 결정(계수 대신 한 건)의 입력이 됐다.
|
|
1903
|
+
🟥 **리포트를 그대로 옮기지 않은 것이 세 번 다 옳았다** — 세 건 모두 거버너 재검증에서
|
|
1904
|
+
라벨/귀속이 교정됐다(ⓐ 「첫 정식 런 #10」 기계 미판별 · ⓑ 도메인 라벨 · ⓒ 캐치 주체)."
|
|
1905
|
+
|
|
1906
|
+
- date: 2026-08-12
|
|
1907
|
+
agent: general-purpose
|
|
1908
|
+
task: "qasp 상류 라우팅 축 — 1.5막 질문 게이트 + 이관 처분 상태 신설 (설계·구현·cross-family 수렴·PR·머지까지 완주)"
|
|
1909
|
+
dispatcher: FH 세션 (qasp+psa 축)
|
|
1910
|
+
tier: opus-5[1m]
|
|
1911
|
+
duration: "약 3.5시간 (bg, 재개 2회)"
|
|
1912
|
+
outcome: accepted
|
|
1913
|
+
evidence: "qasp-dev PR #157 MERGED (squash → main 8ff549e) · 파일 19 · +2735/-26 · CI 4/4 SUCCESS · 회귀 3043 passed/43 skipped (기준선 2967 대비 +76) · cross-family 7R CONVERGED(codex/gpt-5.5). 자기 반증 5건을 스스로 보고했고 그중 하나(자리표시자 18→17)는 내 독립 측정과 일치했다. 전수 되돌림 프로브로 **자기 수리 24종 중 4건이 장식**임을 스스로 적발. 시험관(나)의 몫은 메타감사·정본 지시·머지 조건(사설 companion-store 매니페스트) 강제였고, 실제 설계·구현·수렴은 에이전트가 완주했다."
|
|
1914
|
+
note: "④-e 훅은 이 세션에서 47 dispatch 를 셌는데, 그중 대부분은 이 에이전트 자신의 하위 디스패치(cross-family 7R + 되돌림 프로브)다. 한 클래스로 묶어 1 엔트리로 기록한다 — 규율이 허용하는 형태이고 총 미기록을 막는 것이 훅의 목적이다."
|
|
1915
|
+
|
|
1916
|
+
- date: 2026-08-12
|
|
1917
|
+
invoker: FH 재출하 축 병렬세션 (Opus 5 1M)
|
|
1918
|
+
subagent: codex/gpt-5.5 (cross-family 사이드카, headless `codex exec -m gpt-5.5 -`)
|
|
1919
|
+
count: 6
|
|
1920
|
+
purpose: v1.4.96 npm publish 게이트 — 독립검증. 채널 3종으로 나눠 발주(diff 리뷰 · 클레임 13건 검증 · 동결 게이트리뷰 4회)
|
|
1921
|
+
outcome: accepted
|
|
1922
|
+
evidence: "S 22건 회수 중 실건 19 수리 · 2건은 기계로 반증(count_check 이스케이프 known-pair · 되돌림 프로브 2pass/3fail) · 1건 미수리 잔여(B, 의도). 실건 벡터 3→6→5→2→3. **자력 적발 0 이었던 결함 5건을 잡았다** — 그중 3건이 직전 라운드 수리 산물. 레인 20→46 은 전부 이 회수분의 앵커다."
|
|
1923
|
+
cost_note: "1라운드 killed(내가 레인 전수 스윕을 시켜 시간초과 — 발주자 결함, 범위 축소 후 재주행). 1라운드 절반 무효(WRONG-TARGET — 발주 후 트리 수정)."
|
|
1924
|
+
|
|
1925
|
+
- date: 2026-08-12
|
|
1926
|
+
invoker: FH ifkakao 병렬세션 (Sonnet 5 → Opus 5 중도 전환)
|
|
1927
|
+
subagent: fh-commons:quench-challenger (Axis 2 적대검증, in-session)
|
|
1928
|
+
count: 1
|
|
1929
|
+
purpose: CLAUDE.md §257·§331 stale 브랜치보호 서술 정정본의 사실성·내부정합 검증
|
|
1930
|
+
outcome: accepted
|
|
1931
|
+
evidence: "S 1건 + A 2건 회수, 전부 실건이고 **자력 적발 0**. S = `validate` 필수체크를 «Axis 1 워크플로»로 오귀속(실제로는 별개 잡 — regression-guard.yml 은 여전히 필수 아니고 paths: 필터로 4축 대상 자산 다수에 돌지도 않는다). A1 = «PR 오픈 시점» 과잉주장(실제 synchronize 마다 재실행). A2 = «both layers» 근거 미인용. 셋 다 수정 후 워크플로 YAML + 라이브 gh api 재대조. PR #354 머지(main f62a5a9)."
|
|
1932
|
+
note: "정정이 원 서술보다 더 틀릴 수 있다는 사례 — stale 한 것은 `contexts=[]` 라는 «목록»이었지 «Axis 1 이 필수가 아니다»라는 «귀속»이 아니었는데, 초안이 후자까지 지웠다. 적대검증이 그 구분을 복원했다."
|
|
1933
|
+
|
|
1934
|
+
- date: 2026-08-12
|
|
1935
|
+
session: reship-residual-A-A2-night (Sonnet 5 → Opus 5 1M 전환)
|
|
1936
|
+
agents_summary: "3 dispatches: fh-commons:quench-challenger×1 (PR #355 diff 6축 구조공격) · codex gpt-5.5×1 (같은 diff cross-family, Bash 사이드카) · fh-meta:persona-innovator×1 (Mode F 자율주행 — 운영자 지시)"
|
|
1937
|
+
dispatch_count: 3
|
|
1938
|
+
outcome: accepted
|
|
1939
|
+
evidence: "challenger = A 4건 + B 3건 회수, 그중 최상위(vendored-.git 트리에서 원래 버그 재현)가 첫 수리를 반증해 일반해(선언 조회)로 방향을 바꿨다. codex = 독립으로 같은 B 2건에 수렴(stale SKIP 문구 · 모드 enum 미검증), S/A 0 보고하며 «패딩 대신 없다고 말한다»를 실제로 이행. innovator Mode F = 내부 갭 5 + 외부 프런티어 6(전부 fetch 된 URL) + null result 5, **내 주장 2건 반증**(N≥3 오계상 · 주석 방향). 산출은 PR #355·#356·#357 (main b80ed97) + 카드 §🔱⑳."
|
|
1940
|
+
notes: "★**세 디스패치 중 둘이 내 판정을 뒤집었고 자력 적발은 0이다.** challenger 가 내 첫 수리(`[ -e .git ]`)를 «좁은 문 뒤에서 원래 버그가 재현된다»로 반증했고, innovator 가 내 N≥3 정당화를 **이 레포 자신의 판별자**(첫 발생 직후 고쳤다면 나머지가 막혔겠는가)로 반증했다 — 첫 수리가 두 번째 발견을 *만든* 헬퍼라 한 발견이지 두 재발이 아니고, compaction_probe 수리는 반대 방향이었다. 정직한 값 N=1–2, 임계 미만. **재발 횟수가 아니라 측정된 12건이 이 빌드의 근거**로 헤더를 고쳐 적었다.
|
|
1941
|
+
★**innovator 가 자기 계수를 스스로 UNCALIBRATED-partial(2/16 손검증)로 라벨**했고, 내 전수 재계상은 18 이었다 — 정직 라벨이 있었기에 그 숫자로 작업 크기를 산정하지 않았다.
|
|
1942
|
+
★**H1 규율 이행 확인**: innovator 가 벤더 마케팅 출처의 수치 2건(cross-family 검출률 +40~60% · 공유맥락 온보딩 -35~50%)을 SPECULATIVE 로 격리하고 **어느 제안에도 인용하지 않았다**. 대신 «우리 자체 증거가 더 강하다 — 약한 외부 숫자로 강한 내부 실측을 장식하지 마라»를 명시.
|
|
1943
|
+
🟥 **codex 발주 비용 관측**: diff + 산문 프롬프트 1회로 충분했다(오전 축의 7회 352KB 대비). 같은 클래스 결함을 같은 채널로 두 번 보내지 않은 것이 차이다.
|
|
1944
|
+
⚠️ **goal-quench 미발화** — 사이드카 3회 미달(2회)이라 카드 K 의 트리거 조건(3회 초과)엔 안 걸렸다. 이건 규율 준수이지 갭이 아니다."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrono-meta/fh-gate",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.97",
|
|
4
4
|
"description": "FH runtime adapters — run FH governance, skills, and agents via Claude or Codex with machine-parseable gates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"prepare": "chmod +x bin/fh-gate.js bin/fh-run.js bin/fh-goal.js bin/fh-codex-doctor.js scripts/fh-gate.sh scripts/fh-run.sh scripts/fh-goal.sh",
|
|
28
28
|
"test": "bash scripts/selfcheck.sh",
|
|
29
|
-
"prepublishOnly": "bash scripts/version_lockstep_check.sh && bash scripts/selfcheck.sh && bash scripts/public_surface_scan_files.sh",
|
|
29
|
+
"prepublishOnly": "bash scripts/version_lockstep_check.sh && bash scripts/selfcheck.sh && bash scripts/package_coverage_check.sh --vs-tarball && bash scripts/public_surface_scan_files.sh",
|
|
30
30
|
"release": "bash scripts/public_surface_scan_files.sh && npm publish"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"scripts/test_selfcheck_state_lanes.sh",
|
|
69
69
|
"scripts/test_version_lockstep_lanes.sh",
|
|
70
70
|
"scripts/package_coverage_check.sh",
|
|
71
|
+
"scripts/lane_runner_check.sh",
|
|
71
72
|
"scripts/test_package_coverage_lanes.sh",
|
|
72
73
|
"scripts/test_fh_gate_regressions.sh",
|
|
73
74
|
"templates/local_fh_context.md",
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"templates/temper_check.sh",
|
|
84
85
|
"templates/contrib_session.md",
|
|
85
86
|
"templates/goal-quench-hook-setup.md",
|
|
87
|
+
"templates/goal-quench-settings-merged.json",
|
|
86
88
|
"templates/.claude/rules/session.md",
|
|
87
89
|
"scripts/below_floor_scan.sh",
|
|
88
90
|
"scripts/capability_registry_check.sh",
|
|
@@ -128,6 +130,7 @@
|
|
|
128
130
|
"scripts/gate_pathspec_check.sh",
|
|
129
131
|
"scripts/prepush_guard_check.sh",
|
|
130
132
|
"scripts/psa_scan_lib.sh",
|
|
133
|
+
"scripts/test_psa_singlefile_lanes.sh",
|
|
131
134
|
"scripts/session_close_check.sh",
|
|
132
135
|
"scripts/utterance_landing_check.sh",
|
|
133
136
|
"scripts/test_dispatch_log_lanes.sh",
|
|
@@ -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
|