@chrono-meta/fh-gate 1.4.89 → 1.4.90
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 +179 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +165 -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 +528 -0
- package/package.json +16 -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/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/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 +66 -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 +253 -13
- package/templates/settings.Compaction.snippet.json +56 -0
|
@@ -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
|
package/scripts/chamber_run.sh
CHANGED
|
@@ -43,6 +43,25 @@ TODAY="$(date +%Y-%m-%d)"
|
|
|
43
43
|
_status() { [ -f "$STATUS_F" ] && cat "$STATUS_F" || echo "step-0"; }
|
|
44
44
|
_stamp() { printf '%s\n' "$1" > "$STATUS_F"; }
|
|
45
45
|
|
|
46
|
+
# ── 순서 증인 (ship_readiness_gate §② P1) ────────────────────────────────────
|
|
47
|
+
# 워크스페이스는 `tracks/**` 라 gitignored 다. 그래서 게이트 아티팩트의 **해시만**
|
|
48
|
+
# 추적되는 원장에 적어 커밋한다 — **본문** 유출 0(단 slug·파일명·시각·해시는 남는다),
|
|
49
|
+
# 순서는 로컬 커밋 그래프가 증언(되쓰기 방지는 push 후 서버 ruleset 이 준다).
|
|
50
|
+
# 기록 실패는 **런을 막지 않는다**(KILL 런까지 세우는 건 과차단이고, 과차단은
|
|
51
|
+
# override 를 습관화시킨다). 대신 증인 부재를 **말한다** — 조용히 넘어가지 않는다.
|
|
52
|
+
_WITNESS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/chamber_witness.sh"
|
|
53
|
+
_witness_record() { # $1 = artifact path
|
|
54
|
+
if [ ! -x "$_WITNESS" ] && [ ! -f "$_WITNESS" ]; then
|
|
55
|
+
echo " ⚠️ 순서 증인 미기록 — chamber_witness.sh 부재 (이 런은 ② 승격 근거로 못 쓴다)"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
if bash "$_WITNESS" record "$SLUG" "$1" >/dev/null 2>&1; then
|
|
59
|
+
echo " ↳ 순서 증인: $(basename "$1") 해시 기록됨 — **커밋해야** 증인이 된다"
|
|
60
|
+
else
|
|
61
|
+
echo " ⚠️ 순서 증인 기록 실패: $(basename "$1") (증인 없음 — 미측정이지 0이 아니다)"
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
if [ "$CMD" = "status" ]; then
|
|
47
66
|
echo "chamber run '$SLUG' → STATUS: $(_status)"
|
|
48
67
|
[ -d "$WS" ] && ls -1 "$WS" 2>/dev/null | sed 's/^/ /'
|
|
@@ -85,9 +104,18 @@ fi
|
|
|
85
104
|
# gate (a) is "artifact exists WITH real content", not just "placeholder removed" (Axis-2 LOW-6): require
|
|
86
105
|
# at least one non-empty numbered success condition so a gutted INTENT.md doesn't pass.
|
|
87
106
|
if ! awk '/^## Success conditions/{f=1;next} /^## /{f=0} f && /^[0-9]+\.[[:space:]]*[^[:space:]]/{print; exit}' "$WS/INTENT.md" | grep -q .; then
|
|
88
|
-
|
|
107
|
+
# 계약을 **에러 메시지가 가르친다.** 이 형식은 CHAMBER_RUN_SKELETON.md 에 한 글자도 없어서,
|
|
108
|
+
# 스켈레톤만 읽고 쓴 INTENT 가 반려됐다 — gate-locality(계약이 게이트 구현에만 산다).
|
|
109
|
+
# 같은 런에서 step 6 도 같은 이유로 막혔다(N=2). 문서는 드리프트하지만 에러는 코드와 같이 산다.
|
|
110
|
+
echo " ⛔ step 2 BLOCKED: $WS/INTENT.md has no filled success condition."
|
|
111
|
+
echo " Expected — an English heading, then NUMBERED lines with content:"
|
|
112
|
+
echo " ## Success conditions"
|
|
113
|
+
echo " 1. <condition> · [measured|mandatory-pass|judged]"
|
|
114
|
+
echo " 2. ..."
|
|
115
|
+
echo " A table alone does NOT satisfy this check. Re-run after adding it."; exit 1
|
|
89
116
|
fi
|
|
90
117
|
_stamp "step-2-done"; echo " ✓ step 2: INTENT.md present"
|
|
118
|
+
_witness_record "$WS/INTENT.md"
|
|
91
119
|
|
|
92
120
|
# STEP 3 — budget-entry gate (G2). Cannot invoke goal-quench from bash; MECHANICALLY require a recorded
|
|
93
121
|
# estimate before any (expensive) simulation runs. No ESTIMATE = no run — that IS the entry cap.
|
|
@@ -106,6 +134,7 @@ if grep -qE '^ESTIMATE:[[:space:]]*<' "$WS/BUDGET.md" || ! grep -qE '^ESTIMATE:[
|
|
|
106
134
|
echo " ⛔ step 3 BLOCKED: $WS/BUDGET.md ESTIMATE is empty/placeholder (G2 entry cap), then re-run."; exit 1
|
|
107
135
|
fi
|
|
108
136
|
_stamp "step-3-done"; echo " ✓ step 3: budget ESTIMATE recorded (entry cap satisfied)"
|
|
137
|
+
_witness_record "$WS/BUDGET.md"
|
|
109
138
|
|
|
110
139
|
# STEP 4 — persona simulation (G1-forcing gate). Dispatch is human/Claude-side (bash can't spawn Agents);
|
|
111
140
|
# gate on the ≥3 blind persona artifact. Isolation is the mechanism — the runner enforces the artifact, not the spawn.
|
|
@@ -126,6 +155,7 @@ if [ "$NPERS" -lt 3 ]; then
|
|
|
126
155
|
echo " ⛔ step 4 BLOCKED: SIM_NOTES.md has $NPERS/3 DISTINCT blind persona sections (need all of beginner + main-player + challenger)."; exit 1
|
|
127
156
|
fi
|
|
128
157
|
_stamp "step-4-done"; echo " ✓ step 4: $NPERS blind persona sections present (isolation-gate satisfied)"
|
|
158
|
+
_witness_record "$WS/SIM_NOTES.md"
|
|
129
159
|
|
|
130
160
|
# STEP 5 — Emission Gate. Require a VERDICT: EMIT | PARTIAL-EMIT | KILL.
|
|
131
161
|
if [ ! -f "$WS/EMISSION_VERDICT.md" ]; then
|
|
@@ -149,10 +179,36 @@ if [ -z "$VERDICT" ]; then
|
|
|
149
179
|
echo " ⛔ step 5 BLOCKED: no VERDICT (EMIT|PARTIAL-EMIT|KILL) found in $WS/EMISSION_VERDICT.md, re-run."; exit 1
|
|
150
180
|
fi
|
|
151
181
|
_stamp "step-5-done"; echo " ✓ step 5: Emission Gate verdict = $VERDICT"
|
|
182
|
+
_witness_record "$WS/EMISSION_VERDICT.md"
|
|
183
|
+
# 순서 판정을 **여기서 인쇄한다.** verdict 가 나온 자리가 "게이트가 결과를 알기 전에
|
|
184
|
+
# 돌았는가" 를 물어야 하는 유일한 자리다. 차단하지 않는 이유: KILL 런까지 막으면
|
|
185
|
+
# 과차단이고, 과차단은 override 를 습관화시켜 게이트 전체를 무장해제한다.
|
|
186
|
+
# 대신 **EMIT 일 때는 승격 사용 가부를 명시적으로 말한다** — 침묵이 곧 승인으로
|
|
187
|
+
# 읽히는 것이 이 채널이 존재하는 이유다.
|
|
188
|
+
if [ -f "$_WITNESS" ]; then
|
|
189
|
+
echo " ── 순서 증인 판정 ──"
|
|
190
|
+
bash "$_WITNESS" verify "$SLUG" "$WS" 2>&1 | sed 's/^/ /'
|
|
191
|
+
_wrc="${PIPESTATUS[0]}"
|
|
192
|
+
if [ "$VERDICT" = "EMIT" ] || [ "$VERDICT" = "PARTIAL-EMIT" ]; then
|
|
193
|
+
if [ "${_wrc:-9}" = "0" ]; then
|
|
194
|
+
echo " ✅ 이 EMIT 은 순서 증인을 가진다 — identity ② 승격 근거로 사용 가능"
|
|
195
|
+
else
|
|
196
|
+
echo " 🟥 이 EMIT 에는 순서 증인이 없다(rc=${_wrc:-?}) — identity ② 승격 근거로 쓰지 마라."
|
|
197
|
+
echo " (원장 해시를 커밋한 뒤 재실행하면 증인이 성립한다)"
|
|
198
|
+
# ★ 출력만 하고 exit 0 으로 끝내면, stdout 을 안 읽는 호출자에게는 증인 없는 EMIT 도
|
|
199
|
+
# **성공**이다(cross-family F8). EMIT 은 승격 주장이므로 기계적으로 구분되어야 한다.
|
|
200
|
+
WITNESS_EMIT_FAIL=1
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
152
204
|
|
|
153
205
|
# STEP 6 — actual cost / carry-forward record.
|
|
154
206
|
if grep -qE '^ACTUAL:[[:space:]]*<' "$WS/BUDGET.md" || ! grep -qE '^ACTUAL:[[:space:]]*\S' "$WS/BUDGET.md"; then
|
|
155
|
-
echo " ⛔ step 6 BLOCKED: record ACTUAL cost in $WS/BUDGET.md (actual-vs-estimate calibration)
|
|
207
|
+
echo " ⛔ step 6 BLOCKED: record ACTUAL cost in $WS/BUDGET.md (actual-vs-estimate calibration)."
|
|
208
|
+
echo " Expected — a LINE PREFIX, not a heading:"
|
|
209
|
+
echo " ACTUAL: <number/summary> ← this form is checked"
|
|
210
|
+
echo " ## ACTUAL ← a heading does NOT satisfy it"
|
|
211
|
+
echo " Re-run after adding the line."; exit 1
|
|
156
212
|
fi
|
|
157
213
|
_stamp "step-6-done"; echo " ✓ step 6: actual cost recorded"
|
|
158
214
|
|
|
@@ -181,4 +237,10 @@ case "$VERDICT" in
|
|
|
181
237
|
echo " No emit. Workspace stays as the evidence record; seen-filter will skip re-listing it." ;;
|
|
182
238
|
esac
|
|
183
239
|
echo "chamber run '$SLUG' COMPLETE (STATUS: step-7-done, verdict $VERDICT)."
|
|
240
|
+
# EMIT/PARTIAL-EMIT 인데 순서 증인이 없으면 **비영 종료**한다. KILL 은 영향 없다 —
|
|
241
|
+
# KILL 까지 실패로 세우면 과차단이고, 과차단은 override 를 습관화시킨다.
|
|
242
|
+
if [ "${WITNESS_EMIT_FAIL:-0}" = "1" ]; then
|
|
243
|
+
echo "⚠️ exit 3 — EMIT 이지만 순서 증인 미성립. 승격 주장에 쓰지 마라."
|
|
244
|
+
exit 3
|
|
245
|
+
fi
|
|
184
246
|
exit 0
|