@chrono-meta/fh-gate 1.4.86 → 1.4.87

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.
@@ -868,3 +868,79 @@
868
868
  just declared success. Adversarial review does not substitute for first use; today is the mirror:
869
869
  self-repair does not substitute for adversarial review. Axis 2 shipped NOT CONVERGED with the open
870
870
  leak channel declared in the header rather than closed by assertion.'
871
+
872
+ - date: 2026-08-03
873
+ agent: codex gpt-5.5 (cross-family, stdin form) + fh-meta:challenger (isolated Agent) + one headless
874
+ Sonnet blind sim (claude -p, tools stripped, repo-external cwd)
875
+ task: Axis 2 + target-tier sim for the ablation known-pair calibration — scripts/ablation_calibrate.sh
876
+ (new), scripts/probe_scope_check.sh header, .claude/regression/probes.md rows G-GATE-08/09
877
+ mode: two families dispatched on the same diff; consolidated here as one entry with measured counts
878
+ outcome: accepted
879
+ evidence: 'Codex 4 critical / 4 high / 3 medium; challenger 2S / 7A / 2B. The two families found
880
+ DIFFERENT classes and only partly overlapped. Codex: stderr merged into the scored stream, so a
881
+ single input echo passes P, N and T at once (every control token lives in the input); no runner
882
+ exit-status check; unanchored grep -q 47 accepting 470/1147; baseline gated on a shared fail flag.
883
+ Challenger (executed against the shipped script, not reasoned): control T conflated "could not read"
884
+ with "read it and the answer was not there" and certified a fully tool-enabled runner as calibrated;
885
+ the two arms shared one cwd so the negative arm could read the answer off its neighbour; probes.md
886
+ Count line shipped stale INSIDE this diff (30/mp25 vs actual 32); G-GATE-09 typed mandatory-pass
887
+ while being mechanically unobservable; the public script cited gitignored tracks/** records four
888
+ times as its evidence. All reproduced before acting. Sonnet blind sim of the repaired header: PASS —
889
+ recovered calibrate-first, pre-registration, reps>=3, the allowlist form, and answered "no" to
890
+ "one rep, arm B answered, can you record CUT?".'
891
+ note: 'Second time in one day that the miss landed where self-repair had just declared success. New
892
+ this round: the ANCHOR ITSELF was decorative on first try — reverting three fixes left the lane file
893
+ 15/15 green. Two different causes: two lanes did not carry an input that exposes the defect (they
894
+ went red for an unrelated reason and read as coverage), and one revert silently did not apply. A
895
+ revert is an instrument too; measure that it took effect before believing the anchor.'
896
+
897
+ - date: 2026-08-03
898
+ agent: fh-meta:challenger (isolated Agent) ×6 across three branches + codex gpt-5.5 (cross-family)
899
+ ×1 + headless Sonnet target-tier sims ×6 (claude -p, tools stripped, repo-external cwd)
900
+ task: session 2 — Axis 2 for the CLAUDE.md gate-scope pointer fix (4R), the templates/ domain-scoped
901
+ sections fix (1R), the probe_scope_check carryovers (1R), plus the cross-family leg on the
902
+ load-bearing pointer change and the before/after known-pair sims for each salience change
903
+ mode: consolidated entry with measured counts, per the log's own allowance
904
+ outcome: accepted
905
+ evidence: 'Pointer branch 0S3A → 1S5A → 1S6A → 1S2A; template branch 0S/3A; probe-scope branch
906
+ 0S/3A/5B. The cross-family leg found in ONE pass what four same-family rounds had walked past —
907
+ the record asserted "REDUCED, not withdrawn" in one place and "withdrawn" in three others, 40
908
+ lines apart — and named the mechanism: the file performs visible self-correction in a persuasive
909
+ narrative voice, which makes an internal contradiction easy to wave through. Three separate
910
+ rounds independently pushed toward REDUCTION rather than tightening, and all three reductions
911
+ held: a second tool control, a prose/mechanism parity check, and a `[ -r ]` guard were each
912
+ measured inert or finding-generating and removed.'
913
+ note: 'The dominant new-defect class this session was MY OWN ANCHORS BEING DECORATIVE — four
914
+ instances, four different causes (a lane carrying no input that exposes the defect; a revert that
915
+ silently did not apply; a needle matching unrelated prose; a stub short-circuiting the layer the
916
+ guard lives in). None was self-caught. Harvested to [[feedback_anchor_can_be_decorative]].'
917
+
918
+ - date: 2026-08-04
919
+ agent: codex (gpt-5.5) — cross-family adversarial sidecar
920
+ invoked_by: FH self-dev session (card carry-over autopilot)
921
+ task: >-
922
+ Two separate adversarial audits. (1) A pre-commit hook trigger split plus a new known-pair
923
+ section in scripts/gate_pathspec_check.sh. (2) The §ⓐ restriction-union merge spec in
924
+ knowledge/shared/harness-core/capability_composition_contract.md.
925
+ outcome: accepted
926
+ evidence: >-
927
+ Audit (1): 3 defects, same-family inline pass found 0. The load-bearing one was a worktree-vs-
928
+ staged FAIL-OPEN — the checker read the worktree copy of CLAUDE.md, so staging a broken asset
929
+ line and then repairing the worktree WITHOUT staging passed the gate; reproduced before fixing
930
+ (index 0 occurrences / worktree 1 / anchor exit 0) and re-measured closed after (exit 1 both
931
+ directions). The other two: an `agents/` needle that passed on a partial class drop, and a stale
932
+ comment instructing the next maintainer to restore the exact coupling the change removed.
933
+ Codex explicitly CLEARED the LOADBEARING separation and the bash-3.2 quoting — a negative result
934
+ recorded because "found nothing here" is also evidence.
935
+ Audit (2): 5 defects, inline pass found 4 of them independently. The one net-new finding was the
936
+ worst of the set — §ⓐ.3 check 2 was written `merged ⊒ l` while §ⓐ.1 defined `a ⊑ b` as "a is at
937
+ least as strict as b", i.e. the spec's only mechanical direction check asserted the INVERSE of
938
+ its own invariant. Fixed by removing the glyph and writing every strictness claim as a
939
+ `permits(...) ⊆` relation.
940
+ note: >-
941
+ Second consecutive session where the cross-family leg produced net-new findings that same-family
942
+ rounds walked past, and where the net-new finding was the most severe one. Counted 5 dispatches
943
+ in the SubagentStop tally (2 codex audits plus 3 harness-side spawns); consolidated into this one
944
+ entry per the "counts and outcomes, not one stub per dispatch" rule.
945
+ Instrument note: the invocation form matters — `| codex exec -` (stdin) as recorded in
946
+ [[feedback_sidecar_liveness_not_passive_wait]]; the argv form hangs nondeterministically.
@@ -91,12 +91,75 @@ governance depth to condition 4 (mechanical-anchor / 4-axis gate) — the litmus
91
91
 
92
92
  | # | Item | Source → target | Effect |
93
93
  |---|---|---|---|
94
- | 1 | Session rules | `templates/.claude/rules/session.md` → `{project}/.claude/rules/session.md` | Session-start auto-read, backup, rule hierarchy |
94
+ | 1 | Session rules | `templates/.claude/rules/session.md` → `{project}/.claude/rules/session.md` **→ then §6-b prune + substitute (the copy is not the install)** | Session-start auto-read, backup, rule hierarchy |
95
95
  | 2 | Context filter | `templates/.claudeignore` → `{project}/.claudeignore` | Token footprint control |
96
96
  | 3 | Env card | `templates/fh_env_context.jsonc` → `{project}/.claude/rules/fh_env_context.jsonc` | Environment context for sessions |
97
97
  | 4 | **MCP tool gating** (conditional — offered only when the project mounts an external MCP server: `.mcp.json`/`mcp.json` present, or the user is adding one) | `templates/.claude/rules/mcp_tool_gating.md` → `{project}/.claude/rules/mcp_tool_gating.md` | Name-keyed ask/allow tiers for external MCP tools — server annotations are unreliable (measured 2026-06-11: a live server shipped all-None hints incl. its irreversible send tool); §3 table filled at mount time |
98
98
  | 5 | **Official-plugin scan** (recommend list only — **never auto-install**) | plugin-recommender Tier 0/1 pass on the project's stack | No-reinvention acceleration: matching `*-lsp` for the project's language + workflow plugins (code-review · commit-commands · feature-dev …) from `claude-plugins-official` — see `knowledge/shared/plugin-catalog/recommended_plugins.md` §Category 0.5. Each install user-approved |
99
99
 
100
+ ### 6-b. Prune + substitute — the step that was missing, and the measurement that says so
101
+
102
+ **A copy is not an install.** Item 1 above used to end at the copy, and the field result was measured
103
+ on 2026-08-04 across every repo that inherited this template:
104
+
105
+ | Repo | `{FH_ROOT}` raw | `[CUSTOMIZE]` raw |
106
+ |---|---|---|
107
+ | A (bash/python wiki engine) | 1 | 3 |
108
+ | B | 2 | 6 |
109
+ | C (a *template* repo — identical is correct here) | 2 | 6 |
110
+ | D | **0** | **0** — the one that was customized by hand |
111
+
112
+ Three of four shipped with the hub-path placeholder unresolved. The earlier repair went to the
113
+ *template* (marking domain-scoped sections for deletion, 2026-08-04) and measured a real gain — but
114
+ only *when someone asked*. The field failure is that **there was no moment of asking**. This is that
115
+ moment; it is a step, not a better sentence.
116
+
117
+ Run immediately after each `templates/` copy, in the target repo:
118
+
119
+ ```bash
120
+ TGT={project}/.claude/rules/session.md
121
+ # 1. SUBSTITUTE — the hub path this project should point at
122
+ FH_ROOT_ABS=${FH_ROOT_ABS:-$HOME/projects/forge-harness} # adjust if the hub lives elsewhere on this machine
123
+ sed -i.bak "s|{FH_ROOT}|$FH_ROOT_ABS|g" "$TGT" && rm -f "$TGT.bak" # -i.bak = the form BSD requires and GNU accepts
124
+
125
+ # 2. PRUNE — one ask per SECTION-HEADING marker, then delete the whole section (not just the marker)
126
+ grep -n '<!-- \[CUSTOMIZE\] DOMAIN-SCOPED' "$TGT"
127
+
128
+ # 3. VERIFY — Done-When. BOTH must print 0.
129
+ grep -c '{FH_ROOT}' "$TGT"
130
+ grep -c '<!-- \[CUSTOMIZE\] DOMAIN-SCOPED' "$TGT"
131
+ ```
132
+
133
+ **Step 3 is the Done-When** *[mandatory-pass]* — and the two counts are deliberately narrow.
134
+
135
+ - **Why `{FH_ROOT}`**: an unresolved substitution token. It is the exact thing three of four inherited
136
+ repos shipped raw.
137
+ - **Why only the `<!-- ... -->` form of `DOMAIN-SCOPED`**: those three markers head a deletable
138
+ section. The template also *mentions* the phrase in prose ("TWO are DOMAIN-SCOPED and belong only
139
+ to…"), which is documentation and must survive — counting every occurrence would make the target
140
+ unreachable.
141
+ - **Why `[CUSTOMIZE]` is NOT counted**: the template's own instruction line reads "Change sections
142
+ marked with [CUSTOMIZE] comments to match your project". A zero target for that string can never be
143
+ met while the file explains itself, and an unreachable Done-When trains exactly one behaviour —
144
+ **deleting the marker instead of doing the work** — which is the defect this step exists to prevent.
145
+ `[CUSTOMIZE]` stays an editing hint, judged by the installer, not a counted gate.
146
+
147
+ > That distinction is not theory. The first draft of this step counted all three strings and demanded
148
+ > zero; running it on a fresh copy of the template returned **2 after a full prune** — both from lines
149
+ > that must stay. The instrument was checked against a known pair before this step shipped, which is
150
+ > the only reason the unreachable target did not become the rule (`§Instrument-Calibration`).
151
+
152
+ Do **not** silence a count by deleting the marker alone: the markers are attached to sections, and a
153
+ marker removed from a section that stayed is the same defect with its evidence erased.
154
+
155
+ **Do not split the template into core+domain modules.** The 2026-08-04 measurement pointed at the
156
+ *install step*, not at the file's shape, and this repo's own history says a new mechanism introduced
157
+ speculatively generates its own defects. The markers plus this step are the fix.
158
+
159
+ **Residency**: run this inside the target repo only. One of the counted repos is company-adjacent —
160
+ it was counted and left untouched, and its contents are not reproduced here or anywhere outside its
161
+ own environment.
162
+
100
163
  **Field-asset scaffold (on-demand — NOT a `templates/` install)**: harness-ification surfaces field-specific skills/agents *as needed* — the field's domain work produces them. FH accelerates their *creation*: not the domain content (the field team authors that), but the **gate-compliant structure**. When a skill-worthy recurring pattern appears (3+ reps → `#skill-candidate` tag · `field-harvest` signal · a repeated manual workflow), offer to scaffold a skeleton:
101
164
 
102
165
  - **Skill skeleton** — the frontmatter field set (`name`/`description`/`user-invocable`/`allowed-tools`) that is universal across active FH skills (regen: `grep -L user-invocable plugins/*/skills/*/SKILL.md` returns none; `model:` is deliberately NOT universal since 2026-07-10 — hard pins retired per `sonnet_floor_doctrine.md`, skills session-inherit and express depth as dispatch) + **four stubs, all mandatory in the skeleton**: ① Trigger stub (≥3 phrases — initiate) ② Done-When stub (with check-class — complete) ③ halt stub (budget/convergence guard) ④ persist stub (what state reaches the next run) + Step skeleton — the 5-question loop discipline by construction (`loop_engineering.md`; validate=#3 is the check-class declaration in ②). **Parameterizes** `contention-layer` Step 4's skeleton template (per its need-driven note — `origin: field-scaffold`, `contention-parents`→source-pattern pointer).
@@ -41,6 +41,32 @@ The hub audits and improves itself weekly.
41
41
 
42
42
  ---
43
43
 
44
+ ## Flaky-lane triage — re-running erases the evidence you need
45
+
46
+ **Before you re-run a red CI job, pull the failing lane name out of its log.** A re-run overwrites the
47
+ run's `conclusion`, and GitHub's summary then shows the whole history as green — including the run
48
+ that failed. The failure is not hidden behind a filter; it no longer exists as a queryable state.
49
+
50
+ Measured 2026-08-03: a `sync_from_be_lanes.sh` flake was recorded and, when someone went looking the
51
+ next day, **all 14 runs that day reported `success`**. The re-runs had rewritten the conclusions and
52
+ the failing job's lane name had never been captured anywhere. There was nothing left to diagnose, so
53
+ nothing was diagnosed — a guessed diagnosis was correctly refused rather than written down.
54
+
55
+ Order, on the next occurrence:
56
+
57
+ ```bash
58
+ gh run list --workflow <wf> --limit 30 --json databaseId,conclusion,headSha # find the red run FIRST
59
+ gh run view <id> --log-failed | grep -E '❌|FAIL|not ok' | head -20 # capture the lane NAME
60
+ # only now consider re-running
61
+ ```
62
+
63
+ Record the lane name in the session log even if the re-run goes green — **a flake that cannot be named
64
+ cannot be fixed**, and the second occurrence is the one that gets fixed only if the first was named.
65
+
66
+ This is prose on purpose. N=1: one flake, one lost diagnosis. The mechanization threshold in this repo
67
+ is N≥3 or the same class recurring on another surface; a scheduled log-capture job built on a single
68
+ occurrence would be the speculative build the evidence-threshold rule forbids.
69
+
44
70
  ## 3-Phase Maturity Roadmap
45
71
 
46
72
  The hub's long-term evolution path is managed as a 3-stage model: **Phase I (entering maturity) → Phase II (frontier following) → Phase III (frontier leading)**. Phase transition gates have the simplification principle ("A good harness gets simpler over time") as a common condition.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrono-meta/fh-gate",
3
- "version": "1.4.86",
3
+ "version": "1.4.87",
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": [
@@ -51,6 +51,7 @@
51
51
  "CHEATSHEET.md",
52
52
  "CLAUDE.md",
53
53
  ".claude/registry/agent_cards.json",
54
+ ".claude/registry/README.md",
54
55
  "docs/CONTRIBUTING.md",
55
56
  "bin/fh-codex-doctor.js",
56
57
  "bin/fh-gate.js",
@@ -148,6 +149,9 @@
148
149
  "scripts/test_halffix_lanes.sh",
149
150
  "scripts/test_ollama_panel_lanes.sh",
150
151
  "scripts/test_tag_version_lanes.sh",
151
- "templates/subagent-tally-hook.json"
152
+ "templates/subagent-tally-hook.json",
153
+ "scripts/test_session_close_chain_lanes.sh",
154
+ "scripts/test_node_infra_delta_lanes.sh",
155
+ "scripts/test_wizard_snippet_merge_lanes.sh"
152
156
  ]
153
157
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-commons",
3
- "version": "1.4.86",
3
+ "version": "1.4.87",
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.86",
3
+ "version": "1.4.87",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
@@ -58,7 +58,7 @@ ls .claude/regression/probes.md 2>/dev/null || echo "NO_CUSTOM_PROBES"
58
58
  ```
59
59
 
60
60
  **If custom probes exist**: load and use them. The hub repo ships its golden probe set
61
- (known-answer offline eval, 28 probes with check classes) at exactly this path — when
61
+ (known-answer offline eval, 32 probes with check classes) at exactly this path — when
62
62
  present it is canonical and supersedes the default matrix below.
63
63
 
64
64
  **If no custom probes** (e.g. Mode C install without the hub repo): use the default
@@ -155,6 +155,91 @@ else
155
155
  fail=$((fail + 1))
156
156
  fi
157
157
 
158
+ # ── 5. CLAUDE.md asset-list parity — the FIXED per-class needle table ─────────
159
+ # WHY A TABLE, AND WHY FIXED. A parity check was attempted for this on 2026-08-03 and CUT the same
160
+ # day: it guessed its needles per run, produced four findings in one adversarial round, and — the
161
+ # decisive part — never fired on `CLAUDE.md` at all, because `CLAUDE\.md` was not in the hook's
162
+ # GATE_IMPL trigger. Both defects are addressed here and neither is optional: the needles below are
163
+ # FIXED (a literal table, reviewed as data), and the hook gained a SEPARATE trigger
164
+ # (`ASSETLIST_IMPL`) so this anchor actually runs when the list it guards is edited. The trigger is
165
+ # deliberately NOT `GATE_IMPL`: that variable also feeds `$LOADBEARING`, and enrolling every
166
+ # CLAUDE.md prose edit into cross-family review is a different job. Retrying without both the
167
+ # fixed table AND a trigger repeats the cut.
168
+ #
169
+ # WHAT IT GUARDS. `CLAUDE.md` §FH Improvement 4-Axis Auto-Gate carries a canonical asset list, and
170
+ # `.claude/rules/fh_4axis_gate.md` carries another. When a class is added to the hook but not to a
171
+ # prose list (or dropped from one list only), the divergence is silent — the gate still blocks, so
172
+ # nothing goes red, while the resident text tells readers a class is not covered. That is the exact
173
+ # shape measured on 2026-08-03, when two classes were added to both lists with no anchor behind them.
174
+ #
175
+ # SCOPE, deliberately narrow: this asserts the asset-list SENTENCE names each class. It does not
176
+ # assert the hook and the sentence are equal sets — that stronger claim needs a parse of the regex
177
+ # into classes, which is the guessing this table replaces. Narrow and mechanical beats broad and
178
+ # self-generating; the enumeration sweep in §4 is where breadth lives.
179
+ CLAUDE_MD="$REPO_ROOT/CLAUDE.md"
180
+ # Read the STAGED blob as well as the worktree, and require BOTH to declare the class.
181
+ # Cross-family review (2026-08-04, codex/gpt-5.5) reproduced the fail-open: reading only the worktree
182
+ # lets a commit stage a broken asset line, repair the worktree WITHOUT staging, and pass — the hook
183
+ # sees CLAUDE.md in $STAGED, runs this anchor, and validates content that is not what is being
184
+ # committed. Measured before the fix: index 0 occurrences / worktree 1 / anchor exit 0.
185
+ # Checking only the index would invert the same hole (a broken UNSTAGED edit would pass a manual
186
+ # run), so both are required. `git show :CLAUDE.md` fails outside a repo or with nothing in the
187
+ # index; that is a skip of the staged leg, never a pass of it.
188
+ _asset_line_of() { # $1 = file-or-'-' content source label; reads stdin
189
+ grep -m1 '^\*\*FH 자산을 수정하면\*\*' || true
190
+ }
191
+ ASSET_LINE=$(_asset_line_of < "$CLAUDE_MD" 2>/dev/null || true)
192
+ # Distinguish "not in the index" (a real skip) from "git failed" (an instrument error). The advisory
193
+ # degrade lint flagged the first draft here, and correctly: a bare `|| true` let the staged leg
194
+ # silently abstain on ANY git failure, which half-reopens the fail-open this section was just fixed
195
+ # for. Tracked files always have an index entry, so for this repo the abstain branch is unreachable
196
+ # in normal operation — it exists for a clone where CLAUDE.md is untracked.
197
+ if git -C "$REPO_ROOT" ls-files --error-unmatch CLAUDE.md >/dev/null 2>&1; then
198
+ ASSET_LINE_STAGED=$(git -C "$REPO_ROOT" show :CLAUDE.md 2>/dev/null | _asset_line_of)
199
+ if [ -z "$ASSET_LINE_STAGED" ]; then
200
+ echo " ❌ CLAUDE.md is tracked but its staged blob yielded no declaration line —"
201
+ echo " instrument error (or the staged content dropped the line entirely), NOT a pass."
202
+ fail=$((fail + 1))
203
+ fi
204
+ else
205
+ ASSET_LINE_STAGED="" # untracked clone — worktree leg is the only measurable one
206
+ fi
207
+ if [ -z "$ASSET_LINE" ]; then
208
+ echo " ❌ CLAUDE.md asset-list declaration line not found — instrument error, NOT a pass"
209
+ echo " (looked for a line starting '**FH 자산을 수정하면**' in $CLAUDE_MD)"
210
+ fail=$((fail + 1))
211
+ else
212
+ # label|needle (ERE, matched against the declaration line only)
213
+ for entry in \
214
+ 'SKILL.md|SKILL\.md' \
215
+ 'SKILL_detail.md|SKILL_detail\.md' \
216
+ '.claude/rules|\.claude/rules/' \
217
+ 'knowledge/shared/rules|knowledge/shared/rules/' \
218
+ 'templates/|templates/' \
219
+ 'CLAUDE.md|CLAUDE\.md' \
220
+ 'AGENTS.md|AGENTS\.md' \
221
+ 'scripts/**/*.sh|scripts/\*\*/\*\.sh' \
222
+ 'agent definitions (plugins/*/agents)|plugins/\*/agents/' \
223
+ 'agent definitions (.claude/agents)|\.claude/agents/'
224
+ do
225
+ lbl="${entry%%|*}"; needle="${entry#*|}"
226
+ _hit=1
227
+ printf '%s' "$ASSET_LINE" | grep -qE "$needle" || _hit=0
228
+ # The staged leg only votes when it exists — an empty index blob is "not measured", not "absent".
229
+ if [ -n "$ASSET_LINE_STAGED" ]; then
230
+ printf '%s' "$ASSET_LINE_STAGED" | grep -qE "$needle" || _hit=0
231
+ fi
232
+ if [ "$_hit" -eq 1 ]; then
233
+ echo " ✅ CLAUDE.md asset list declares: $lbl"; pass=$((pass + 1))
234
+ else
235
+ echo " ❌ CLAUDE.md asset list no longer declares: $lbl"
236
+ echo " The hook still gates it, so nothing goes red — the resident text now tells readers"
237
+ echo " a gated class is not covered. Re-add it, or retire the class from the hook too."
238
+ fail=$((fail + 1))
239
+ fi
240
+ done
241
+ fi
242
+
158
243
  echo
159
244
  if [ "$fail" -eq 0 ]; then
160
245
  echo "gate_pathspec_check: PASS ($pass pairs)"
@@ -65,6 +65,12 @@ ACCEPTED_ABSENT=(
65
65
  # user's own cross-context wiring — the template it is copied FROM is what ships.
66
66
  ".claude/rules/local_fh_context.md"
67
67
  ".claude/regression/probes.md"
68
+ # Its sibling, and absent for the same reason one layer up: this file records which sections of
69
+ # THIS repo's resident CLAUDE.md were measured load-bearing. A consumer's CLAUDE.md is their
70
+ # own text, so our verdicts are not merely useless to them — cited from a shipped doc they would
71
+ # read as claims about THEIR file. The shipped docs name it as the place verdicts live in the
72
+ # harness repo, which is a pointer for contributors, not a promise of a shipped artifact.
73
+ ".claude/regression/ablation_verdicts.md"
68
74
  "scripts/sync-to-be.sh"
69
75
  "scripts/sync_guard_check.sh"
70
76
  # Return path (companion store → hub) and its anchor. Same reason as the forward path above: the
@@ -77,6 +83,27 @@ ACCEPTED_ABSENT=(
77
83
  # regression run must not compare against this harness's probe set). Shipping the reader without
78
84
  # its corpus would put a script in the package that can only ever report "instrument error".
79
85
  "scripts/probe_scope_check.sh"
86
+ # The known-pair precondition for `probe_scope_check.sh`'s ablation procedure. Absent for the same
87
+ # reason as its subject — the procedure ablates THIS repo's resident CLAUDE.md, so a consumer has
88
+ # nothing to point it at — plus one of its own: every run spends API calls against the consumer's
89
+ # account. Shipping a script whose only effect on a consumer's machine is a bill is worse than
90
+ # omitting it.
91
+ "scripts/ablation_calibrate.sh"
92
+ # Its anchor. Absent for exactly one reason — its SUBJECT is absent — and that pairing is the whole
93
+ # rule: shipping an anchor whose subject is not in the package is the defect that put a red
94
+ # selfcheck in front of every 1.4.85/1.4.86 consumer. selfcheck's block SKIPs when the subject is
95
+ # missing, so the package stays green without pretending the lanes ran.
96
+ "scripts/test_ablation_calibrate_lanes.sh"
97
+ # Anchor for probe_scope_check.sh, which is ACCEPTED_ABSENT above for want of its corpus. Same
98
+ # pairing rule: an anchor whose subject does not ship must not ship either, or the consumer's
99
+ # selfcheck goes red on a subject they do not have.
100
+ "scripts/test_probe_scope_lanes.sh"
101
+ # Measures what the LIVE `claude` CLI does with several SessionStart hooks on one matcher —
102
+ # so every run needs the CLI, auth, and spends tokens on the consumer's account. Same reason
103
+ # `ablation_calibrate.sh` is absent: shipping a script whose only effect on a consumer's
104
+ # machine is a bill is worse than omitting it. selfcheck reports it NOT EXERCISED (exit 2)
105
+ # where the CLI is missing, so the package stays green without pretending the lanes ran.
106
+ "scripts/test_sessionstart_multihook_lanes.sh"
80
107
  )
81
108
 
82
109
  out=$(python3 - "${ACCEPTED_ABSENT[@]}" <<'PY'
@@ -202,6 +202,39 @@ else
202
202
  fail=1
203
203
  fi
204
204
 
205
+ # And the ablation calibrator, for the third instance of the same reason. Its whole job is telling
206
+ # apart states that look identical from outside — "the arm could not answer" vs "the runner is dead"
207
+ # vs "the runner read the answer off disk" — and round 2 of its own adversarial review found that
208
+ # three of its round-1 fixes had no discriminating lane at all. Unwired lanes are green on one
209
+ # machine and nowhere else, which is the case this file exists to prevent. Stub runners, no API
210
+ # spend, so running them here costs nothing. Not hermetic w.r.t. the filesystem: two isolation lanes
211
+ # create and remove an empty, per-PID, git-invisible directory in the worktree.
212
+ # probe-scope lanes — the subject had 15 sibling checkers with an anchor and none of its own, so
213
+ # three repairs shipped on 2026-08-03 that could each be reverted with nothing turning red. Same
214
+ # SKIP/FAIL shape as the blocks above: a missing SUBJECT is a legitimate skip, a present subject with
215
+ # a missing anchor is a real failure.
216
+ if [ ! -f scripts/probe_scope_check.sh ]; then
217
+ echo "SKIP test_probe_scope_lanes.sh (subject scripts/probe_scope_check.sh absent)"
218
+ elif [ -f scripts/test_probe_scope_lanes.sh ]; then
219
+ if ! bash scripts/test_probe_scope_lanes.sh; then
220
+ fail=1
221
+ fi
222
+ else
223
+ echo "FAIL test_probe_scope_lanes.sh: probe_scope_check.sh present but its anchor is missing"
224
+ fail=1
225
+ fi
226
+
227
+ if [ ! -f scripts/ablation_calibrate.sh ]; then
228
+ echo "SKIP test_ablation_calibrate_lanes.sh (subject scripts/ablation_calibrate.sh absent)"
229
+ elif [ -f scripts/test_ablation_calibrate_lanes.sh ]; then
230
+ if ! bash scripts/test_ablation_calibrate_lanes.sh; then
231
+ fail=1
232
+ fi
233
+ else
234
+ echo "FAIL test_ablation_calibrate_lanes.sh: ablation_calibrate.sh present but its anchor is missing"
235
+ fail=1
236
+ fi
237
+
205
238
  if [ ! -f scripts/fh_node_check.sh ]; then
206
239
  echo "SKIP test_node_check_lanes.sh (subject scripts/fh_node_check.sh absent)"
207
240
  elif [ -f scripts/test_node_check_lanes.sh ]; then
@@ -213,6 +246,52 @@ else
213
246
  fail=1
214
247
  fi
215
248
 
249
+ # The infra-delta half of the same subject. Separate suite, same pairing rule: it exists only because
250
+ # fh_node_check.sh does, so its absence beside a present subject is a FAIL, not a skip.
251
+ if [ ! -f scripts/fh_node_check.sh ]; then
252
+ echo "SKIP test_node_infra_delta_lanes.sh (subject scripts/fh_node_check.sh absent)"
253
+ elif [ -f scripts/test_node_infra_delta_lanes.sh ]; then
254
+ if ! bash scripts/test_node_infra_delta_lanes.sh; then
255
+ fail=1
256
+ fi
257
+ else
258
+ echo "FAIL test_node_infra_delta_lanes.sh: fh_node_check.sh present but its anchor is missing"
259
+ fail=1
260
+ fi
261
+
262
+ # SessionStart multi-hook + install-wizard snippet merge. Subject for both = the shipped settings
263
+ # snippets; a clone without them is a legitimate SKIP, a clone with them and no anchor is not.
264
+ for _pair in \
265
+ "templates/settings.SessionStart.snippet.json|scripts/test_sessionstart_multihook_lanes.sh" \
266
+ "templates/settings.SessionStart.snippet.json|scripts/test_wizard_snippet_merge_lanes.sh"
267
+ do
268
+ _subj="${_pair%%|*}"; _anc="${_pair#*|}"
269
+ if [ ! -f "$_subj" ]; then
270
+ echo "SKIP ${_anc##*/} (subject $_subj absent)"
271
+ elif [ -f "$_anc" ]; then
272
+ # THREE-valued, like the session-close anchors above — and for a third reason they do not have.
273
+ # test_sessionstart_multihook_lanes.sh measures what the LIVE `claude` CLI does with several
274
+ # SessionStart hooks on one matcher. It declares exit 2 = NOT EXERCISED (no CLI / no auth /
275
+ # opt-out). A CI runner structurally cannot have that CLI, so collapsing 2 into fail=1 makes
276
+ # every Linux run red forever — over-blocking, which is how a red CI stops being read at all
277
+ # (the same reasoning that keeps the session-close check advisory on ordinary pushes).
278
+ # 2 does NOT set fail, and it prints a line that cannot be misread as a pass. On a machine that
279
+ # DOES have the CLI the suite runs in full and a real failure still exits 1.
280
+ # Measured 2026-08-04: the first draft of this wiring flattened 2 into fail=1 and turned CI red
281
+ # while the suite had correctly reported "NOT EXERCISED — the `claude` CLI is not on PATH" —
282
+ # i.e. it rebuilt, ten lines below the comment warning against it, the exact flattening defect.
283
+ bash "$_anc"; _rc=$?
284
+ if [ "$_rc" -eq 2 ]; then
285
+ echo "NOT EXERCISED ${_anc##*/}: this environment cannot run the measurement (exit 2 — never a pass)"
286
+ elif [ "$_rc" -ne 0 ]; then
287
+ fail=1
288
+ fi
289
+ else
290
+ echo "FAIL ${_anc##*/}: $_subj present but its anchor is missing"
291
+ fail=1
292
+ fi
293
+ done
294
+
216
295
  # Two guards that read the AUTHOR's own actions rather than the repo's files. Both were added
217
296
  # 2026-07-31; the pipe-verdict lane shipped in PR #209 WITHOUT this wiring, which is itself the
218
297
  # half-fix class the second guard exists to catch — found by running that guard on this repo.
@@ -249,7 +328,7 @@ else
249
328
  fail=1
250
329
  fi
251
330
 
252
- for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh; do
331
+ for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh scripts/test_session_close_chain_lanes.sh; do
253
332
  if [ ! -f scripts/session_close_check.sh ]; then
254
333
  echo "SKIP ${_anchor##*/} (subject scripts/session_close_check.sh absent)"
255
334
  elif [ -f "$_anchor" ]; then
@@ -39,6 +39,7 @@ if command -v gh >/dev/null 2>&1; then
39
39
  # `[{"number":227},{"number":226},{"number":225}]` → old 1, new 3; `[]` → 0 both ways — which is
40
40
  # why an environment with zero open PRs can never surface this, and why the 0-case alone is not a
41
41
  # calibration. Observed live the same day: 2 open PRs reported as 1.
42
+ # Anchored by scripts/test_session_close_chain_lanes.sh lane ①-b-P3.
42
43
  PRS=$(gh pr list --author "@me" --state open --json number 2>/dev/null | grep -o '"number"' | wc -l | tr -d ' ' || true)
43
44
  [ "${PRS:-0}" -gt 0 ] && echo "⚠️ ①-b $PRS open PR(s) by you — classify: self-mergeable vs awaiting-external"
44
45
  fi
@@ -72,11 +73,16 @@ if [ "${FH_CHANGED:-0}" -gt 0 ]; then
72
73
  fi
73
74
  fi
74
75
 
75
- # real-time completion log — required whenever any commit landed today
76
+ # ④-log real-time completion log — required whenever any commit landed today.
77
+ # NAMING: this block does NOT implement CLAUDE.md's ④ (memory hygiene). It implements the
78
+ # "Real-time completion tracking" paragraph that sits above the chain. Labelling it ④ meant a green
79
+ # "④" told a reader memory hygiene had been verified when nothing had checked it — false coverage,
80
+ # the same class as a summary line that prints PASS while the exit code refuses. The label now says
81
+ # what it checks. (Numbering mismatch found 2026-08-02 by the lane-writing pass.)
76
82
  COMMITS_TODAY=$(git -C "$FH" log --since="today 00:00" --oneline 2>/dev/null | wc -l | tr -d ' ')
77
83
  FC="$FH/tracks/_meta/fh_completed_${TODAY}.md"
78
84
  if [ "$COMMITS_TODAY" -gt 0 ] && [ ! -f "$FC" ]; then
79
- echo "❌ commits landed today but tracks/_meta/fh_completed_${TODAY}.md is missing"
85
+ echo "❌ ④-log commits landed today but tracks/_meta/fh_completed_${TODAY}.md is missing"
80
86
  FAIL=1
81
87
  fi
82
88
 
@@ -150,12 +156,20 @@ if [ "$HOOK_OK" -eq 0 ]; then
150
156
  echo " (that file is gitignored, so a fresh clone has none). An unmeasured dispatch count is"
151
157
  echo " NOT a count of zero. Install: templates/subagent-tally-hook.json → .claude/settings.json"
152
158
  fi
153
- DISPATCHED=$(grep -c "^$TODAY$" "$TALLY" 2>/dev/null | tr -d ' ' || echo 0)
159
+ # `grep -c` PRINTS 0 and EXITS 1 when the count is zero. Under `set -o pipefail` (line 16) that
160
+ # makes the pipeline fail, `|| echo 0` appends a SECOND line, and the value becomes "0\n0" — which
161
+ # `[ -eq ]` rejects as a bash error, so the branch it guards is skipped. The guard was therefore
162
+ # disarmed in EXACTLY the case it exists for (zero log entries). Measured 2026-08-04 during a close:
163
+ # the check printed ✅ while `[: 0\n0: integer expression expected` went to stderr.
164
+ # Fix = no `|| echo`, plus integer sanitation. This is the documented prescription for this class
165
+ # ([[feedback_pipefail_fallback_disarms_guard]]) applied to the checker that cited it.
166
+ _int() { case "${1:-}" in (''|*[!0-9]*) echo 0 ;; (*) echo "$1" ;; esac; }
167
+ DISPATCHED=$(_int "$(grep -c "^$TODAY$" "$TALLY" 2>/dev/null | tr -d ' ' || true)")
154
168
  # Both quotings, because the file carries both: hand-written entries use `- date: 2026-08-02`
155
169
  # while anything appended via yaml.dump renders `- date: '"'"'2026-08-02'"'"'`. Matching one form counted
156
170
  # half the entries as absent — a divergent-normalizer miss inside the check that exists to catch
157
171
  # missing records. Known-pair calibrated below in test_dispatch_log_lanes.sh.
158
- LOGGED=$(grep -cE "^- date: *'?$TODAY'?" "$LOG" 2>/dev/null | tr -d ' ' || echo 0)
172
+ LOGGED=$(_int "$(grep -cE "^- date: *'?$TODAY'?" "$LOG" 2>/dev/null | tr -d ' ' || true)")
159
173
  if [ "${DISPATCHED:-0}" -gt 0 ] && [ "${LOGGED:-0}" -eq 0 ]; then
160
174
  echo "❌ ④-e $DISPATCHED sub-agent dispatch(es) today and ZERO invocation-log entries — the 60/40"
161
175
  echo " promotion gate and the UAP loop both read that file; an unlogged session is invisible to"
@@ -93,6 +93,39 @@ hookless_verdict() { # $1=hook_configured -> MEASURED | NOT-MEASURED
93
93
  grep -q "NOT MEASURED" "$CHECK" ; chk $? "the subject actually carries that branch (not just this lane)"
94
94
  [ -f "$SCRIPT_DIR/../templates/subagent-tally-hook.json" ] ; chk $? "installable snippet exists for a clone that has no settings.json"
95
95
 
96
+ echo "── pipefail disarm (the guard was dead in EXACTLY its target case) ──"
97
+ # `grep -c` PRINTS 0 and EXITS 1 on a zero count. Under `set -o pipefail` the old
98
+ # `grep -c … | tr -d ' ' || echo 0` therefore produced "0\n0", `[ -eq ]` threw, and the ❌ branch
99
+ # was skipped — so ④-e reported ✅ whenever the log had ZERO entries, which is the one situation it
100
+ # exists to block. Measured live 2026-08-04 during a session close.
101
+ # Lane 1: reproduce the OLD form and assert it is broken (a control — if this ever passes, the
102
+ # premise changed and the fix below is measuring nothing).
103
+ old_form=$(bash -c 'set -uo pipefail; grep -c "^NOSUCHDATE$" '"$CHECK"' 2>/dev/null | tr -d " " || echo 0')
104
+ [ "$(printf %s "$old_form" | grep -c "")" -eq 2 ] ; chk $? "control: the old \`|| echo 0\` form really does yield TWO lines under pipefail"
105
+ # Lane 2: the shipped form yields a single sanitised integer.
106
+ new_form=$(bash -c 'set -uo pipefail; _int() { case "${1:-}" in (""|*[!0-9]*) echo 0 ;; (*) echo "$1" ;; esac; }; _int "$(grep -c "^NOSUCHDATE$" '"$CHECK"' 2>/dev/null | tr -d " " || true)"')
107
+ [ "$new_form" = "0" ] ; chk $? "shipped form yields a single sanitised 0"
108
+ ( set -uo pipefail; [ "$new_form" -eq 0 ] ) 2>/dev/null ; chk $? "and \`[ -eq ]\` accepts it (the old form threw here)"
109
+ # Lane 3-5: BEHAVIOUR, not spelling. The first draft of these lanes string-matched the old form and
110
+ # went GREEN on a revert — the needle did not match `tr -d ' '` (quotes), so the anchor was
111
+ # decorative in the way this repo already named ([[feedback_anchor_can_be_decorative]], cause:
112
+ # "needle matching unrelated text"). Extract the subject's OWN assignment lines and evaluate them.
113
+ _eval_subject_assign() { # $1 = variable name; echoes what the SUBJECT computes for a zero match
114
+ local var="$1" line
115
+ line=$(grep -E "^${var}=" "$CHECK" | head -1)
116
+ [ -n "$line" ] || { echo "NOLINE"; return; }
117
+ bash -c "set -uo pipefail
118
+ _int() { case \"\${1:-}\" in (''|*[!0-9]*) echo 0 ;; (*) echo \"\$1\" ;; esac; }
119
+ TODAY=NOSUCHDATE; TALLY='$CHECK'; LOG='$CHECK'
120
+ $line
121
+ printf %s \"\$$var\"" 2>/dev/null
122
+ }
123
+ for _v in DISPATCHED LOGGED; do
124
+ _got=$(_eval_subject_assign "$_v")
125
+ [ "$(printf %s "$_got" | grep -c '')" -le 1 ] ; chk $? "subject's \$$_v is ONE line on a zero match (was two: the disarm)"
126
+ ( set -uo pipefail; [ "${_got:-x}" -eq 0 ] ) 2>/dev/null ; chk $? "subject's \$$_v survives \`[ -eq 0 ]\` (the disarm threw here)"
127
+ done
128
+
96
129
  echo ""
97
130
  if [ "$FAILED" -ne 0 ]; then echo "DISPATCH-LOG LANES: FAIL"; exit 1; fi
98
131
  echo "DISPATCH-LOG LANES: PASS ($PASS/$PASS)"