@chrono-meta/fh-gate 1.4.89 → 1.4.91
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/judgment_circuits.txt +14 -0
- package/.claude/rules/fh_4axis_gate.md +7 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +25 -0
- package/CLAUDE.md +202 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
- package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
- package/package.json +23 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
- package/scripts/branch_claim.sh +266 -0
- package/scripts/chamber_run.sh +64 -2
- package/scripts/chamber_witness.sh +439 -0
- package/scripts/compaction_probe.sh +456 -0
- package/scripts/digest_landing_check.sh +385 -0
- package/scripts/directional_diff_gate.sh +459 -0
- package/scripts/fh_env_delta_scan.sh +15 -0
- package/scripts/fh_session_load.sh +34 -1
- package/scripts/field_canon_preload.sh +129 -0
- package/scripts/hook_source_lib.sh +41 -0
- package/scripts/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +106 -0
- package/scripts/session_close_check.sh +134 -1
- package/scripts/test_branch_claim_lanes.sh +231 -0
- package/scripts/test_dispatch_log_lanes.sh +35 -1
- package/scripts/test_field_canon_lanes.sh +142 -0
- package/scripts/test_hook_source_gate_lanes.sh +81 -0
- package/scripts/test_marker_crossfamily_lanes.sh +132 -0
- package/scripts/test_marker_floor_lanes.sh +9 -8
- package/scripts/test_relay_channel_lanes.sh +583 -0
- package/scripts/test_reviewer_capability_conformance.sh +173 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/utterance_landing_check.sh +209 -0
- package/templates/.git-hooks/pre-commit +297 -13
- package/templates/settings.Compaction.snippet.json +56 -0
- package/templates/settings.FieldCanon.snippet.json +51 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Multi-Session Close Protocol — 같은 하네스에서 둘 이상이 돌 때의 마감
|
|
2
|
+
|
|
3
|
+
> **왜 여기 있나.** 이 프로토콜은 2026-08-09 까지 **세션 카드에만** 있었다. 카드는 gitignored 이고
|
|
4
|
+
> **세대마다 통째로 덮인다** — 그날 실제로 이월 8건이 그렇게 사라졌고, 하필 그중 하나가
|
|
5
|
+
> *"⑤-c 발화 착지 검증"*(카드가 내용 착지를 안 본다는 걸 고치려던 항목)이었다. 규율을 담은 곳이
|
|
6
|
+
> 규율이 막으려던 방식으로 유실됐다. **그래서 tracked 로 옮겼다.** 이건 승격이 아니라 이사다.
|
|
7
|
+
>
|
|
8
|
+
> **범위**: FH 자신의 다축 마감. `templates/` 로 전파하지 않는다 — 필드 하네스는 대개 단일
|
|
9
|
+
> 세션이라 아직 필요가 없고, 전파는 그쪽에서 병렬이 실제로 돌기 시작할 때다.
|
|
10
|
+
|
|
11
|
+
## 0. 전제 — 두 가지 형상을 구분한다
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
거버너 + 위임 한 세션이 카드를 쥐고 나머지는 그 세션이 띄운 에이전트
|
|
15
|
+
→ 카드 소유가 명확하고 last-writer 경쟁이 없다. **기본형**
|
|
16
|
+
대등 peer 독립 세션 둘 이상이 각자 마감한다
|
|
17
|
+
→ 아래 전부가 필요해진다. **다른 레포일 때만 권장**
|
|
18
|
+
```
|
|
19
|
+
2026-08-09 의 2축이 안전했던 이유는 모델이 좋아서가 아니라 **두 축이 다른 레포**였기 때문이다
|
|
20
|
+
(`forge-harness` / `qasp-dev`). 같은 워킹트리를 공유하는 대등 peer 는
|
|
21
|
+
`[[feedback_shared_checkout_ops_touch_others_work]]` 를 정면으로 부른다.
|
|
22
|
+
|
|
23
|
+
## 1. 순서 — 질문이 재대조보다 **앞이고 더 넓다**
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
① peer 가 자기 델타를 append (자기 소유 파일에만. 아래 §4)
|
|
27
|
+
② 핑
|
|
28
|
+
③ ★ 접기 전에 「지금부터 마감이다, 더 있나」를 **묻는다**
|
|
29
|
+
④ 접는다 (⑤ 원자 실행: 로그 → 카드)
|
|
30
|
+
⑤ `gh pr list --merged` 재대조
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**③이 ⑤로 대체되지 않는다.** 재대조는 *내가 아는 축*만 훑고, 질문은 *상대가 아는 축*을 가져온다.
|
|
34
|
+
2026-08-09 실측: 그 질문이 델타 8건을 꺼냈고 **PR 이 없어 `gh pr list` 로 구조적으로 안 잡히는
|
|
35
|
+
것들**이 다수였다(스펙/설계만 바뀐 것 · PR 이전 브랜치 커밋 · 외부 승인 대기 · 이월 누락).
|
|
36
|
+
|
|
37
|
+
## 2. 세 가지를 각각 못 믿는다
|
|
38
|
+
|
|
39
|
+
### 2-a. 보냈다 ≠ 닿았다
|
|
40
|
+
크로스세션 메시지는 **수신자 사용자 승인 대기로 보류**될 수 있고, 그래도 발신 호출은 성공을
|
|
41
|
+
반환한다. 같은 날 카드는 이미 「5곳 전부 통지」라고 적고 있었다.
|
|
42
|
+
|
|
43
|
+
⚠️ **만료 통지는 계기끼리 어긋났다 — 한쪽으로 단정하지 않는다.** 보류 2건 중 하나에 대해
|
|
44
|
+
「승인 없이 만료 — 전달되지 않음」 통지가 왔는데, **운영자 확인으로는 도달해 있었다.**
|
|
45
|
+
어느 쪽이 맞는지 이 세션은 못 가른다. 그러니 **«만료 = 영구 미도달» 로 적지 마라**(초안이 그렇게
|
|
46
|
+
적었다가 반증됐다). 확실한 것은 하나뿐이다 — **발신 성공은 도달의 증거가 아니고, 통지도 아니다.**
|
|
47
|
+
도달을 확인하는 유일한 방법은 **답이 오는 것**이다.
|
|
48
|
+
→ **보낸 수가 아니라 답 온 수를 적고, 미회신 수를 카드에 명시한다.** 기계화 불가(스크립트는 회신을
|
|
49
|
+
못 본다) — `session_close_check.sh` ①-c 가 이 문장을 살리언스로 띄운다.
|
|
50
|
+
|
|
51
|
+
### 2-a-2. 탐지는 넓게, **통지는 좁게** — 둘을 같은 목록으로 쓰지 마라
|
|
52
|
+
2026-08-09 의 오발신은 탐지가 넓어서가 아니라 **탐지 목록을 그대로 수신자 목록으로 썼기** 때문이다
|
|
53
|
+
(원래 알려야 할 상대는 1곳이었다). 두 목록은 목적이 다르다:
|
|
54
|
+
```
|
|
55
|
+
탐지 넓게 — 살아있는 것 전부. 여분이 «마감 미완» 이라는 신호다 (§2-b)
|
|
56
|
+
통지 좁게 — **지금 돌아가는(working/busy) 세션에만.** 끝난 세션에 보내는 건 소음이고,
|
|
57
|
+
수신자 승인을 태우며, 상대 기록에 남지도 않는다
|
|
58
|
+
```
|
|
59
|
+
**운영자 지시(2026-08-09)**: 「지금 돌아가는 동일 하네스 세션에만 보내라」.
|
|
60
|
+
|
|
61
|
+
### 2-b. 살아있다 ≠ 일하는 중이다 · 그리고 그 여분이 신호다
|
|
62
|
+
소켓이 살아있으면 **프로세스**가 살아있는 것이지 세션이 활성인 게 아니다. **판별 기준을 정확히
|
|
63
|
+
적는다**: 소켓 파일명의 PID 에 프로세스가 살아있고 그 cwd 가 이 하네스면 잡힌다 — 그게 전부다.
|
|
64
|
+
|
|
65
|
+
그래서 **세 가지가 섞여 들어오고, 통지가 필요한 건 첫째뿐이다**(2026-08-09 실측):
|
|
66
|
+
```
|
|
67
|
+
working 세션 → 접기 전에 물어라
|
|
68
|
+
놀고 있는 터미널 → 진짜 세션이지만 프롬프트에서 대기 중. 보통 건너뛴다
|
|
69
|
+
남은 프로세스 → 마감이 안 끝난 것 (1회 관측, 손으로 죽여야 했다)
|
|
70
|
+
예열 워커(bg-spare) → **세션이 아예 아니다.** args 로 기계 제외 — 안 걸러내면 순수 오탐
|
|
71
|
+
```
|
|
72
|
+
⚠️ **초판은 여분 전체를 «마감 미완» 이라고 적었다. 한 사례에서 과일반화한 것이고 틀렸다** —
|
|
73
|
+
실제 다수는 예열 워커와 놀고 있는 터미널이었다. **셋을 가르는 건 fleet 뷰뿐이고, 이 스캔은 못 한다.**
|
|
74
|
+
⚠️ **job 상태파일로 좁히지 마라 — 반대로 틀린다.** 같은 날 실측: 상태파일이 **돌고 있는 세션을
|
|
75
|
+
`done` + `firstTerminalAt` 로 표시**했다. 소켓이 넓은 만큼 상태파일은 좁다. **교차 판독.**
|
|
76
|
+
|
|
77
|
+
### 2-c. 닫혔다 ≠ 안 열렸다
|
|
78
|
+
「한 번도 안 닫힘」과 「닫고 또 일함」은 **밖에서 동일하게 Completed** 로 보이는데 처방이 반대다
|
|
79
|
+
(전자는 마저 닫기, 후자는 다시 접기). `session_close_check.sh` ①-d 가 마감 게이트 통과를 세서
|
|
80
|
+
2회 이상이면 알린다. **자기신고다** — 패턴을 감지하지 증명하지 않는다.
|
|
81
|
+
|
|
82
|
+
## 3. 세대 간 승계 — 동료-질문으로는 구조적으로 안 잡힌다
|
|
83
|
+
|
|
84
|
+
§1의 질문은 **동료 세션**을 향한다. **직전 세대 카드 자신의 이월분**은 그 질문의 사정거리 밖이다.
|
|
85
|
+
2026-08-09 실측: 그렇게 사라진 이월이 8건이었고 파일은 전부 살아 있었다 — 소실이 아니라
|
|
86
|
+
**카드에서만 안 잡히던 상태**.
|
|
87
|
+
|
|
88
|
+
→ **카드를 재작성할 때 이전 카드의 이월 블록을 grep 대조한다.** 새로 쓰는 게 아니라 **덜어낸 것을
|
|
89
|
+
확인하는** 절차다.
|
|
90
|
+
|
|
91
|
+
## 4. 쓰기 규율 — 공유 자원은 append 도 충돌한다
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
✅ 자기 소유 파일 · 자기 섹션에만 append → **즉시 커밋**
|
|
95
|
+
❌ `git add -A` (남의 미커밋 append 를 삼킨다)
|
|
96
|
+
❌ 남의 카드 직접 편집 — 내용을 넘기고 소유자가 싣는다
|
|
97
|
+
❌ `reset --hard` — Destructive-Op 게이트 대상
|
|
98
|
+
```
|
|
99
|
+
**메모리 인덱스도 같은 자원이다.** 2026-08-09: 한쪽이 항목을 archive 로 내리는 동안 다른 쪽이
|
|
100
|
+
hot 에 재색인해 **양 tier 동시 등재**가 됐다. 적용 직후 검사는 0이었고 **나중에 겹쳤다** — 즉
|
|
101
|
+
한 번의 검사로는 못 잡는다.
|
|
102
|
+
|
|
103
|
+
## 5. 마감 전 마지막 — 내용 착지를 따로 검증한다
|
|
104
|
+
|
|
105
|
+
close check 는 **형식**만 본다(카드가 로그보다 새로운가, 필수 아티팩트가 있는가). **내용이
|
|
106
|
+
착지했는지는 안 본다.** 2026-08-09 에 `CONSISTENT` 를 열세 번 내고도 발화 2건이 미기록이었고,
|
|
107
|
+
`scripts/utterance_landing_check.sh`(컨트롤 동반 grep)가 처음 잡았다.
|
|
108
|
+
|
|
109
|
+
**가장 잘 빠지는 것은 잊은 발화가 아니라 «행동으로 대응한 발화»다** — 답장에서 조사·판단까지
|
|
110
|
+
하고 나면 처리된 느낌이 남고, 그 판단이 어디에도 안 적힌 채 세션이 끝난다.
|
|
111
|
+
|
|
112
|
+
## 명명된 잔여
|
|
113
|
+
|
|
114
|
+
- ①-c 의 peer 판별은 **cwd 매칭**이라 **다른 worktree 의 peer 를 못 잡는다**(Orca 류 격리 사용 시).
|
|
115
|
+
- ①-d 스탬프와 §5 검사 결과는 **자기신고**다. 형식은 잡고 위조는 못 잡는다.
|
|
116
|
+
- §1 의 질문은 **사람/세션이 해야 한다** — 기계 트리거가 없다. ①-c 는 «물어라» 를 띄울 뿐이다.
|
|
117
|
+
- 이 문서 자체가 n=1(2026-08-09 2축 마감 1회) 위에 서 있다. **두 번째 사례가 나오면 다시 봐라** —
|
|
118
|
+
특히 §0 의 「거버너+위임이 기본형」은 그 한 번에서 나온 판단이다.
|
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.91",
|
|
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": [
|
|
@@ -90,10 +90,22 @@
|
|
|
90
90
|
"scripts/substrate_jump_detector.sh",
|
|
91
91
|
"scripts/tier_census_grep.sh",
|
|
92
92
|
"scripts/test_marker_floor_lanes.sh",
|
|
93
|
+
"scripts/test_marker_crossfamily_lanes.sh",
|
|
94
|
+
"scripts/directional_diff_gate.sh",
|
|
95
|
+
"scripts/reviewer_capability_corpus.tsv",
|
|
96
|
+
"scripts/test_reviewer_capability_conformance.sh",
|
|
93
97
|
"plugins/fh-meta/CHANGELOG.md",
|
|
94
98
|
"knowledge/shared/learnings/subagent_invocations_log.yaml",
|
|
95
99
|
"scripts/chamber_candidate_collect.sh",
|
|
100
|
+
"scripts/chamber_witness.sh",
|
|
101
|
+
"scripts/digest_landing_check.sh",
|
|
102
|
+
"scripts/relay_channel.sh",
|
|
103
|
+
"scripts/test_relay_channel_lanes.sh",
|
|
96
104
|
"scripts/fh_session_load.sh",
|
|
105
|
+
"scripts/branch_claim.sh",
|
|
106
|
+
"scripts/test_branch_claim_lanes.sh",
|
|
107
|
+
"scripts/hook_source_lib.sh",
|
|
108
|
+
"scripts/test_hook_source_gate_lanes.sh",
|
|
97
109
|
"templates/.claude/rules/mcp_tool_gating.md",
|
|
98
110
|
".claude/rules/fh_4axis_gate.md",
|
|
99
111
|
"templates/degrade_direction_scan.sh",
|
|
@@ -107,6 +119,7 @@
|
|
|
107
119
|
"scripts/prepush_guard_check.sh",
|
|
108
120
|
"scripts/psa_scan_lib.sh",
|
|
109
121
|
"scripts/session_close_check.sh",
|
|
122
|
+
"scripts/utterance_landing_check.sh",
|
|
110
123
|
"scripts/test_dispatch_log_lanes.sh",
|
|
111
124
|
"scripts/memory_link_check.py",
|
|
112
125
|
"scripts/test_memory_link_check.sh",
|
|
@@ -154,6 +167,14 @@
|
|
|
154
167
|
"templates/subagent-tally-hook.json",
|
|
155
168
|
"scripts/test_session_close_chain_lanes.sh",
|
|
156
169
|
"scripts/test_node_infra_delta_lanes.sh",
|
|
157
|
-
"scripts/test_wizard_snippet_merge_lanes.sh"
|
|
170
|
+
"scripts/test_wizard_snippet_merge_lanes.sh",
|
|
171
|
+
"scripts/compaction_probe.sh",
|
|
172
|
+
"scripts/judgment_circuit_lint.sh",
|
|
173
|
+
"scripts/novelty_claim_check.sh",
|
|
174
|
+
"templates/settings.Compaction.snippet.json",
|
|
175
|
+
"scripts/field_canon_preload.sh",
|
|
176
|
+
"scripts/test_field_canon_lanes.sh",
|
|
177
|
+
"templates/settings.FieldCanon.snippet.json",
|
|
178
|
+
".claude/judgment_circuits.txt"
|
|
158
179
|
]
|
|
159
180
|
}
|
|
@@ -197,13 +197,54 @@ mode**, with an honest *same-family* note but **no "reduced value / degraded" fr
|
|
|
197
197
|
this floor. `unset` → ask-once at first load-bearing need (accept → proceed; decline → record + this
|
|
198
198
|
branch). Only proceed to the discovery ladder below when consent is `accepted`.
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
Every rung **emits a typed `crossfamily:` value into the Axes 2–3 marker** — the rung is not
|
|
201
|
+
done until its verdict is recorded. The value is a closed enum, validated by `pre-commit`
|
|
202
|
+
(`validate_marker_floor`, fixtures in `scripts/test_marker_crossfamily_lanes.sh`); free prose is
|
|
203
|
+
rejected at commit time.
|
|
204
|
+
|
|
205
|
+
| # | Panel state | Emit | Ack |
|
|
206
|
+
|---|---|---|---|
|
|
207
|
+
| 1 | frontier cross-family CLI present → recruit it (decorrelated, at-floor) — best | `panel(<families>)` | — |
|
|
208
|
+
| 2 | only local 4090 → canary pre-screen + in-session opus governor (canary, not full decorrelation) | `panel(<families>)` **only if** a capable non-Claude model actually reviewed; otherwise rung 3 | — |
|
|
209
|
+
| 3 | **probed and nothing capable reachable** → in-session same-family | `DEGRADED_SINGLE_FAMILY` | **required** |
|
|
210
|
+
| — | **capable panel reachable, not recruited** (a choice, not a constraint) | `DEGRADED_PANEL_UNUSED` | **required** |
|
|
211
|
+
| — | consent `declined` (branch above — chosen floor, not a degrade) | `declined` | — |
|
|
212
|
+
| — | change is not load-bearing — decorrelation not required | `single-family` | — |
|
|
213
|
+
| — | **panel never probed** | `UNKNOWN` | **required** |
|
|
214
|
+
|
|
215
|
+
**The three degrade values are the load-bearing split**: *could not* (`DEGRADED_SINGLE_FAMILY`) ·
|
|
216
|
+
*did not* (`DEGRADED_PANEL_UNUSED`) · *did not look* (`UNKNOWN`). Free prose merges all three, and
|
|
217
|
+
each merge hides a different thing — an unrun probe renders as a zero finding
|
|
218
|
+
(`[[feedback_not_found_is_not_zero_family]]`), and an unused panel renders as an unavailable one.
|
|
219
|
+
The sibling-harness signal that motivated this field said it in its own words about itself:
|
|
220
|
+
*"못 한 것이 아니라 안 한 것이다."* All three require an ack naming *what was probed and what it
|
|
221
|
+
returned*. **`DEGRADED_PANEL_UNUSED` was added by dogfooding**: the first real marker written under
|
|
222
|
+
this lane found the panel reachable (codex · agy · gemini) and no cross-family review run — a state
|
|
223
|
+
the original five-value enum could not express without lying
|
|
224
|
+
(`[[feedback_adversarial_review_not_substitute_for_first_use]]`).
|
|
225
|
+
|
|
226
|
+
**Panel membership is capability-gated, and ineligibility is tested FIRST.** A runtime `/models`
|
|
227
|
+
list is a **serving** list, not a generative one. Measured in a sibling harness (2026-08-08): 6 of
|
|
228
|
+
14 served entries could not review anything (embedding · reranker · OCR · safeguard), and because
|
|
229
|
+
the embeddings' ids matched no family prefix **each counted as its own family — satisfying the
|
|
230
|
+
family-diversity check while the panel was incapable**. Order is the invariant, not the list:
|
|
231
|
+
`GLM-OCR` matches both `ocr` and `glm`, `Qwen3-Embedding` matches both `embed` and `qwen`, so an
|
|
232
|
+
eligibility-first test lets them through silently. If nothing capable survives the filter, that is
|
|
233
|
+
rung 3 — **not** a smaller panel.
|
|
234
|
+
|
|
235
|
+
**Attribution: `panel(...)` records the families *requested*, not confirmed.** At least one
|
|
236
|
+
procurement path (an internal corp-gateway CLI) returns **no model attribution in its response**, so "3/3 responded"
|
|
237
|
+
does not establish which model contributed. Record request-fact and response-fact separately; do not
|
|
238
|
+
launder the former into the latter (`[[feedback_instrument_cannot_discriminate_hypotheses]]`).
|
|
239
|
+
|
|
240
|
+
Env non-determinism (CLI presence varies) → **never hard-fail the run** — but never *silent* either.
|
|
241
|
+
The two are different properties, and the prose this replaces conflated them: the degrade proceeds
|
|
242
|
+
(a commit is a reversible surface) **and** lands in the verdict with an ack. That is how this rung
|
|
243
|
+
and `CLAUDE.md` §Field-Harness gate ("never a silent same-family pass") are simultaneously true.
|
|
244
|
+
Distinguish the **unavailable-but-wanted** case (consent given, panel down → `DEGRADED_SINGLE_FAMILY`
|
|
245
|
+
+ ack; for a *load-bearing corp* surface, fail-closed per `local_pmh_context.md` — note that file is
|
|
246
|
+
**pmh-repo-local and unreadable from an FH session**, so on this side the typed value + ack is the
|
|
247
|
+
enforceable layer) from the **declined** case above (chosen floor, first-class, no degrade framing).
|
|
207
248
|
|
|
208
249
|
## Step 7 — Output
|
|
209
250
|
|
|
@@ -221,7 +262,14 @@ at N per run (no loop).
|
|
|
221
262
|
|
|
222
263
|
- Mechanical sidecar discovery returns the available panel. *[mandatory-pass]*
|
|
223
264
|
- Family-diversity recruitment picks ≥1 cross-family verifier when present (family by model probe). *[mandatory-pass]*
|
|
224
|
-
- Degrade ladder applied
|
|
265
|
+
- Degrade ladder applied AND its verdict emitted as a typed `crossfamily:` value in the Axes 2–3
|
|
266
|
+
marker (`panel(<families>)` | `declined` | `DEGRADED_SINGLE_FAMILY` | `DEGRADED_PANEL_UNUSED` |
|
|
267
|
+
`UNKNOWN`); the three degrade values carry substantive grounds on the same line. `single-family`
|
|
268
|
+
is NOT accepted on a load-bearing change — that block only runs there, so "decorrelation not
|
|
269
|
+
required" is a contradiction, and as a no-ack pass it was a free bypass of the lane.
|
|
270
|
+
*[mandatory-pass — enforced by pre-commit, fixtures `scripts/test_marker_crossfamily_lanes.sh`]*
|
|
271
|
+
- Panel members are review-capable: embedding/reranker/OCR/safeguard classes are excluded **before**
|
|
272
|
+
family-diversity is counted, never after. *[mandatory-pass — same fixtures, ordering anchor c4/c5]*
|
|
225
273
|
- Paid recruit was per-run spend-gated (or `paid_auto` set); free local tier may auto-fire. *[mandatory-pass]*
|
|
226
274
|
- Governor (CC) retains terminal verdict; every accepted sidecar finding is source-grounded. *[judged, pair: judge-robustness / phantom-quench back-trace]*
|
|
227
275
|
- Marker records panel + families only with a captured sidecar transcript. *[mandatory-pass]*
|
|
@@ -170,6 +170,39 @@ Execution priority (behavioral):
|
|
|
170
170
|
|
|
171
171
|
> **Detail**: See `SKILL_detail.md §Step3-Execution` — action preview example, FH plugin auto-install bash + error handling table + success format, agent-composer mapping update format — read when executing Step 3.
|
|
172
172
|
|
|
173
|
+
### Step 3-D. Dispatch Standing Request — the one moment this is cheap to settle
|
|
174
|
+
|
|
175
|
+
FH/PMH treat isolated delegation as part of what the harness is, so **parallel dispatch is the
|
|
176
|
+
intended default for anyone running them**. It is not default *by assertion*: some runtimes ship a
|
|
177
|
+
line making unprompted dispatch conditional on a user request, and `CLAUDE.md §Agent Dispatch
|
|
178
|
+
Operation` will not read that condition as met unless a standing request is on disk. **A session with
|
|
179
|
+
no such record asks before every dispatch** — which is the friction this step exists to remove, once,
|
|
180
|
+
here, instead of in every future session.
|
|
181
|
+
|
|
182
|
+
So ask, and write the answer down. Three parts, all required — a record missing one is not a record,
|
|
183
|
+
and a session may never supply the missing part itself:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
① the user's own words, quoted — ask them to say it, then quote what they said
|
|
187
|
+
② a lease — THE USER picks the length. Do not suggest a default and
|
|
188
|
+
do not fill it in. Renewal needs a fresh utterance later
|
|
189
|
+
③ a scope — which prohibition line this covers. Granting one never
|
|
190
|
+
grants another (agents ≠ workflows ≠ deep-research):
|
|
191
|
+
name each one the user actually intends
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Write it into `CLAUDE.local.md` (gitignored). **Declined is a valid, recorded answer** — write that
|
|
195
|
+
down too, so later sessions stop re-asking rather than reading silence as an unanswered question.
|
|
196
|
+
|
|
197
|
+
**Any mechanical settings change belongs here as well**, under the same consent: if enabling the
|
|
198
|
+
intended default needs a `settings.json` edit, propose it in this step with the diff shown, and apply
|
|
199
|
+
it only on approval. Do not change settings silently on the strength of the doctrine alone — the
|
|
200
|
+
doctrine says dispatch is the default posture, not that the wizard may edit a user's config unasked.
|
|
201
|
+
|
|
202
|
+
⚠️ **The lease is honoured by reading, not by machinery.** `scripts/consent_registry_check.sh`
|
|
203
|
+
enforces leases for *registered consent classes*, and this grant is not one of those — say so when
|
|
204
|
+
recording it rather than implying an expiry alarm exists.
|
|
205
|
+
|
|
173
206
|
### Step 4. Acceleration Baseline Setup
|
|
174
207
|
|
|
175
208
|
After executing approved items, install the automatic maintenance structure: zshrc hook (idempotent, preview-then-confirm) + sentinel initialization (per-project) + weekly-audit cadence (no cron — zshrc hook + session-start detection are the durable mechanisms).
|
|
@@ -526,31 +526,120 @@ fi
|
|
|
526
526
|
# scripts/package_coverage_check.sh caught the omission — without it an npm-installed wizard hit
|
|
527
527
|
# FileNotFoundError here and registered nothing while reporting success upstream).
|
|
528
528
|
python3 - "$FH_DIR" <<'PY'
|
|
529
|
-
import json, os, sys, collections
|
|
529
|
+
import json, os, sys, glob, collections, re, shutil
|
|
530
530
|
hub = sys.argv[1]
|
|
531
|
-
snippet = os.path.join(hub, "templates", "settings.SessionStart.snippet.json")
|
|
532
531
|
target = os.path.join(hub, ".claude", "settings.json")
|
|
533
|
-
|
|
532
|
+
|
|
533
|
+
# DISCOVER every shipped snippet, do not name one. Hardcoding a single snippet path is why the
|
|
534
|
+
# compaction hooks shipped unwired while their own README recited the shipping-is-not-wiring lesson
|
|
535
|
+
# (high review, 2026-08-08 — and PreToolUse was already sitting in the same hole, so N=2).
|
|
536
|
+
# A new templates/settings.*.snippet.json is picked up with zero edits here: generation, not detection.
|
|
537
|
+
snippets = sorted(glob.glob(os.path.join(hub, "templates", "settings.*.snippet.json")))
|
|
538
|
+
if not snippets:
|
|
539
|
+
# exit 1, NOT 0. The pre-rewrite code raised FileNotFoundError here, and that loud failure is what
|
|
540
|
+
# made a broken npm `files[]` detectable — the comment above records it happening for real once.
|
|
541
|
+
# The first rewrite replaced it with a quiet exit 0, i.e. it deleted the detector while keeping the
|
|
542
|
+
# bug (high re-review 2026-08-08 #1). Zero snippets means zero hooks registered; that is never a
|
|
543
|
+
# success, and every other zero-registration path below already exits 1.
|
|
544
|
+
print("NO SNIPPETS under templates/settings.*.snippet.json — zero hooks registered.")
|
|
545
|
+
print(" → package `files[]` may have dropped templates/, or this is not an FH hub. NOT a success.")
|
|
546
|
+
raise SystemExit(1)
|
|
547
|
+
|
|
534
548
|
d = collections.OrderedDict()
|
|
535
549
|
if os.path.exists(target):
|
|
536
550
|
d = json.load(open(target), object_pairs_hook=collections.OrderedDict)
|
|
537
|
-
|
|
551
|
+
shutil.copy2(target, target + ".prewizard") # back up before rewriting
|
|
538
552
|
hooks = d.setdefault("hooks", collections.OrderedDict())
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
553
|
+
|
|
554
|
+
def fh_keys(entry_groups):
|
|
555
|
+
"""FH script basenames this snippet owns — the replace key is DERIVED from the snippet's own
|
|
556
|
+
commands, never hardcoded, so it cannot drift from what is actually being installed."""
|
|
557
|
+
# Key on the snippet's own PATH FORM (scripts/<name>.sh), not a bare basename. A bare basename
|
|
558
|
+
# made the survivor filter delete a user's own hook that merely shared a filename — e.g. their
|
|
559
|
+
# ~/tools/compaction_probe.sh — which is the exact loss the hook-level merge exists to prevent,
|
|
560
|
+
# reintroduced through key derivation (high re-review 2026-08-08 #2).
|
|
561
|
+
keys = set()
|
|
562
|
+
for g in entry_groups:
|
|
563
|
+
for h in g.get("hooks", []):
|
|
564
|
+
for m in re.findall(r'((?:[A-Za-z0-9_\-]+/)*scripts/[A-Za-z0-9_\-]+\.sh)', h.get("command", "")):
|
|
565
|
+
keys.add("scripts/" + m.rsplit("scripts/", 1)[1])
|
|
566
|
+
return keys
|
|
567
|
+
|
|
568
|
+
registered, bad_schema, intended = [], [], {}
|
|
569
|
+
for snip in snippets:
|
|
570
|
+
try:
|
|
571
|
+
blob = json.load(open(snip))
|
|
572
|
+
except Exception as e:
|
|
573
|
+
print("SKIP (unparsable):", os.path.basename(snip), e); bad_schema.append(snip); continue
|
|
574
|
+
proj = (blob.get("project_settings_json") or {}).get("hooks") or {}
|
|
575
|
+
if not proj:
|
|
576
|
+
# settings_local_json_MODE_D_ONLY entries carry private paths — never auto-written here.
|
|
577
|
+
print("SKIP (no project_settings_json):", os.path.basename(snip)); bad_schema.append(snip); continue
|
|
578
|
+
for event, entry in proj.items():
|
|
579
|
+
# Shape-validate before touching the user's file. A valid-JSON / invalid-SCHEMA snippet used
|
|
580
|
+
# to be written anyway and reported as success, then detonate on the NEXT run inside the
|
|
581
|
+
# survivor filter — far from the snippet that caused it (lanes BS-*, LT-*).
|
|
582
|
+
if not isinstance(entry, list) or not all(
|
|
583
|
+
isinstance(g, dict) and isinstance(g.get("hooks"), list) for g in entry):
|
|
584
|
+
print("SKIP (bad schema):", os.path.basename(snip), event); bad_schema.append(snip); continue
|
|
585
|
+
keys = fh_keys(entry)
|
|
586
|
+
if not keys:
|
|
587
|
+
print("SKIP (no FH script in commands):", os.path.basename(snip), event)
|
|
588
|
+
bad_schema.append(snip); continue
|
|
589
|
+
intended[event] = intended.get(event, set()) | keys
|
|
590
|
+
# Merge at HOOK level, not group level. A group-level filter drops the whole group when a
|
|
591
|
+
# user's own hook shares a group with the FH one (cross-family review 2026-07-30 reproduced
|
|
592
|
+
# the loss: [my_telemetry.sh, fh_session_load.sh] lost my_telemetry.sh entirely).
|
|
593
|
+
kept = []
|
|
594
|
+
for g in hooks.get(event, []):
|
|
595
|
+
survivors = [h for h in g.get("hooks", [])
|
|
596
|
+
if not any(k in h.get("command", "") for k in keys)]
|
|
597
|
+
if survivors:
|
|
598
|
+
g = dict(g); g["hooks"] = survivors; kept.append(g)
|
|
599
|
+
hooks[event] = kept + entry
|
|
600
|
+
registered.append(f"{event}({','.join(sorted(keys)) or '?'})")
|
|
601
|
+
|
|
549
602
|
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
550
603
|
with open(target, "w") as fh:
|
|
551
604
|
json.dump(d, fh, indent=2, ensure_ascii=False); fh.write("\n")
|
|
552
|
-
|
|
605
|
+
# RE-READ WHAT WE JUST WROTE. The success message used to be unconditional — it never checked the
|
|
606
|
+
# serialized result, so a snippet that failed to register still printed "registered ->". Verify, then
|
|
607
|
+
# claim (`[[feedback_gate_verification_must_execute]]`).
|
|
608
|
+
written = json.load(open(target))
|
|
609
|
+
missing = []
|
|
610
|
+
for event, keys in intended.items():
|
|
611
|
+
blob = json.dumps(written.get("hooks", {}).get(event, []))
|
|
612
|
+
for k in sorted(keys):
|
|
613
|
+
if k not in blob:
|
|
614
|
+
missing.append(f"{event}:{k}")
|
|
615
|
+
for r in registered: print(" hook registered ->", r)
|
|
616
|
+
print("snippets scanned:", len(snippets), "· hook entries registered:", len(registered),
|
|
617
|
+
"· skipped:", len(bad_schema), "->", target)
|
|
618
|
+
if missing:
|
|
619
|
+
print("FAILED to register:", ", ".join(missing)); raise SystemExit(1)
|
|
620
|
+
# FAIL CLOSED on any skipped snippet. The good ones are already written (a malformed sibling must not
|
|
621
|
+
# cost you the working hooks), but the RUN exits non-zero — an installer that skips a snippet and
|
|
622
|
+
# reports success leaves hooks unwired, which is the exact defect this rewrite exists to close.
|
|
623
|
+
# Lanes MG-N1..N4 caught this: the first draft skipped-and-exited-0 = fail-open.
|
|
624
|
+
if bad_schema:
|
|
625
|
+
print("SKIPPED snippet(s):", ", ".join(sorted({os.path.basename(b) for b in bad_schema})))
|
|
626
|
+
print(" → non-zero exit on purpose: a skipped snippet is unregistered hooks, not a warning.")
|
|
627
|
+
raise SystemExit(1)
|
|
628
|
+
if not registered:
|
|
629
|
+
print("no hook entries registered — nothing to claim"); raise SystemExit(1)
|
|
553
630
|
PY
|
|
631
|
+
# CONSUME the merge block's exit status. It raises SystemExit(1) on a skipped/failed registration,
|
|
632
|
+
# and the surrounding bash has no `set -e` — so without this check the wizard sailed past a failed
|
|
633
|
+
# registration and §Step5 still printed success, which is the exact "reports success while unwired"
|
|
634
|
+
# shape the fail-closed direction was added to prevent (high re-review 2026-08-08 #6).
|
|
635
|
+
# The failure is SURFACED, not swallowed: the operator sees which snippet did not register.
|
|
636
|
+
_MERGE_RC=$?
|
|
637
|
+
if [ "$_MERGE_RC" -ne 0 ]; then
|
|
638
|
+
echo "🟥 hook registration FAILED (exit $_MERGE_RC) — one or more snippets are UNREGISTERED."
|
|
639
|
+
echo " Do not read the completion report below as 'hooks are wired'. Fix the snippet and re-run"
|
|
640
|
+
echo " this step; the rest of the wizard continues so the remaining setup is not lost."
|
|
641
|
+
WIZARD_HOOK_REG_FAILED=1
|
|
642
|
+
fi
|
|
554
643
|
chmod +x "$FH_DIR/scripts/fh_node_check.sh" 2>/dev/null
|
|
555
644
|
# VERIFY against a THROWAWAY state file (FH_NODE_STATE). Verifying against the real state would
|
|
556
645
|
# consume the user's one-shot event report, so their actual first session goes quiet and the notice
|