@chrono-meta/fh-gate 1.4.71 → 1.4.72

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.
@@ -11,13 +11,13 @@
11
11
  "plugins": [
12
12
  {
13
13
  "name": "fh-meta",
14
- "version": "1.4.71",
14
+ "version": "1.4.72",
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.71",
20
+ "version": "1.4.72",
21
21
  "description": "Project-agnostic utility skills — 4 skills (convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate) + 1 agent (quench-challenger). Domain-independent utilities transplantable into any project.",
22
22
  "source": "./plugins/fh-commons"
23
23
  }
package/CATALOG.md CHANGED
@@ -4,6 +4,34 @@ AI reads this file first when searching past work. Open individual files for det
4
4
 
5
5
  ---
6
6
 
7
+ ## 2026-07-26 — Confidentiality gate: scope decoupled from the 4-axis classifier (gate-locality N=5)
8
+
9
+ - **Origin**: source-reading a cross-audited sister asset (`PromptPartner/agentsmith`, `leak-gate.sh`).
10
+ Its `SCOPE=()` comment — *"an excluded file is a hole a real leak can sit in"* — asked of this repo
11
+ surfaced a different shape of the same hole: the pre-commit confidentiality scan sat below
12
+ `exit 0 # No FH assets staged`, so its trigger silently inherited the 4-axis asset pathspec. A commit
13
+ staging only non-asset paths skipped it entirely. Known-pair measured, both directions: the identical
14
+ leak line blocks in `CATALOG.md` and passes unseen in `README.md` alone. Surface: 46/241 tracked files
15
+ (19.1%) unscannable that way, 32 of them also outside npm `files[]`.
16
+ - **Repair**: the confidentiality/privacy guards moved into `run_universal_guards()`, invoked before the
17
+ early exit — they guard the publish boundary, not FH asset structure. Credential SHAPES imported from
18
+ the sister asset (the scan previously matched only operator-private identity tokens, so a live AWS/GitHub/
19
+ Anthropic key scanned clean), live-calibrated over the whole tree: one false positive, closed by an
20
+ exact-literal exemption. A push-time content scan was added over the commits a push actually publishes.
21
+ - **Verification**: 7 cross-family adversarial rounds (codex/gpt-5.5). 30 findings; 2 accepted as named
22
+ residuals with the success line narrowed to claim only what it checks, the rest fixed. One author
23
+ refutation withdrawn when the auditor showed `npm publish` is not a backstop for a public git repo.
24
+ Anchors: `scripts/universal_guard_check.sh` (pre-commit) and `scripts/prepush_guard_check.sh` (pre-push),
25
+ both testing the STAGED blob and scoring a hook runtime fault as its own failure class.
26
+ - **Also**: `validate_yaml.sh` was found to be the defect rather than the file it flagged (it grepped for
27
+ `': '` without checking whether the value was quoted, and its `--fix` folded quotes into an already-correct
28
+ string) — rewritten parse-first with an `UNCALIBRATED` degrade. `sync-to-be.sh` returned the status of its
29
+ last loop iteration, so a successful sync reported failure, the Stop-hook cooldown stamp was never written,
30
+ and the companion store went uncommitted for ~3.7h; introduced by the same-day patch that added mirror banners.
31
+ - **Residual**: three near-duplicate scan implementations remain; single-sourcing them is the next PR and the
32
+ real convergence — every leniency divergence found here was a propagation miss between those copies.
33
+ - Tags: `gate-locality` `confidentiality` `cross-family` `known-pair` `sister-asset` `publish-boundary`
34
+
7
35
  ## Sessions
8
36
 
9
37
  <!-- Add entries in reverse date order (newest at top) -->
package/README.ja.md CHANGED
@@ -198,7 +198,7 @@ FH_BACKEND=auto npx --package @chrono-meta/fh-gate fh-gate "src/foo.ts" full
198
198
  # → FH_GATE_VERDICT: PASS | PENDING | BLOCKED | ESCALATE
199
199
  ```
200
200
 
201
- `fh-gate` は両ランタイムに同じ FH ガバナンスプロンプトを使います。`FH_BACKEND=claude` は `claude --print` を、`FH_BACKEND=codex` は `codex exec` を実行し、`FH_BACKEND=auto` は両 CLI が揃っていれば Codex を優先します。
201
+ `fh-gate` は両ランタイムに同じ FH ガバナンスプロンプトを使います。`FH_BACKEND=claude` は `claude --print` を、`FH_BACKEND=codex` は `codex exec` を実行し、`FH_BACKEND=auto` は両 CLI が揃っていれば Codex を優先します — ただし `auto` はフォールバック*選択*であり、レグは 1 つだけ走ります。`FH_BACKEND=cross` は両ファミリーを走らせて findings を union します(一方だけが見つけた指摘も指摘なので、投票ではなく union)。判定はレグ中で最も重いものです。コストは約 2 倍なので既定ではなく、判定・ゲート・不可逆な面の変更に使います。出力は実際に走ったレグを常に明示します(`FH_GATE_LEGS:`、`FH_GATE_DECORRELATED:`) — 片方のファミリーしかない環境では単一レグに縮退し、その事実を明記します。
202
202
 
203
203
  Claude Code の外でスキルやエージェントを直接実行するには `fh-run` を使います:
204
204
 
package/README.ko.md CHANGED
@@ -197,7 +197,7 @@ FH_BACKEND=auto npx --package @chrono-meta/fh-gate fh-gate "src/foo.ts" full
197
197
  # → FH_GATE_VERDICT: PASS | PENDING | BLOCKED | ESCALATE
198
198
  ```
199
199
 
200
- `fh-gate`는 두 런타임에 동일한 FH 거버넌스 프롬프트를 씁니다. `FH_BACKEND=claude`는 `claude --print`를, `FH_BACKEND=codex`는 `codex exec`를 실행하며, `FH_BACKEND=auto`는 두 CLI가 모두 있으면 Codex를 우선합니다.
200
+ `fh-gate`는 두 런타임에 동일한 FH 거버넌스 프롬프트를 씁니다. `FH_BACKEND=claude`는 `claude --print`를, `FH_BACKEND=codex`는 `codex exec`를 실행하며, `FH_BACKEND=auto`는 두 CLI가 모두 있으면 Codex를 우선합니다 — 다만 `auto`는 폴백 *선택*이라 레그를 하나만 돌립니다. `FH_BACKEND=cross`는 두 패밀리를 모두 돌려 findings를 union 합니다(한쪽만 본 지적도 지적이므로 투표가 아니라 union). 판정은 레그 중 가장 무거운 것입니다. 비용이 약 2배라 기본값이 아니며, 판정·게이트·비가역 표면 변경에 씁니다. 출력은 실제로 돈 레그를 항상 밝힙니다(`FH_GATE_LEGS:`, `FH_GATE_DECORRELATED:`) — 한 패밀리만 설치된 머신에서는 단일 레그로 내려가되 그 사실을 명시합니다. 단일 패밀리 결과가 교차검증된 것처럼 읽히는 편이 더 나쁘기 때문입니다.
201
201
 
202
202
  Claude Code 밖에서 스킬이나 에이전트를 직접 실행하려면 `fh-run`을 씁니다:
203
203
 
package/README.md CHANGED
@@ -209,7 +209,7 @@ FH_BACKEND=auto npx --package @chrono-meta/fh-gate fh-gate "src/foo.ts" full
209
209
  # → FH_GATE_VERDICT: PASS | PENDING | BLOCKED | ESCALATE
210
210
  ```
211
211
 
212
- `fh-gate` uses the same FH governance prompt for both runtimes. `FH_BACKEND=claude` runs `claude --print`; `FH_BACKEND=codex` runs `codex exec`; `FH_BACKEND=auto` prefers Codex when both CLIs are present.
212
+ `fh-gate` uses the same FH governance prompt for both runtimes. `FH_BACKEND=claude` runs `claude --print`; `FH_BACKEND=codex` runs `codex exec`; `FH_BACKEND=auto` prefers Codex when both CLIs are present — note that `auto` is fallback *selection*: it runs ONE leg. `FH_BACKEND=cross` runs BOTH families and unions their findings (a finding only one family saw is still a finding, so it unions rather than votes); the verdict is the most severe across legs. It costs ~2x, so it is for load-bearing verdict/gate/irreversible-surface changes, not a default. The output always declares which legs actually ran (`FH_GATE_LEGS:`, `FH_GATE_DECORRELATED:`) — on a machine with only one family, `cross` degrades to that single leg and says so, because a single-family result that reads as cross-checked is worse than an honest one.
213
213
 
214
214
  For direct skill or agent execution outside Claude Code, use `fh-run`:
215
215
 
package/README.zh.md CHANGED
@@ -187,7 +187,7 @@ FH_BACKEND=auto npx --package @chrono-meta/fh-gate fh-gate "src/foo.ts" full
187
187
  # → FH_GATE_VERDICT: PASS | PENDING | BLOCKED | ESCALATE
188
188
  ```
189
189
 
190
- `fh-gate` 对两种运行时使用同一套 FH 治理提示。`FH_BACKEND=claude` 运行 `claude --print`;`FH_BACKEND=codex` 运行 `codex exec`;`FH_BACKEND=auto` 在两个 CLI 都存在时优先选择 Codex
190
+ `fh-gate` 对两种运行时使用同一套 FH 治理提示。`FH_BACKEND=claude` 运行 `claude --print`;`FH_BACKEND=codex` 运行 `codex exec`;`FH_BACKEND=auto` 在两个 CLI 都存在时优先选择 Codex —— 但 `auto` 是回退式*选择*,只运行一条腿。`FH_BACKEND=cross` 会运行两个模型家族并对 findings 取并集(只有一方发现的问题仍然是问题,因此是并集而非投票),判定取各腿中最严重者。成本约为 2 倍,因此并非默认值,适用于判定/门禁/不可逆面的变更。输出始终声明实际运行了哪些腿(`FH_GATE_LEGS:`、`FH_GATE_DECORRELATED:`) —— 在只装了一个家族的机器上,`cross` 会降级为单腿并明确说明,因为让单家族结果读起来像交叉验证过更糟。
191
191
 
192
192
  若要在 Claude Code 之外直接执行技能或 agent,使用 `fh-run`:
193
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrono-meta/fh-gate",
3
- "version": "1.4.71",
3
+ "version": "1.4.72",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-commons",
3
- "version": "1.4.71",
3
+ "version": "1.4.72",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-meta",
3
- "version": "1.4.71",
3
+ "version": "1.4.72",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: salience-splitter
3
- description: Splits an over-loaded always-loaded context asset — a SKILL.md, CLAUDE.md, or memory index — into a lean always-loaded layer + an on-demand layer, using a governance-semantic criterion (not length, but when the content is needed), connected by imperative pointers. Based on paper §9.5 Protocol-Priority Split pattern. Diagnoses, classifies, splits, and verifies in one pass. Renamed from skill-splitter (old name still routes here). Triggers: "SKILL.md too large", "split this skill", "skill is bloated", "skill file too long", "CLAUDE.md 너무 커".
3
+ description: 'Splits an over-loaded always-loaded context asset — a SKILL.md, CLAUDE.md, or memory index — into a lean always-loaded layer + an on-demand layer, using a governance-semantic criterion (not length, but when the content is needed), connected by imperative pointers. Based on paper §9.5 Protocol-Priority Split pattern. Diagnoses, classifies, splits, and verifies in one pass. Renamed from skill-splitter (old name still routes here). Triggers: "SKILL.md too large", "split this skill", "skill is bloated", "skill file too long", "CLAUDE.md 너무 커".'
4
4
  user-invocable: true
5
5
  allowed-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
6
6
  model: sonnet
@@ -59,13 +59,87 @@ FH_TASK_DESCRIPTION="${FH_TASK_DESCRIPTION:-}"
59
59
  FH_DIFF_PATH="${FH_DIFF_PATH:-}"
60
60
 
61
61
  case "$FH_BACKEND" in
62
- claude|codex|auto) ;;
62
+ claude|codex|auto|cross) ;;
63
63
  *)
64
- echo "ERROR: FH_BACKEND must be 'claude', 'codex', or 'auto' (got: $FH_BACKEND)" >&2
64
+ echo "ERROR: FH_BACKEND must be 'claude', 'codex', 'auto', or 'cross' (got: $FH_BACKEND)" >&2
65
65
  exit $EXIT_ARG_ERROR
66
66
  ;;
67
67
  esac
68
68
 
69
+ # ── FH_BACKEND=cross — decorrelated review (2026-07-26) ───────────────────────────────────────
70
+ # `auto` is a FALLBACK: it picks codex if present, else claude, and runs ONE leg. That is backend
71
+ # SELECTION, not decorrelation. A same-family reviewer shares the author's optimistic reading, which
72
+ # is the failure mode the FH load-bearing gate exists to catch — and a 7-round cross-family audit of
73
+ # that gate found 30 issues an in-family pass had not.
74
+ #
75
+ # `cross` runs BOTH families and UNIONs their findings. Union, not vote: detection is the task, and a
76
+ # finding only one leg saw is still a finding (majority voting would discard exactly the decorrelated
77
+ # signal this mode is for). The verdict is the most severe across legs.
78
+ #
79
+ # THE INVARIANT THAT MATTERS MOST: the output declares WHICH LEGS ACTUALLY RAN (`FH_GATE_LEGS:`).
80
+ # `cross` is a REQUEST, not a guarantee — a machine may have only one family installed. When a leg is
81
+ # unavailable or errors, this degrades to the surviving leg and SAYS SO, loudly, in both the human and
82
+ # the machine-readable output. A single-leg result that reads as if it were cross-checked is the same
83
+ # class of defect as a check that did not run reading as PASS.
84
+ #
85
+ # COST: cross is ~2x. It is deliberately NOT a default — [[feedback_decorrelation_not_fanout_cost_boundary]]
86
+ # says match the parallelisation axis to the failure mode rather than multiply. Callers should request
87
+ # it for load-bearing verdict/gate/irreversible-surface changes, which is exactly the trigger the
88
+ # pre-commit load-bearing leg already computes.
89
+ if [[ "$FH_BACKEND" == "cross" ]]; then
90
+ _CROSS_LEGS=""; _CROSS_OUT=""; _CROSS_WORST="PASS"; _CROSS_RAN=0; _CROSS_ERR=""
91
+ _sev_rank() { case "$1" in PASS) echo 0 ;; PENDING) echo 1 ;; ESCALATE) echo 2 ;; BLOCKED) echo 3 ;; *) echo -1 ;; esac; }
92
+ for _leg in claude codex; do
93
+ command -v "$_leg" >/dev/null 2>&1 || { _CROSS_ERR="${_CROSS_ERR}${_CROSS_ERR:+, }$_leg (not installed)"; continue; }
94
+ # `set -e` is on: an assignment from a command substitution carries the child's exit status, and
95
+ # this child EXITS NON-ZERO BY DESIGN for every non-PASS verdict (PENDING 1 / BLOCKED 2 /
96
+ # ESCALATE 3). Written bare, the first leg that found anything would kill the whole cross run —
97
+ # i.e. cross would work only when there was nothing to find. `|| _legrc=$?` keeps the status
98
+ # without letting it abort. Verified against a BLOCKED leg, not assumed.
99
+ _legrc=0
100
+ _legout=$(FH_BACKEND="$_leg" "$0" "$@" 2>/dev/null) || _legrc=$?
101
+ if [ "$_legrc" -ge 10 ]; then
102
+ _CROSS_ERR="${_CROSS_ERR}${_CROSS_ERR:+, }$_leg (harness error $_legrc)"; continue
103
+ fi
104
+ _legverdict=$(printf '%s' "$_legout" | grep -m1 '^FH_GATE_VERDICT:' | awk '{print $2}')
105
+ [ -z "$_legverdict" ] && { _CROSS_ERR="${_CROSS_ERR}${_CROSS_ERR:+, }$_leg (no verdict line)"; continue; }
106
+ _CROSS_RAN=$((_CROSS_RAN+1))
107
+ _CROSS_LEGS="${_CROSS_LEGS}${_CROSS_LEGS:+,}$_leg"
108
+ [ "$(_sev_rank "$_legverdict")" -gt "$(_sev_rank "$_CROSS_WORST")" ] && _CROSS_WORST="$_legverdict"
109
+ _CROSS_OUT="${_CROSS_OUT}
110
+ # ── leg: $_leg (verdict $_legverdict) ──
111
+ $(printf '%s' "$_legout" | sed -n '/^---$/,$p')"
112
+ done
113
+
114
+ if [ "$_CROSS_RAN" -eq 0 ]; then
115
+ echo "ERROR: FH_BACKEND=cross — no leg produced a verdict (${_CROSS_ERR:-none available}). Failing closed." >&2
116
+ exit $EXIT_HARNESS_ERROR
117
+ fi
118
+
119
+ printf 'FH_STATUS: SUCCESS\n'
120
+ printf 'FH_GATE_VERDICT: %s\n' "$_CROSS_WORST"
121
+ printf 'FH_GATE_MODE: cross\n'
122
+ printf 'FH_GATE_LEGS: %s\n' "$_CROSS_LEGS"
123
+ if [ "$_CROSS_RAN" -lt 2 ]; then
124
+ # Degraded, and it must be impossible to mistake this for a two-family result.
125
+ printf 'FH_GATE_DECORRELATED: no\n'
126
+ printf 'FH_GATE_DEGRADED: %s\n' "$_CROSS_ERR"
127
+ echo "WARN: FH_BACKEND=cross ran a SINGLE leg ($_CROSS_LEGS) — $_CROSS_ERR." >&2
128
+ echo " This verdict is NOT decorrelated. Treat it as $_CROSS_LEGS alone." >&2
129
+ else
130
+ printf 'FH_GATE_DECORRELATED: yes\n'
131
+ fi
132
+ printf '%s\n' "$_CROSS_OUT"
133
+ echo "→ fh-gate: cross mode — legs [$_CROSS_LEGS], union verdict $_CROSS_WORST" >&2
134
+ case "$_CROSS_WORST" in
135
+ PASS) exit $EXIT_PASS ;;
136
+ PENDING) exit $EXIT_PENDING ;;
137
+ BLOCKED) exit $EXIT_BLOCKED ;;
138
+ ESCALATE) exit $EXIT_ESCALATE ;;
139
+ *) exit $EXIT_HARNESS_ERROR ;;
140
+ esac
141
+ fi
142
+
69
143
  # FH_TIMEOUT lands in command position via the unquoted ${_TIMEOUT_CMD} idiom below.
70
144
  # `timeout DURATION COMMAND [ARG]...` treats the word after the duration as the command,
71
145
  # so an unvalidated value word-splits into arbitrary execution with no shell metacharacters
@@ -34,7 +34,7 @@ while [ $# -gt 0 ]; do
34
34
  esac
35
35
  done
36
36
  cat >/dev/null # consume the prompt on stdin
37
- [ -n "$out" ] && printf '%s' "$FAKE_PAYLOAD" > "$out"
37
+ [ -n "$out" ] && printf '%s' "${FAKE_PAYLOAD_CODEX:-$FAKE_PAYLOAD}" > "$out"
38
38
  exit 0
39
39
  FAKE
40
40
  chmod +x "$FAKEBIN/codex"
@@ -47,7 +47,7 @@ cat > "$FAKEBIN/claude" <<'FAKE'
47
47
  #!/usr/bin/env bash
48
48
  cat >/dev/null # consume the prompt on stdin
49
49
  if [ -n "${FAKE_ENVELOPE:-}" ]; then printf '%s\n' "$FAKE_ENVELOPE"; exit 0; fi
50
- printf '{"is_error":false,"subtype":"success","structured_output":%s}\n' "$FAKE_PAYLOAD"
50
+ printf '{"is_error":false,"subtype":"success","structured_output":%s}\n' "${FAKE_PAYLOAD_CLAUDE:-$FAKE_PAYLOAD}"
51
51
  exit 0
52
52
  FAKE
53
53
  chmod +x "$FAKEBIN/claude"
@@ -201,6 +201,50 @@ printf '#!/usr/bin/env bash\nexit 1\n' > "$NOENT/od"; chmod +x "$NOENT/od"
201
201
  check "no CSPRNG (openssl+od stubbed to fail) → fails closed" 10 \
202
202
  env PATH="$NOENT:$PATH" FH_DRY_RUN=1 bash "$GATE" "package.json" quick test
203
203
 
204
+ echo
205
+ echo "── FH_BACKEND=cross (decorrelated review) ──"
206
+ # cross runs BOTH families and UNIONs. `auto` is fallback SELECTION and runs one leg; conflating the
207
+ # two would let a single-family verdict read as decorrelated, which is the defect class this mode
208
+ # exists to remove. These pairs pin: the union verdict, the leg accounting, and — most importantly —
209
+ # that a degraded (single-leg) run says so in machine-readable form.
210
+ CROSS_PASS='{"status":"SUCCESS","verdict":"PASS","findings_count":0,"findings_a":0,"findings_b":0,"findings":[]}'
211
+ CROSS_BLOCK='{"status":"SUCCESS","verdict":"BLOCKED","findings_count":1,"findings_a":1,"findings_b":0,"findings":[{"grade":"A","location":"x:1","title":"t","evidence":"e","fix":"f"}]}'
212
+
213
+ check_out() { # <name> <expected-exit> <grep-ere that MUST appear> -- <cmd...>
214
+ local name="$1" expect="$2" want="$3"; shift 3
215
+ local got
216
+ "$@" >"$TMPROOT/out" 2>"$TMPROOT/err"; got=$?
217
+ if [ "$got" -eq "$expect" ] && grep -qE "$want" "$TMPROOT/out"; then
218
+ printf 'PASS %-58s (exit %s)\n' "$name" "$got"; pass=$((pass + 1))
219
+ else
220
+ printf 'FAIL %-58s expected %s + /%s/, got %s\n' "$name" "$expect" "$want" "$got"
221
+ sed 's/^/ /' "$TMPROOT/out" | head -4; sed 's/^/ /' "$TMPROOT/err" | head -2
222
+ fail=$((fail + 1))
223
+ fi
224
+ }
225
+ run_cross() { # <claude-payload> <codex-payload>
226
+ env PATH="$FAKEBIN:$PATH" FH_BACKEND=cross FH_MODEL=fake \
227
+ FAKE_PAYLOAD_CLAUDE="$1" FAKE_PAYLOAD_CODEX="$2" FAKE_PAYLOAD="$1" \
228
+ bash "$GATE" "package.json" quick test
229
+ }
230
+
231
+ check_out "cross: both PASS → PASS, decorrelated" 0 'FH_GATE_DECORRELATED: yes' \
232
+ run_cross "$CROSS_PASS" "$CROSS_PASS"
233
+ # UNION, not vote: one leg blocking is enough. A majority rule would discard precisely the finding
234
+ # only the other family saw, which is the entire point of running two.
235
+ check_out "cross: one leg BLOCKED → union BLOCKED" 2 'FH_GATE_VERDICT: BLOCKED' \
236
+ run_cross "$CROSS_PASS" "$CROSS_BLOCK"
237
+ check_out "cross: both legs' findings survive the union" 2 'FH_GATE_LEGS: claude,codex' \
238
+ run_cross "$CROSS_BLOCK" "$CROSS_BLOCK"
239
+ # A machine with only one family is the COMMON case, not an edge case. It must not be silent.
240
+ ONELEG="$TMPROOT/oneleg"; mkdir -p "$ONELEG"; cp "$FAKEBIN/claude" "$ONELEG/claude"
241
+ check_out "cross: codex absent → single leg, DECORRELATED: no" 0 'FH_GATE_DECORRELATED: no' \
242
+ env PATH="$ONELEG:/usr/bin:/bin" FH_BACKEND=cross FH_MODEL=fake \
243
+ FAKE_PAYLOAD_CLAUDE="$CROSS_PASS" FAKE_PAYLOAD="$CROSS_PASS" \
244
+ bash "$GATE" "package.json" quick test
245
+ check "cross: no family available → fails closed" 10 \
246
+ env PATH="/usr/bin:/bin" FH_BACKEND=cross bash "$GATE" "package.json" quick test
247
+
204
248
  echo
205
249
  echo "────────────────────────────────────────────────────────────────────"
206
250
  printf 'fh-gate regressions: %d passed, %d failed\n' "$pass" "$fail"