@chrono-meta/fh-gate 1.4.87 → 1.4.89

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.87",
14
+ "version": "1.4.89",
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.87",
20
+ "version": "1.4.89",
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/CLAUDE.md CHANGED
@@ -602,6 +602,42 @@ Default operation is a **standard interactive session**. Agent dispatch (single
602
602
 
603
603
  **Why not Agent View by default**: Agent View introduces worktree isolation (blocks settings.json writes, Stop hook timing differs), session context gaps (session card stale content bug), and path friction — with no benefit unless the user is actively managing multiple agent sessions. Parallel agents via `Agent` tool work identically in a standard session.
604
604
 
605
+ **Fourth reason — gate-integrity in a worktree, and the answer is CONDITIONAL on how `core.hooksPath`
606
+ was set (measured 2026-08-05, both arms).** Do not carry a single verdict here; the two installs
607
+ behave differently:
608
+
609
+ | `core.hooksPath` | Which hook actually runs in a worktree | Consequence |
610
+ |---|---|---|
611
+ | **relative** — `templates/.git-hooks`, the form every FH doc installs (`CHEATSHEET.md`, `.claude/rules/fh_4axis_gate.md`, `install-wizard`, `self_evolution_routine.md`) | the **worktree's own copy** | Editing that copy *inside the worktree* disables the gate for that worktree — measured: neutralized hook → FH-asset commit with no marker succeeded (`rc=0`). The verifier becomes the verified, and the edit is invisible to `git status` in the main tree. |
612
+ | **absolute** — a hand-set full path (this operator's machine; **not** what any doc tells you to run) | the **main tree's copy** | A worktree-local edit has no effect; a known-positive is blocked there exactly as in the main tree (`rc=1`). |
613
+
614
+ An earlier draft of this section reported only the absolute-path arm and declared the
615
+ "worktree bypasses the gates" hypothesis *refuted* — from **n=1 on a non-canonical setting**, with a
616
+ do-not-revisit label attached. The relative-path arm, which is what everyone else runs, reproduces
617
+ the bypass. Freezing a conclusion is a defect when the measurement did not cover the shipped
618
+ configuration.
619
+
620
+ Separately and in **both** arms, the **evidence side** breaks: `tracks/` is gitignored, so it does not
621
+ follow into a worktree, so the Axis 2+3 marker and the Axis 4 `edit_manifest.yaml` are *structurally
622
+ absent* — an FH-asset commit in a worktree fails on evidence it has no way to have. That degrades
623
+ fail-closed (correct), but a gate that **cannot** be satisfied is what trains the bypass. Note the
624
+ hook itself prints `mkdir -p …/tracks/_meta` on that failure, i.e. the actor's own error message
625
+ teaches the marker-creation path — so "just don't fabricate it" is prose sitting under a machine
626
+ instruction pointing the other way.
627
+
628
+ **Therefore: do not commit FH assets from a worktree.** Not "carry the evidence in carefully" — a
629
+ carried marker and a fabricated one are byte-identical, so *marker provenance* is unenforceable by
630
+ construction. Land FH-asset changes from the standard session.
631
+
632
+ **Do not let that unenforceability launder the enforceable part** (caught by an adversarial round on
633
+ the paragraph above, which had used it to do exactly that): *being in a worktree* is trivially
634
+ detectable — `git rev-parse --git-common-dir` differs from `--git-dir` there and matches in the main
635
+ tree — and `templates/.git-hooks/pre-commit` currently has **zero** lines of worktree detection. A
636
+ true statement about one thing (provenance) was standing in for an untested claim about another
637
+ (location). It is left un-mechanized for a *scope* reason, not an impossibility one: measured
638
+ recurrence is 1, below this repo's own N≥3 mechanization threshold. If it recurs, the check is a
639
+ two-line hook addition, not a research problem.
640
+
605
641
  **Forbidden responses**: *"I can't do that — I'm not in that project's cwd"* — self-check Agent dispatch first.
606
642
 
607
643
  Mapped paths: check `auto_project_mapping.md` or `find ~/projects -maxdepth 1 -type d` for actuals.
@@ -196,6 +196,13 @@ destroys live state without anyone noticing. This is why the loss class is calle
196
196
  plaintext only (encoded tokens out of scope); a line-split backstop catches a token wrapped across
197
197
  lines; `PUBLIC_SURFACE_OK=1` overrides and is logged to a gitignored audit trail for the weekly audit.
198
198
  Residuals (split-encoding, override-not-populated, override abuse) are documented, not silent.
199
+ **Verdict labelling (2026-08-06)**: with the override absent, a clean scan reports
200
+ `⚠️ PARTIAL — company/companion literals UNMEASURED`, **never `✅ PASS`**. The commit still
201
+ proceeds (reversible surface → advisory degrade), but a run whose operator-literal layer never
202
+ executed may not present the same verdict as one where it did — a missing measurement is not a
203
+ zero. Anchored in `universal_guard_check.sh` as a **pair**: absent override must say `PARTIAL`,
204
+ and the control (override present, no hit) must still say `✅ PASS`, so the label cannot drift
205
+ back to a bare PASS *or* become an unconditional warning.
199
206
  - **(c) `npm publish`** — mechanically gated by `scripts/public_surface_scan_files.sh`, wired into
200
207
  `prepublishOnly` (`npm run release` also runs it *outside* the lifecycle). Unlike (b) it scans the
201
208
  **full content of the exact npm-published file set** (`npm pack --dry-run`), *not* a commit diff — so a
@@ -204,6 +211,36 @@ destroys live state without anyone noticing. This is why the loss class is calle
204
211
  or the file set are unresolved, when the parse looks partial, **or when the gitignored operator override
205
212
  is absent** — defaults-only would otherwise green-PASS a HIGH company literal on a fresh clone or CI runner.
206
213
 
214
+ **The git-push surface, and why it was the lenient one (2026-08-06).** (c) blocked on an absent
215
+ override; the `git push` gate in `templates/.git-hooks/pre-push` only warned. Both make content
216
+ public, so two irreversible surfaces were degrading in **opposite directions on the same state** —
217
+ the actual defect, and `git push` (the one nobody publishes through deliberately) was the permissive
218
+ side. The 2026-07-26 reasoning behind that warn was not wrong, it was **unscoped**: an absent
219
+ override in a fresh clone / CI runner / worktree is a legitimate per-operator configuration gap (the
220
+ file is gitignored, so it is absent there *by construction*), and blocking it trains
221
+ `PUBLIC_SURFACE_OK` into a reflex — which disarms the same channel the publish gate depends on.
222
+
223
+ So the warn is **scoped, not reverted**. `psa_detect_operator_context` (`scripts/psa_scan_lib.sh`)
224
+ splits the state: in an **operator-configured checkout** an absent override is *evidence missing
225
+ where evidence is expected* → BLOCK; everywhere else → WARN, exactly as before. The signal is
226
+ `CLAUDE.local.md`, the operator's own gitignored binding file.
227
+
228
+ **Calibration matters here more than the rule** — a second candidate signal, "`tracks/_meta` is
229
+ non-empty", reads as the same test and is not: `tracks/_meta/.gitkeep` and one sibling are
230
+ **tracked**, so a fresh clone satisfies it and would have been blocked, re-shipping the 07-26
231
+ over-block under a new name. It was rejected by measuring it against a known pair, not by reasoning
232
+ about it. **Named residual, deliberately in the under-blocking direction**: an operator who never
233
+ created a `CLAUDE.local.md` stays in the WARN arm. **Second residual, and it is the sharper one**:
234
+ deleting `CLAUDE.local.md` drops this checkout back into the WARN arm, and unlike `PUBLIC_SURFACE_OK=1`
235
+ that bypass **writes no log line**. It is a conscious act on the operator's own file, so it is not a
236
+ weak-model fail-open — but it is a quieter exit than the sanctioned one, which is the wrong ordering
237
+ for a bypass. Not closed here: making it loud means the hook must distinguish "never had one" from
238
+ "had one and lost it", and that needs state the hook does not currently keep. Named rather than
239
+ mechanized, per this repo's own threshold — mechanize on the first measured recurrence.
240
+ Anchored as a pair in `prepush_guard_check.sh`
241
+ (6-a absent override → PASS · 6-b absent override + operator checkout → BLOCK); both arms are
242
+ required, since 6-b passing alone would not distinguish a scoped block from a blanket one.
243
+
207
244
  **Named residuals for (c)** — it is a denylist **on the npm CLI path with scripts enabled**, not a
208
245
  universal secret-scanner:
209
246
 
@@ -944,3 +944,128 @@
944
944
  entry per the "counts and outcomes, not one stub per dispatch" rule.
945
945
  Instrument note: the invocation form matters — `| codex exec -` (stdin) as recorded in
946
946
  [[feedback_sidecar_liveness_not_passive_wait]]; the argv form hangs nondeterministically.
947
+
948
+ - date: '2026-08-04'
949
+ agent: codex/gpt-5.5 (cross-family sidecar, auto-decorrelation)
950
+ context: >-
951
+ FH self-dev — two load-bearing gate changes (PR #253 degrade-scan S5 rule, PR #254/#255 Axis-1
952
+ pathspec parity + anchor wiring). Pre-push gate demanded a cross-family leg on both.
953
+ dispatches: '3 (2 completed, 1 killed on timeout and re-run)'
954
+ outcome: accepted
955
+ evidence: >-
956
+ Round 1 (PR #253): REFUTED the author's own fix. The first repair widened the S5 rule with a
957
+ NAMED filter list; codex produced reproduced counter-examples in BOTH directions — still-missed
958
+ real disarms (`grep -Ec`, `grep --count`, `grep -Fcx`, `| cat`) and a NEW false positive the
959
+ repair introduced (`| tail -n +2` yields one line, not a disarm). It also caught that a fixture
960
+ labelled known-NEGATIVE was in fact a positive, because its upstream succeeded and therefore
961
+ never exercised the failure path it claimed to pin. All four re-run locally before acceptance.
962
+ Round 2 (PR #255 wiring): no blocker; one residual raised ("a distribution that SHOULD be
963
+ complete but drops a subject reports SKIP, not FAIL"), which was then TESTED and confirmed
964
+ uncovered by any existing anchor. Recorded as a WEAK signal — the auditor stated it could not
965
+ trace F1-F3 because those implementations were not in the diff it was given, and that residual
966
+ was closed locally by measurement, not by the auditor.
967
+ note: >-
968
+ Third consecutive session where the cross-family leg produced net-new findings the same-family
969
+ round walked past. New this session: the two rounds differed in SIGNAL STRENGTH (refutation vs
970
+ nothing-found), and both markers record which one it was — recording them at equal weight would
971
+ launder a weak pass into a strong one.
972
+ Instrument note: the FIRST attempt derailed into its own sidecar-availability exploration and
973
+ timed out (exit 124, ~7 min). Re-running with an explicit "Do NOT run shell commands, do NOT use
974
+ tools, answer ONLY from the text below" preamble fixed it. Stdin form `codex exec -m gpt-5.5
975
+ --skip-git-repo-check -` as recorded in [[feedback_sidecar_liveness_not_passive_wait]].
976
+ These were CLI sidecars, not Agent-tool spawns, so the SubagentStop hook tallied ZERO — the
977
+ ④-e check passed on a true zero while three real dispatches had happened. Named gap: the tally
978
+ is blind to CLI-invoked sidecars.
979
+
980
+ - date: '2026-08-05'
981
+ agent: (unattributed — SubagentStop tally only)
982
+ context: >-
983
+ Close chain of the 2026-08-04 session, which crossed midnight. The tally file recorded ONE
984
+ dispatch dated 2026-08-05; this session made no Agent-tool spawn that it is aware of.
985
+ dispatches: '1 (tallied, origin not attributable)'
986
+ outcome: sustained
987
+ evidence: >-
988
+ Recorded rather than guessed. The substantive sidecar work of this session is the 2026-08-04
989
+ entry above (3 codex CLI dispatches). MEASURED here, after the claim had already been written
990
+ into a commit message and the session card: a `codex exec` call does NOT increment the tally
991
+ (before=1, after=1 on `grep -c '^2026-08-05$'`), and the hook matcher is `SubagentStop`, i.e.
992
+ Agent-tool spawns only. So the CLI-blindness claim holds — but its evidence arrived AFTER
993
+ publication, which is the ordering CLAUDE.md §Instrument-Calibration forbids.
994
+ note: >-
995
+ What produced the 4 tallies dated 2026-08-04 and this 1 dated 2026-08-05 is UNKNOWN. Not
996
+ invented: a fabricated outcome would poison the 60/40 promotion gate worse than a missing one,
997
+ which is the stated reason the hook only tallies and never writes an entry. Open question for
998
+ the next session that touches the tally: which tool events actually fire SubagentStop here.
999
+
1000
+ - date: '2026-08-05'
1001
+ agent: persona-innovator ×2 · quench-challenger ×3 · beginner ×5 (Agent tool) + codex gpt-5.5 ×2 (CLI)
1002
+ context: >-
1003
+ Innovator autopilot session. Two sister-asset absorption scans (innovator); steel-quench Wave 1
1004
+ and Wave 3 on the selfcheck run-twice repair, plus one opus Axis-2 pass on the AGENTS.md change
1005
+ (quench-challenger); five Sonnet-pinned cold-read sims on one AGENTS.md checklist item
1006
+ (beginner); two cross-family audits (codex CLI, tool-use disabled — not counted by the tally
1007
+ hook, which only fires on Agent-tool SubagentStop).
1008
+ dispatches: '10 via Agent tool + 2 CLI sidecars (tally recorded 14 — the delta is unattributed,
1009
+ see the 2026-08-05 entry above about unknown tally sources; not invented here either)'
1010
+ outcome: accepted
1011
+ evidence: >-
1012
+ Every class changed the outcome, which is why this is `accepted` rather than `partial`.
1013
+ quench-challenger: Wave 1 produced 2S/3A/4B and Wave 3 produced 2S/1A/3B on the REPAIR — one of
1014
+ those overturned a judgment already reported to the operator (a "refuted" verdict measured on a
1015
+ non-canonical absolute core.hooksPath; the documented relative-path install reproduces the
1016
+ bypass). The opus Axis-2 pass produced 2S that caused a full revert of the AGENTS.md change.
1017
+ beginner: 5 rounds, each blocking at a DIFFERENT point, each block introduced by the previous
1018
+ round's repair; author self-catch across rounds 1-4 was ZERO. innovator: 4 external repos
1019
+ verified by gh api with known-pair controls, 3 net-new items confirmed by in-repo greps with
1020
+ live controls. codex: round 1 was fed a STALE diff (my error — the target was not frozen) so 2
1021
+ of its findings were already closed and 1 was a misread; round 2 on a fingerprinted target found
1022
+ the BSD `tr -d '[:space:]'` defect that both same-family rounds missed.
1023
+ note: >-
1024
+ Logged at close, after the session_close_check ④-e block caught the omission — the obligation
1025
+ is prose in CLAUDE.md and I did not discharge it during the session despite dispatching 10+
1026
+ agents. That is the same shape the ④-e check was built for. Consolidated per class with counts
1027
+ rather than one stub per dispatch, which the check explicitly permits.
1028
+
1029
+ - date: '2026-08-06'
1030
+ agent: beginner ×5 (Sonnet-pinned cold read) · quench-challenger ×1 (opus Axis-2) — same session as 2026-08-05
1031
+ context: >-
1032
+ The innovator autopilot session crossed midnight; the tally records 14 dispatches under
1033
+ 2026-08-06 while the substantive description lives in the 2026-08-05 entry above. These are not
1034
+ additional work — they are the same session's later half: the five cold-read rounds on the
1035
+ AGENTS.md worktree item and the opus adversarial pass that ended it.
1036
+ dispatches: '6 attributable here (tally: 14 under this date; the remainder shares the
1037
+ unattributed-source question already recorded on 2026-08-05)'
1038
+ outcome: accepted
1039
+ evidence: >-
1040
+ The opus Axis-2 pass returned 2S — a recipe that commits to the WRONG BRANCH (git forbids two
1041
+ worktrees sharing a branch, so the main checkout and a worktree are always on different
1042
+ branches, and the recipe never aligned them), and a hook that hands out a marker-fabrication
1043
+ one-liner at the exact moment it blocks. Both were reproduced against the live repo, and the
1044
+ change was REVERTED in full rather than patched. The five beginner rounds each blocked at a
1045
+ different point with author self-catch of zero across rounds 1-4.
1046
+ note: >-
1047
+ Written after the close check refused the push a SECOND time: the first correction logged the
1048
+ session under 2026-08-05 while the check counts today's date, so "logged" and "logged where the
1049
+ check looks" came apart. Recorded rather than back-dated — the split is real (the tally itself
1050
+ splits 34/14 across the boundary) and pretending otherwise would misstate when the work ran.
1051
+
1052
+ - date: 2026-08-06
1053
+ agent: general-purpose ×2 (블라인드 응시자), fh-commons:quench-challenger ×2 (Axis 2)
1054
+ purpose: >-
1055
+ qasp 풀체인 단련 — 출제자/응시자 분리 블라인드 실행 2회(2막 대조 · 3층 난이도)와,
1056
+ FH 마감 체커 fail-open 수리에 대한 Axis 2 적대검증 2라운드(sonnet → at-floor opus).
1057
+ model: sonnet ×3, opus ×1
1058
+ outcome: accepted
1059
+ evidence: >-
1060
+ 블라인드 2회가 계기 결함 2건을 직접 산출했다 — 1차는 두 런 차이를 "플레이키"로 오귀속해
1061
+ 리포트에 코드 상태 지문이 없음을 드러냈고(#84→PR #86), 2차는 counts 만 대조해 "변화 없음"이라
1062
+ 결론내 집계가 개별을 가리는 문제를 드러냈다(→PR #87). 두 오귀속 모두 **주어진 산출물만 보고는
1063
+ 그렇게 결론낼 수밖에 없었다** — 그래서 결함이 응시자가 아니라 계기 쪽임이 판별됐다.
1064
+ 챌린저 R1(sonnet)은 수리 대상 바로 윗줄에 같은 fail-open 이 남아 있음을 재현으로 잡았고,
1065
+ 게이트가 floor 미달로 거부해 R2(opus, at-floor)를 돌리자 R1 이 못 본 4건이 추가로 나왔다
1066
+ (재발견 0건, 전부 실제 재현). known-pair 6/6.
1067
+ note: >-
1068
+ 45 dispatch 대 1 로그 항목으로 ④-e 가 통과한 상태였다 — 이 항목이 그 간극을 메운다.
1069
+ 카운트가 큰 것은 Explore 등 내부 에이전트가 함께 집계되기 때문이고, 의미 있는 위임은 위 4건이다.
1070
+ ★방법론 소득이 결함보다 크다: 출제자=응시자면 "잘 나온다"로 끝나고, 분리하면 매 라운드
1071
+ 계기 결함이 나온다. 그리고 floor 는 장식이 아니었다 — sonnet R1 이 놓친 HIGH 를 opus R2 가 잡았다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrono-meta/fh-gate",
3
- "version": "1.4.87",
3
+ "version": "1.4.89",
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/selfcheck.sh && bash scripts/public_surface_scan_files.sh",
29
+ "prepublishOnly": "bash scripts/version_lockstep_check.sh && bash scripts/selfcheck.sh && bash scripts/public_surface_scan_files.sh",
30
30
  "release": "bash scripts/public_surface_scan_files.sh && npm publish"
31
31
  },
32
32
  "engines": {
@@ -64,7 +64,9 @@
64
64
  "scripts/fh-goal.sh",
65
65
  "scripts/count_check.sh",
66
66
  "scripts/selfcheck.sh",
67
+ "scripts/version_lockstep_check.sh",
67
68
  "scripts/test_selfcheck_state_lanes.sh",
69
+ "scripts/test_version_lockstep_lanes.sh",
68
70
  "scripts/package_coverage_check.sh",
69
71
  "scripts/test_package_coverage_lanes.sh",
70
72
  "scripts/test_fh_gate_regressions.sh",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-commons",
3
- "version": "1.4.87",
3
+ "version": "1.4.89",
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.87",
3
+ "version": "1.4.89",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -164,9 +164,41 @@ for f in "${FILES[@]}"; do
164
164
  # yields "9\n0" and the `-gt` guard goes silent. Verified as a known pair (both directions) in
165
165
  # scripts/test_degrade_scan_shell_probes.sh; narrowing without that anchor would just trade a
166
166
  # noisy probe for a blind one.
167
+ #
168
+ # WIDENED 2026-08-04 — the narrowing had gone one step too far, and the proof is that this
169
+ # probe was BLIND to the live instance that bit us the day before. PR #251 fixed
170
+ # `grep -c … | tr -d ' ' || echo 0` in session_close_check.sh ④-e; running this scanner over
171
+ # the pre-#251 file produced ZERO hits (re-measured: fixed rule = 2, old rule = 0).
172
+ #
173
+ # THE ANCHOR WAS ON THE WRONG PROPERTY. The old regex asked "is there an upstream pipe" and
174
+ # required the counter to be the LAST stage. Neither is the discriminator. The discriminator
175
+ # is: **does the failing side still EMIT?** `grep -c` prints "0" and exits 1 on no-match, so it
176
+ # disarms with no upstream pipe at all, and any trailing stage that passes that "0" through
177
+ # keeps the "0\n0" intact.
178
+ #
179
+ # A first fix widened to a NAMED filter list (tr|head|sed|…). Cross-family review (gpt-5.5,
180
+ # 2026-08-04) refuted it with reproduced counter-examples, and the refutation held on
181
+ # measurement in BOTH directions:
182
+ # · still missed, all verified to yield "0\n0": `grep -Ec …`, `grep --count …`, `grep -Fcx …`
183
+ # (combined/long flag forms the `-c` literal never matched) and `| cat`.
184
+ # · newly over-matched: `| tail -n +2` after the counter yields ONE line — not a disarm.
185
+ # Name-based approximation produced under-match and over-match simultaneously. So the trailing
186
+ # chain is no longer enumerated: the rule keys on the COUNTER (grep with a `c` in any flag
187
+ # cluster or `--count`; or `wc` behind a real pipe) plus the `|| echo 0` fallback, and accepts
188
+ # any chain between them.
189
+ #
190
+ # PRESERVED from the 2026-07-28 narrowing, still pinned as known-negatives: (a) `a || b ||
191
+ # echo 0` has no pipeline, and (b) a stage that emits NOTHING on failure (`| jq -r … ||
192
+ # echo 0`) — there the fallback supplies the only line, as intended.
193
+ # KNOWN RESIDUAL, stated not hidden: a trailing stage that can swallow the counter's output
194
+ # (`| tail -n +2`, `| sed -n '/[1-9]/p'`) is now flagged though it is not a disarm. That is a
195
+ # deliberate recall-over-precision trade on an ADVISORY probe, taken because the measured cost
196
+ # of the other direction was a real defect shipping. Revisit if non-fixture FPs appear.
197
+ # Known pair: scripts/test_degrade_scan_shell_probes.sh (9 positives / negatives silent).
198
+ # Reverting this line turns the positive lane red — checked by applying the revert.
167
199
  while IFS= read -r m; do
168
- emit "$f" "${m%%:*}" "S5:pipefail-fallback(sh)" "\`|| echo 0\` fallback on a pipeline ending in a counter (grep -c/wc) — that stage emits even when an upstream stage fails, so under \`set -o pipefail\` the value gains a SECOND line, the integer comparison errors out, and the guard passes silently; split the pipeline and sanitize to an integer"
169
- done < <(grep -nE '[^|]\|[[:space:]]*([a-z]+[[:space:]]+)*(grep[^|]*-c|wc)[^|]*\|\|[[:space:]]*echo[[:space:]]+[\"'"'"']?0' "$f" 2>/dev/null \
200
+ emit "$f" "${m%%:*}" "S5:pipefail-fallback(sh)" "\`|| echo 0\` fallback after a COUNTER (grep with -c / --count, or wc) — the counter PRINTS \"0\" and exits non-zero on no-match, so the fallback appends a SECOND line, the value becomes \"0\\n0\", the integer test dies with 'integer expression expected' (stderr only) and the guard goes silent; split it and sanitize: n=\$(...); n=\${n:-0}; case \$n in *[!0-9]*) n=0;; esac. VERIFY the trailing stage can actually emit — if it swallows the count (| tail -n +2), this is a known false positive"
201
+ done < <(grep -nE '(([^|]\|[[:space:]]*([a-z]+[[:space:]]+)*wc[^|]*)|(grep([[:space:]]+-[A-Za-z]*c[A-Za-z]*|[[:space:]]+--count)[^|]*))([^|]*\|[^|]*)*\|\|[[:space:]]*echo[[:space:]]+[\"'"'"']?0' "$f" 2>/dev/null \
170
202
  | grep -vE '^[0-9]+:[[:space:]]*#' \
171
203
  | grep -vE '#[[:space:]]*noqa[:[:space:]]*degrade')
172
204
  fi
@@ -107,7 +107,12 @@ spec_matches() { # $1 = path
107
107
  for pair in \
108
108
  "plugins/fh-meta/skills/frontier-digest/SKILL_detail.md|tracks/_meta/x.md|PATHSPEC covers SKILL_detail.md" \
109
109
  "plugins/fh-meta/skills/frontier-digest/SKILL.md|README.md|PATHSPEC still covers SKILL.md" \
110
- "CLAUDE.md|CLAUDE.local.md|PATHSPEC covers CLAUDE.md but not the local override"
110
+ "CLAUDE.md|CLAUDE.local.md|PATHSPEC covers CLAUDE.md but not the local override" \
111
+ "scripts/selfcheck.sh|README.md|PATHSPEC covers scripts/*.sh (seam #1 — the HEAVY term already gated it, this array did not)" \
112
+ "scripts/sub/nested.sh|package.json|PATHSPEC covers scripts/ recursively (git/bash globs cross '/')" \
113
+ "templates/regression_guard.sh|package-lock.json|PATHSPEC covers templates/*.sh — i.e. the guard can guard ITSELF" \
114
+ "templates/.git-hooks/pre-commit|.gitignore|PATHSPEC covers the git-hook floor (the hook that hard-blocks commits)" \
115
+ "plugins/fh-meta/agents/challenger.md|tracks/_meta/y.md|PATHSPEC covers agent definitions (seam #3)"
111
116
  do
112
117
  IFS='|' read -r pos neg label <<< "$pair"
113
118
  ok=1
@@ -166,6 +166,21 @@ check "operator override absent (per-operator) → PASS " "$R" pass \
166
166
  "refs/heads/f $(cd "$R" && git rev-parse HEAD) refs/heads/f $B"
167
167
  rm -rf "$R"
168
168
 
169
+ # ── Pair 6-b (2026-08-06): the OTHER arm of the same state, which the 07-26 flag could not see.
170
+ # Identical to 6-a except the checkout carries CLAUDE.local.md — the operator's gitignored binding
171
+ # file, absent in every clone/CI/worktree by construction. There the missing override is not an
172
+ # unset per-operator config, it is evidence missing where evidence is expected, on a surface that
173
+ # publishes. This is what `npm publish` already blocked while `git push` waved through; the two
174
+ # irreversible surfaces now degrade in the same direction.
175
+ # The PAIR is what makes it a measurement: 6-a (no CLAUDE.local.md → PASS) is the known-negative and
176
+ # must keep passing, or this is not a scoped block, it is the 07-26 over-block re-shipped. ──
177
+ R=$(newrepo); B=$(cd "$R" && git rev-parse HEAD)
178
+ ( cd "$R" && echo ok > g2.md && git add g2.md && git commit -qm g2 >/dev/null \
179
+ && rm -f .claude/rules/.public-surface-patterns && printf '# operator binding\n' > CLAUDE.local.md )
180
+ check "override absent + operator checkout → BLOCK" "$R" block \
181
+ "refs/heads/f $(cd "$R" && git rev-parse HEAD) refs/heads/f $B"
182
+ rm -rf "$R"
183
+
169
184
  R=$(newrepo); B=$(cd "$R" && git rev-parse HEAD)
170
185
  ( cd "$R" && echo ok > g.md && git add g.md && git commit -qm g >/dev/null \
171
186
  && : > .claude/rules/.public-surface-patterns.defaults ) # present but EMPTY = broken, not unconfigured
@@ -38,6 +38,9 @@
38
38
  # PSA_DEFAULTS_OK 1 = committed defaults present, readable, non-empty
39
39
  # PSA_OVERRIDE_PRESENT 1 = operator override present, readable, non-empty
40
40
  # PSA_BAD_ROWS count of rows dropped as unusable (uncompilable regex, or no TAB)
41
+ # State set by psa_detect_operator_context (separate call — see that function):
42
+ # PSA_OPERATOR_CONTEXT 1 = an operator-configured checkout, where an absent override is missing
43
+ # evidence rather than a legitimate fresh-clone absence
41
44
  # A caller that treats PSA_BAD_ROWS>0 or PSA_DEFAULTS_OK=0 as "clean" has a hole: those states mean
42
45
  # the instrument is incomplete, and an incomplete instrument cannot certify anything.
43
46
 
@@ -115,6 +118,39 @@ PSA_ROWS
115
118
  PSA_STREAM="$valid"
116
119
  }
117
120
 
121
+ # psa_detect_operator_context <repo_root> → sets PSA_OPERATOR_CONTEXT (0/1)
122
+ #
123
+ # Answers ONE question: is an absent operator override a *legitimate absence* (fresh clone, CI
124
+ # runner, worktree — the file is gitignored, so it is absent there by construction) or *evidence
125
+ # missing where evidence is expected* (a checkout the operator has configured, whose literals should
126
+ # be there and are not)?
127
+ #
128
+ # 2026-07-26 changed pre-push from BLOCK to WARN on an absent override, and that change was right for
129
+ # the first case: this repo's own selfcheck flagged the block as over-firing, and over-blocking trains
130
+ # PUBLIC_SURFACE_OK into a reflex, which disarms the same channel the publish gate depends on. It was
131
+ # wrong for the second case, which the flag could not distinguish. This function is that distinction —
132
+ # it does not reopen the reverted block, it scopes it.
133
+ #
134
+ # Signal: CLAUDE.local.md — the operator's own gitignored binding file. Chosen because it is
135
+ # gitignored (verified: `.gitignore:17`), so no clone, CI checkout or worktree carries it.
136
+ #
137
+ # CALIBRATED, and one candidate was REJECTED by that calibration: "tracks/_meta is non-empty" looks
138
+ # like the same signal and is not — `tracks/_meta/.gitkeep` and one more file are TRACKED, so a fresh
139
+ # clone satisfies it and would have been blocked. The discriminator was measured against a known
140
+ # pair before it was trusted, not reasoned about.
141
+ #
142
+ # NAMED RESIDUAL (deliberate, and in the safe direction): an operator who never created a
143
+ # CLAUDE.local.md stays in the WARN arm. This under-blocks rather than over-blocks — the failure mode
144
+ # this scoping exists to avoid is the reflex, not the miss, and the push gate's content scan still
145
+ # runs on the committed defaults in that arm.
146
+ psa_detect_operator_context() {
147
+ local root="$1"
148
+ PSA_OPERATOR_CONTEXT=0
149
+ [ -n "$root" ] || return 0
150
+ [ -f "$root/CLAUDE.local.md" ] && PSA_OPERATOR_CONTEXT=1
151
+ return 0
152
+ }
153
+
118
154
  # psa_scan_tagged — reads "path<TAB>content" lines on stdin, prints one line per reportable hit.
119
155
  # Returns 0 = nothing reportable, 1 = at least one hit.
120
156
  #