@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.
@@ -25,6 +25,74 @@ check() { # check <label> <cmd...>
25
25
  fail=1
26
26
  fi
27
27
  }
28
+ # ⚠️ KNOWN DEFECT, NOT FIXED HERE — `check()` above has the same evidence-discarding shape the
29
+ # lane blocks below were repaired for (2026-08-05): it decides on `"$@" 2>/dev/null` (stderr of the
30
+ # DECIDING run is destroyed) and then re-runs to print. It is left alone deliberately: `check()` is
31
+ # called by every `node --check` / `bash -n` line in this file, so changing it changes the whole
32
+ # surface at once, which is a different job from repairing the four lane blocks (CLAUDE.md
33
+ # §Added-Scope Gate question 2). `scripts/probe_scope_check.sh`'s caller near the probe-scope block
34
+ # carries the same shape. Both are tracked separately — do NOT read the lane-block repair below as
35
+ # having cleared this file.
36
+
37
+ # _show_failure <captured-output> — print a FAILING suite's evidence without truncating it away.
38
+ # Single source for all four lane blocks (a second copy would drift; the divergent-normalizer class).
39
+ # WHY NOT `tail -N`: measured 2026-08-05 on sync_from_be_lanes.sh — output is 98 lines and a planted
40
+ # lane failure at line ~22 is INVISIBLE to `tail -20` (0 hits), while the summary banner still reads
41
+ # "1 failed". The reader gets a FAIL verdict sitting on top of passing log lines — the exact shape
42
+ # this whole repair exists to remove. The failing-line extraction finds it (1 hit, known pair).
43
+ # All four suites mark failures with `❌` (`no()` in sync_from_be_lanes.sh:21, `chk()` in the other
44
+ # three) or an early `FAIL ` line when a subject is missing; grep handles the multi-byte glyph
45
+ # (known pair: 1 hit on a ❌ line, 0 on a ✅-only line — verified, not assumed).
46
+ _show_failure() {
47
+ local out="$1" fails n banner shown nonblank
48
+ # Whitespace-only counts as empty: guarding with [ -z "$out" ] alone let a suite emitting " "
49
+ # fall into the died-early branch and print indented blank lines — silence rendered as evidence.
50
+ # SHELL PATTERN, NOT `tr`: the obvious `tr -d '[:space:]'` is a measured defect on BSD. Given a
51
+ # line containing invalid UTF-8, macOS `tr` aborts with `tr: Illegal byte sequence` and emits
52
+ # NOTHING, so the guard concludes "empty" and reports "no output captured" while real evidence is
53
+ # sitting in $out — the exact mis-report this helper exists to prevent, reintroduced by the guard
54
+ # against it. (GNU tr passes the bytes through; the arms disagree, and the failing arm is the
55
+ # author's own machine.) Case-matching is a shell builtin: no subprocess, no charset decoding, so
56
+ # invalid bytes cannot make it lie. Known pair: invalid-byte→nonblank, spaces/tabs/newlines→blank,
57
+ # ""→blank, "hello"→nonblank (4/4), while the tr form returns 0 bytes on arm 1.
58
+ case "$out" in *[![:space:]]*) nonblank=1 ;; *) nonblank= ;; esac
59
+ # NO LOCALE PIN HERE, and its absence is a measured result — same disposition, and same reasoning,
60
+ # as the pin `.github/workflows/validate.yml` removed after refuting its own locale hypothesis.
61
+ # Two model families independently suspected that matching the multi-byte `❌` would break under a
62
+ # C/POSIX locale (one filed it as UNCALIBRATED for the GNU arm, the other as an unpinned-locale
63
+ # defect), so `LC_ALL=C` was added — then both arms were actually measured:
64
+ # BSD grep (macOS) : C, UTF-8 → 1 hit each
65
+ # GNU grep 3.12 (Linux) : C, C.UTF-8, unset → 1 hit each; and with invalid UTF-8 bytes mixed
66
+ # in, the ❌ line still extracts (no binary-file
67
+ # collapse, the specific feared mode)
68
+ # The hypothesis is REFUTED on both arms, so the pin demonstrated nothing and was removed rather
69
+ # than kept as insurance — a knob retained because it might help is indistinguishable from one
70
+ # that does, and the next reader would inherit it as evidence that the danger is real.
71
+ fails=$(printf '%s\n' "$out" | grep -E '❌|^FAIL ' || true)
72
+ banner=$(printf '%s\n' "$out" | grep -E '════' | tail -1 || true)
73
+ if [ -n "$fails" ]; then
74
+ shown=$(printf '%s\n' "$fails" | head -25)
75
+ printf '%s\n' "$shown" | sed 's/^/ /'
76
+ n=$(printf '%s\n' "$fails" | wc -l | tr -d ' ')
77
+ [ "$n" -gt 25 ] && echo " … ($((n - 25)) more failing lines not shown)"
78
+ elif [ -z "$nonblank" ]; then
79
+ echo " (no output captured — the suite produced nothing before failing)"
80
+ else
81
+ echo " (no ❌/FAIL line found — suite likely died early; showing tail)"
82
+ printf '%s\n' "$out" | tail -12 | sed 's/^/ /'
83
+ fi
84
+ # Summary banner: matched by shape, not by position. A blind `tail -2` re-printed lines already
85
+ # shown above (measured: a ❌ within the last 2 lines appeared twice, and the "N more not shown"
86
+ # notice was immediately followed by one of the lines it had just declined to show), and on empty
87
+ # input it emitted a stray indented line. Print it only when it exists and is not already on screen.
88
+ # Dedupe against what was ACTUALLY PRINTED ($shown), not against the full $fails set. Searching
89
+ # $fails suppressed the banner whenever a failing line beyond the head -25 cut merely contained
90
+ # the banner text — i.e. it hid the banner precisely because a line the reader never saw mentioned
91
+ # it. $shown is empty in the non-fails branches, so the banner prints there as before.
92
+ if [ -n "$banner" ] && ! printf '%s\n' "${shown:-}" | grep -qF -- "$banner"; then
93
+ printf ' %s\n' "$banner"
94
+ fi
95
+ }
28
96
 
29
97
  # Node executables (npm-shipped)
30
98
  for f in bin/*.js; do
@@ -34,7 +102,18 @@ done
34
102
  # Codex adapter drift: the thin Codex runtime must keep reading canonical FH
35
103
  # skill/agent surfaces without silently accepting Claude-native primitives as
36
104
  # Codex-native.
37
- check "fh-codex-doctor --strict" bash -c 'node bin/fh-codex-doctor.js --strict >/dev/null'
105
+ # NOT via check(): that helper decides on a run whose stderr is discarded, and this call site used to
106
+ # additionally discard the subject's STDOUT (`--strict >/dev/null`). fh-codex-doctor writes 100% of
107
+ # its drift diagnostics to stdout (measured 2026-08-05: 686 B stdout / 0 B stderr), so a failure
108
+ # printed a bare `FAIL` line carrying no diagnosis at all — worse than the truncation this session
109
+ # repaired in the lane blocks. Fixed at the call site; check() itself is a separate job (see above).
110
+ if _out=$(node bin/fh-codex-doctor.js --strict 2>&1); then
111
+ echo "PASS fh-codex-doctor --strict"
112
+ else
113
+ echo "FAIL fh-codex-doctor --strict"
114
+ _show_failure "$_out"
115
+ fail=1
116
+ fi
38
117
 
39
118
  # Bash surface: npm-shipped scripts + local bin wrappers + gate-chain infra
40
119
  for f in scripts/*.sh bin/fh-gate bin/fh-run bin/fh-goal \
@@ -224,6 +303,38 @@ else
224
303
  fail=1
225
304
  fi
226
305
 
306
+ # gate-pathspec anchor — wired here 2026-08-04. It was reachable ONLY from templates/.git-hooks/
307
+ # pre-commit, i.e. only in a clone where the operator had run `git config core.hooksPath`. Every
308
+ # other clone, every CI run, and the npm package carried the anchor file and never executed it —
309
+ # the built-but-not-wired shape, one layer up: the anchor for the gate had no anchor of its own.
310
+ # That mattered the same day: PR #254 added five known-pairs to it, all of which would have been
311
+ # unexecuted outside the author's machine.
312
+ # Subject = the two implementations it reads (the hook's HEAVY term and the guard's GUARD_PATHSPEC).
313
+ # Absent subject → package/partial surface → legitimate SKIP; present subject with the anchor gone
314
+ # → FAIL, same shape as every block above.
315
+ # NAMED RESIDUAL (cross-family, gpt-5.5, 2026-08-04): if a distribution that SHOULD be complete
316
+ # accidentally drops one subject, this reports SKIP, not FAIL — silent non-coverage. Measured the
317
+ # same day: removing `templates/.git-hooks` from package.json `files[]` and running
318
+ # scripts/package_coverage_check.sh still PASSED, so no existing anchor catches that omission
319
+ # either. Deliberately NOT patched with a stricter branch: the only discriminator available
320
+ # ("templates/ exists but the hook does not") would be built on an UNMEASURED assumption about how
321
+ # a narrower package is shaped, and this repo's rule is not to build before the constraint is
322
+ # measured. What is cheap and honest is naming WHICH subject is missing, so a SKIP is diagnosable
323
+ # instead of opaque. Revisit when a real partial distribution is observed.
324
+ _gps_missing=""
325
+ [ -f templates/.git-hooks/pre-commit ] || _gps_missing="templates/.git-hooks/pre-commit"
326
+ [ -f templates/regression_guard.sh ] || _gps_missing="${_gps_missing:+$_gps_missing, }templates/regression_guard.sh"
327
+ if [ -n "$_gps_missing" ]; then
328
+ echo "SKIP gate_pathspec_check.sh (subject absent: $_gps_missing) — not-checked, NOT a pass"
329
+ elif [ -f scripts/gate_pathspec_check.sh ]; then
330
+ if ! bash scripts/gate_pathspec_check.sh; then
331
+ fail=1
332
+ fi
333
+ else
334
+ echo "FAIL gate_pathspec_check.sh: the gate implementations are present but their coverage anchor is missing"
335
+ fail=1
336
+ fi
337
+
227
338
  if [ ! -f scripts/ablation_calibrate.sh ]; then
228
339
  echo "SKIP test_ablation_calibrate_lanes.sh (subject scripts/ablation_calibrate.sh absent)"
229
340
  elif [ -f scripts/test_ablation_calibrate_lanes.sh ]; then
@@ -415,28 +526,48 @@ fi
415
526
  if [ ! -f templates/.git-hooks/pre-push ]; then
416
527
  echo "SKIP test_tag_version_lanes.sh (subject templates/.git-hooks/pre-push absent)"
417
528
  elif [ -f scripts/test_tag_version_lanes.sh ]; then
418
- if ! bash scripts/test_tag_version_lanes.sh >/dev/null 2>&1; then
529
+ # RUN-ONCE, CAPTURE (2026-08-05) rationale in the sync_from_be_lanes block later in this file.
530
+ if _out=$(bash scripts/test_tag_version_lanes.sh 2>&1); then
531
+ echo "PASS test_tag_version_lanes.sh (mismatch blocks · match silent · scope · override)"
532
+ else
419
533
  echo "FAIL test_tag_version_lanes.sh: the tag/version guard would mis-route"
420
- bash scripts/test_tag_version_lanes.sh 2>&1 | tail -12
534
+ _show_failure "$_out"
421
535
  fail=1
422
- else
423
- echo "PASS test_tag_version_lanes.sh (mismatch blocks · match silent · scope · override)"
424
536
  fi
425
537
  else
426
538
  echo "FAIL test_tag_version_lanes.sh: pre-push present but its anchor is missing"
427
539
  fail=1
428
540
  fi
429
541
 
542
+ # Shipped-manifest version lockstep. Distinct from the tag lane above: that one compares the git TAG
543
+ # to package.json; this one compares package.json to every version string it SHIPS — including the
544
+ # per-plugin entries inside marketplace.json, which the tag lane never opens. Measured 2026-08-06:
545
+ # a bump left the second marketplace entry behind and the tag lane passed 8/8 straight through it.
546
+ if [ ! -f scripts/version_lockstep_check.sh ]; then
547
+ echo "SKIP test_version_lockstep_lanes.sh (subject scripts/version_lockstep_check.sh absent)"
548
+ elif [ -f scripts/test_version_lockstep_lanes.sh ]; then
549
+ if _out=$(bash scripts/test_version_lockstep_lanes.sh 2>&1); then
550
+ echo "PASS test_version_lockstep_lanes.sh (drift blocks · aligned silent · unreadable = exit 2, not pass)"
551
+ else
552
+ echo "FAIL test_version_lockstep_lanes.sh: the shipped-manifest lockstep guard would mis-route"
553
+ _show_failure "$_out"
554
+ fail=1
555
+ fi
556
+ else
557
+ echo "FAIL test_version_lockstep_lanes.sh: version_lockstep_check.sh present but its anchor is missing"
558
+ fail=1
559
+ fi
560
+
430
561
  # ④-e dispatch-log reconciliation + its tally hook. Wired in the same commit that ships them: the
431
562
  # obligation they mechanize lost 20/20 in a single session, so leaving the checker itself unrun
432
563
  # would be the same defect one layer up.
433
564
  if [ -f scripts/test_dispatch_log_lanes.sh ]; then
434
- if ! bash scripts/test_dispatch_log_lanes.sh >/dev/null 2>&1; then
565
+ if _out=$(bash scripts/test_dispatch_log_lanes.sh 2>&1); then
566
+ echo "PASS test_dispatch_log_lanes.sh (date-spelling + verdict + tally-hook lanes)"
567
+ else
435
568
  echo "FAIL test_dispatch_log_lanes.sh: the dispatch-log reconciliation would mis-report"
436
- bash scripts/test_dispatch_log_lanes.sh 2>&1 | tail -14
569
+ _show_failure "$_out"
437
570
  fail=1
438
- else
439
- echo "PASS test_dispatch_log_lanes.sh (date-spelling + verdict + tally-hook lanes)"
440
571
  fi
441
572
  fi
442
573
 
@@ -445,12 +576,12 @@ fi
445
576
  # through in silence, then a package discriminator keyed on a file that actually ships. Both are
446
577
  # known-POSITIVEs in the suite, so neither can come back green.
447
578
  if [ -f scripts/test_selfcheck_state_lanes.sh ]; then
448
- if ! bash scripts/test_selfcheck_state_lanes.sh >/dev/null 2>&1; then
579
+ if _out=$(bash scripts/test_selfcheck_state_lanes.sh 2>&1); then
580
+ echo "PASS test_selfcheck_state_lanes.sh (four input states + both shipped mis-routings)"
581
+ else
449
582
  echo "FAIL test_selfcheck_state_lanes.sh: a subject-presence discriminator would mis-route"
450
- bash scripts/test_selfcheck_state_lanes.sh 2>&1 | tail -12
583
+ _show_failure "$_out"
451
584
  fail=1
452
- else
453
- echo "PASS test_selfcheck_state_lanes.sh (four input states + both shipped mis-routings)"
454
585
  fi
455
586
  fi
456
587
 
@@ -463,12 +594,34 @@ fi
463
594
  if [ ! -f scripts/sync-from-be.sh ]; then
464
595
  echo "SKIP sync_from_be_lanes.sh (subject scripts/sync-from-be.sh absent)"
465
596
  elif [ -f scripts/sync_from_be_lanes.sh ]; then
466
- if ! bash scripts/sync_from_be_lanes.sh >/dev/null 2>&1; then
597
+ # ── RUN-ONCE, CAPTURE canonical note for the four LANE BLOCKS (2026-08-05) ─────────────────
598
+ # SCOPE, stated precisely because the first draft of this note over-claimed: this covers the four
599
+ # lane blocks only (tag-version · dispatch-log · selfcheck-state · sync_from_be). The same
600
+ # evidence-discarding shape SURVIVES in `check()` at the top of this file and in the
601
+ # probe_scope_check caller — both named there, both deliberately out of scope, both still open.
602
+ # An adversarial round caught the original "all four sites in this file" wording as a false
603
+ # completion claim: it would have stopped the next reader from re-searching. Half a fix with a
604
+ # done-label on it is worse than half a fix.
605
+ # The old form ran the suite twice: once discarded to /dev/null to decide, once re-run to print.
606
+ # For a DETERMINISTIC suite that is merely wasteful. For a non-deterministic one it destroys the
607
+ # evidence: the failing run's output goes to /dev/null and the reader is shown the SECOND run,
608
+ # which may pass. Measured here 2026-08-04 (run 30955950695) — CI printed
609
+ # FAIL sync_from_be_lanes.sh: return-path lanes failed
610
+ # ════ lanes: 70 passed · 0 failed ════
611
+ # i.e. a FAIL verdict over a PASSING transcript, and the actual failure was never recorded
612
+ # anywhere. That is why this lane sat "flaky, cause unknown" on the session card for two days:
613
+ # the instrument was discarding the only evidence that could close it. Reproduced as a known
614
+ # pair before this edit (arm A run-twice → evidence lost + self-contradiction; arm B run-once →
615
+ # evidence preserved), so the fix is anchored, not asserted.
616
+ # NOTE this does NOT make the suite deterministic — the underlying non-determinism is still
617
+ # UNDIAGNOSED and stays an open item. It makes the next occurrence diagnosable instead of
618
+ # self-erasing. Do not read a green CI after this change as the flake being fixed.
619
+ if _out=$(bash scripts/sync_from_be_lanes.sh 2>&1); then
620
+ echo "PASS sync_from_be_lanes.sh (return-path lanes)"
621
+ else
467
622
  echo "FAIL sync_from_be_lanes.sh: return-path lanes failed"
468
- bash scripts/sync_from_be_lanes.sh 2>&1 | tail -20
623
+ _show_failure "$_out"
469
624
  fail=1
470
- else
471
- echo "PASS sync_from_be_lanes.sh (return-path lanes)"
472
625
  fi
473
626
  else
474
627
  echo "FAIL sync_from_be_lanes.sh: sync-from-be.sh present but its anchor is missing"
@@ -15,6 +15,9 @@
15
15
 
16
16
  set -uo pipefail
17
17
 
18
+ # 상속된 git 환경변수를 끊는다 — export 된 GIT_DIR 이 있으면 `git -C "$FH"` 가 인자로 받은
19
+ # 레포가 아니라 그 레포를 잰다(Axis 2 at-floor LOW, 2026-08-06). READ-ONLY 체커라 부작용 없음.
20
+ unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
18
21
  FH="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
19
22
  TODAY=$(date +%Y-%m-%d)
20
23
  CARD="$FH/tracks/_meta/reference_next_session_starter.md"
@@ -25,11 +28,58 @@ _mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0;
25
28
  echo "── session close check: $FH ($TODAY) ──"
26
29
 
27
30
  # ① status snapshot — uncommitted / unpushed work must be known, not forgotten
28
- DIRTY=$(git -C "$FH" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
29
- UNPUSHED=$(git -C "$FH" log --oneline @{u}.. 2>/dev/null | wc -l | tr -d ' ')
30
- [ "$DIRTY" -gt 0 ] && echo "⚠️ ① $DIRTY uncommitted path(s) decide: commit or leave deliberately"
31
- [ "$UNPUSHED" -gt 0 ] && echo "⚠️ ① $UNPUSHED unpushed commit(s) push before close or record why"
32
- [ "$DIRTY" -eq 0 ] && [ "$UNPUSHED" -eq 0 ] && echo "✅ ① working tree clean, nothing unpushed"
31
+ # DIRTY 같은 형태였다 — `status --porcelain 2>/dev/null | wc -l` git 죽어도 0 을 세어
32
+ # "깨끗함"으로 승격된다(재현: `.git/index` 손상 exit 128, 출력 0줄). 종료코드를 먼저 본다.
33
+ # ⚠️ 줄은 **바로 아래 UNPUSHED 수리와 같은 결함**이었고, 처음엔 아래만 고쳤다
34
+ # 반쪽 수리를 고치는 커밋에서 반쪽 수리를 뻔했다(Axis 2 챌린저가 잡음, 2026-08-06).
35
+ # `--untracked-files=all` `status.showUntrackedFiles=no` config 덮어쓴다. config 아래에서는
36
+ # git 이 **성공적으로 침묵**해(exit 0 · 빈 출력) 종료코드 가드로도 안 잡힌다 — 부재가 다시
37
+ # "깨끗함"으로 렌더된다(Axis 2 at-floor MED, 재현: 미추적 파일 1건이 0 으로 보고됨).
38
+ if _st=$(git -C "$FH" status --porcelain --untracked-files=all 2>/dev/null); then
39
+ DIRTY_KNOWN=1
40
+ DIRTY=$(printf '%s' "$_st" | grep -c . || true)
41
+ else
42
+ DIRTY_KNOWN=0
43
+ DIRTY=0
44
+ fi
45
+ # upstream 유무를 **먼저 판정**한다. `@{u}..` 는 upstream 이 없으면 실패해 0줄을 내고, 그 0을
46
+ # `wc -l` 이 0으로 세어 "nothing unpushed" 로 승격된다 — **한 번도 머신을 떠난 적 없는 커밋이
47
+ # '푸시할 것 없음'으로 읽히는 fail-open**. 부재를 깨끗함으로 읽는 것이라 0 을 신뢰하면 안 된다.
48
+ # (downstream fork's review lane caught it first; this file is the upstream original — 2026-08-06.)
49
+ if git -C "$FH" rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1; then
50
+ UPSTREAM_KNOWN=1
51
+ UNPUSHED=$(git -C "$FH" log --oneline @{u}.. 2>/dev/null | wc -l | tr -d ' ')
52
+ else
53
+ UPSTREAM_KNOWN=0
54
+ UNPUSHED=0
55
+ fi
56
+ [ "$DIRTY_KNOWN" -eq 0 ] && echo "⚠️ ① UNMEASURED — git status failed; working-tree cleanliness is UNKNOWN, not clean"
57
+ [ "$DIRTY_KNOWN" -eq 1 ] && [ "$DIRTY" -gt 0 ] && echo "⚠️ ① $DIRTY uncommitted path(s) — decide: commit or leave deliberately"
58
+ [ "$UPSTREAM_KNOWN" -eq 0 ] && echo "⚠️ ① UNMEASURED — no upstream for this branch; unpushed count is UNKNOWN, not zero"
59
+ [ "$UPSTREAM_KNOWN" -eq 1 ] && [ "$UNPUSHED" -gt 0 ] && echo "⚠️ ① $UNPUSHED unpushed commit(s) — push before close or record why"
60
+ # "as of last fetch" — 원격을 조회하지 않는다. 로컬 remote-tracking ref 가 낡았으면 이 0 도 낡은 값이다
61
+ # (Axis 2 챌린저 MED, 2026-08-06). fetch 를 넣지 않은 것은 마감 체커가 READ-ONLY·오프라인 안전이기 때문.
62
+ # 추적 파일이 assume-unchanged/skip-worktree 로 마킹돼 있으면 그 수정은 porcelain 에 **안 뜬다** —
63
+ # `-uall` 로도 안 잡히는 별개 계기다(Axis 2 at-floor MED, 재현 확인).
64
+ MASKED=$(git -C "$FH" ls-files -v 2>/dev/null | grep -c '^[a-z]' || true)
65
+ [ "${MASKED:-0}" -gt 0 ] \
66
+ && echo "⚠️ ① $MASKED file(s) assume-unchanged/skip-worktree — their edits are INVISIBLE here"
67
+
68
+ # ★ 잰 범위 ≠ 주장 범위 (Axis 2 at-floor HIGH, 2026-08-06).
69
+ # `@{u}..` 는 **현재 브랜치만** 잰다. 다른 로컬 브랜치에만 있는 미푸시 커밋은 통째로 안 보이는데
70
+ # 화면 문구는 "nothing unpushed"(레포 전체)라고 말한다 — 이 파일의 존재 이유 정중앙이다.
71
+ # 재현: 다른 브랜치에 미푸시 커밋 1건 → `✅ nothing unpushed` 가 그대로 떴다.
72
+ # 원격이 하나도 없으면 이 값이 전 커밋 수로 부풀므로 원격 존재를 먼저 가드한다.
73
+ OTHER_UNPUSHED=0
74
+ if [ -n "$(git -C "$FH" remote 2>/dev/null)" ]; then
75
+ OTHER_UNPUSHED=$(git -C "$FH" log --branches --not --remotes --oneline 2>/dev/null | wc -l | tr -d ' ')
76
+ fi
77
+ [ "${OTHER_UNPUSHED:-0}" -gt 0 ] \
78
+ && echo "⚠️ ① $OTHER_UNPUSHED commit(s) on local branches never pushed anywhere (all-branch scan)"
79
+
80
+ [ "$DIRTY_KNOWN" -eq 1 ] && [ "$DIRTY" -eq 0 ] && [ "$UPSTREAM_KNOWN" -eq 1 ] && [ "$UNPUSHED" -eq 0 ] \
81
+ && [ "${OTHER_UNPUSHED:-0}" -eq 0 ] && [ "${MASKED:-0}" -eq 0 ] \
82
+ && echo "✅ ① working tree clean, nothing unpushed anywhere (as of last fetch)"
33
83
 
34
84
  # ①-b open-PR sweep (surface-not-auto — requires gh; skip silently offline)
35
85
  if command -v gh >/dev/null 2>&1; then
@@ -163,6 +163,15 @@ cat > "$TMP/s5_positive.sh" <<'EOF'
163
163
  set -uo pipefail
164
164
  N=$(find /nope . -maxdepth 1 2>/dev/null | grep -c . || echo 0)
165
165
  M=$(git log --oneline 2>/dev/null | wc -l || echo 0)
166
+ # WIDENED 2026-08-04. Every line below was INVISIBLE to the narrowed rule, and each was verified to
167
+ # actually produce "0\n0" before being pinned here (line count measured, not assumed):
168
+ P=$(cat /etc/hosts | grep -c . | tr -d ' ' || echo 0) # transparent filter after the counter
169
+ Q=$(grep -c "^nosuchline$" /etc/hosts 2>/dev/null | tr -d ' ' || echo 0) # the PR #251 shape
170
+ R=$(grep -Ec "^nosuchline$" /etc/hosts || echo 0) # combined flag cluster -Ec
171
+ S=$(grep --count "^nosuchline$" /etc/hosts || echo 0) # long option
172
+ T=$(grep -Fcx "nosuchline" /etc/hosts || echo 0) # -Fcx
173
+ U=$(false | grep -c . | cat || echo 0) # trailing stage that always emits
174
+ V=$(false | grep -c . | grep -v nosuch || echo 0) # trailing grep whose pattern misses the "0"
166
175
  EOF
167
176
  cat > "$TMP/s5_negative.sh" <<'EOF'
168
177
  #!/usr/bin/env bash
@@ -174,8 +183,8 @@ J=$(printf '%s' "$x" | jq -r '.a // 0' 2>/dev/null || echo 0)
174
183
  # A comment describing the defect must not be scored as the defect: cmd | grep -c . || echo 0
175
184
  EOF
176
185
  n=$(s_hits "$TMP/s5_positive.sh")
177
- [ "$n" -eq 2 ] && ok "S5 known-positive: counter-stage fallbacks (grep -c, wc) detected 2/2" \
178
- || bad "S5 known-positive: expected 2 S-hits, got $n — the pipefail disarm is invisible"
186
+ [ "$n" -eq 9 ] && ok "S5 known-positive: 9/9 — incl. -Ec/--count/-Fcx flag clusters, trailing \`| cat\`/\`| grep -v\`, and the no-upstream-pipe form" \
187
+ || bad "S5 known-positive: expected 9 S-hits, got $n — 2 = the pre-2026-08-04 rule (pipe-presence anchor); 4 = the first widening, which still missed every combined flag cluster (\`-Ec\`, \`--count\`, \`-Fcx\`) and any trailing stage outside a hardcoded name list"
179
188
 
180
189
  n=$(s_hits "$TMP/s5_negative.sh")
181
190
  [ "$n" -eq 0 ] && ok "S5 known-negative: \`||\` chains, empty-on-failure pipelines and comments stay silent" \
@@ -75,7 +75,14 @@ PY
75
75
  [ "$ok" -eq 0 ] ; chk $? "hook ends in exit 0 — a non-zero hook exit discards its stdout SILENTLY"
76
76
  # run it against a scratch HUB and confirm it appends today's date
77
77
  CLAUDE_PROJECT_DIR="$T/hub" bash -c "$cmd" >/dev/null 2>&1
78
- [ "$(grep -c "$(date +%Y-%m-%d)" "$T/hub/tracks/_meta/.subagent_dispatch_tally" 2>/dev/null || echo 0)" -ge 1 ]
78
+ # Split + sanitize, not `grep -c || echo 0`: on no-match `grep -c` PRINTS "0" and exits 1, so
79
+ # the fallback appends a SECOND line and `[ -ge ]` dies with "integer expression expected".
80
+ # Here that error happens to land on the FAIL branch — honest scope: this was never a live
81
+ # fail-open, it was a verdict reached by a bash error instead of a comparison, one refactor
82
+ # away from flipping. Found by the S5 sweep after the rule was widened (2026-08-04).
83
+ _tally_n=$(grep -c "$(date +%Y-%m-%d)" "$T/hub/tracks/_meta/.subagent_dispatch_tally" 2>/dev/null); _tally_n=${_tally_n:-0}
84
+ case "$_tally_n" in (*[!0-9]*|'') _tally_n=0 ;; esac
85
+ [ "$_tally_n" -ge 1 ]
79
86
  chk $? "hook actually appends a dated line when run (not merely present)"
80
87
  fi
81
88
  else
@@ -93,6 +93,109 @@ else
93
93
  echo " ⏭️ package.json absent — premise unchecked (not a pass)"
94
94
  fi
95
95
 
96
+ echo ""
97
+ echo "── _show_failure: a FAILING suite's evidence must survive to the reader ──"
98
+ # WHY THIS LANE EXISTS (2026-08-05): the four lane blocks in selfcheck.sh used to decide on a
99
+ # discarded run (`>/dev/null`) and then RE-RUN to print. On a non-deterministic suite the re-run can
100
+ # pass, so CI printed a FAIL verdict above a PASSING transcript and the real failure was destroyed —
101
+ # measured in run 30955950695. The repair captures once; this lane is the mechanical anchor for the
102
+ # half that actually makes a failure readable. Without it the repair is unverifiable: reverting to
103
+ # `tail -20` leaves CI green, which is exactly [[feedback_built_but_not_wired]] / anchor-is-decorative.
104
+ # LIFTED, not re-spelled — same reason as the discriminator above.
105
+ FN=$(sed -n '/^_show_failure() {/,/^}$/p' "$SELFCHECK")
106
+ if [ -z "$FN" ]; then
107
+ echo "FAIL _show_failure is no longer defined in selfcheck.sh — this lane cannot verify what it claims."
108
+ echo " If the helper was renamed or removed, update the lane WITH the subject."
109
+ exit 1
110
+ fi
111
+ eval "$FN"
112
+
113
+ # Fixture: a long transcript whose ONLY failing line sits far above any tail window, plus a
114
+ # summary banner at the end that still says something failed. This is the shape that fooled the
115
+ # reader in the CI run above.
116
+ _LONG=$(for i in $(seq 1 40); do echo " ✅ lane L$i ok"; done; echo " ❌ lane L41 tripped — THE EVIDENCE"; for i in $(seq 42 96); do echo " ✅ lane L$i ok"; done; echo "════ lanes: 96 passed · 1 failed ════")
117
+
118
+ _OUT=$(_show_failure "$_LONG")
119
+ printf '%s' "$_OUT" | grep -q 'THE EVIDENCE'; chk $? "the failing line survives (it is 56 lines above the end)"
120
+ printf '%s' "$_OUT" | grep -q '1 failed' ; chk $? "the summary banner is still shown"
121
+
122
+ # CONTROL — the old form must FAIL this same fixture. Without this, the lane could pass for a
123
+ # reason unrelated to the repair (e.g. a fixture short enough that any tail window catches it).
124
+ printf '%s\n' "$_LONG" | tail -20 | grep -q 'THE EVIDENCE'; [ $? -ne 0 ]
125
+ chk $? "CONTROL: the pre-repair form (tail -20) does NOT surface it — the fixture discriminates"
126
+
127
+ # Degenerate inputs: silence must not read as evidence, and a suite that dies before printing any
128
+ # ❌ must say so rather than showing a blank.
129
+ _OUT=$(_show_failure "")
130
+ printf '%s' "$_OUT" | grep -q 'no output captured'; chk $? "empty output is named, not shown as a blank line"
131
+ _OUT=$(_show_failure "some early crash text
132
+ Traceback: boom")
133
+ printf '%s' "$_OUT" | grep -q 'died early'; chk $? "output with no ❌/FAIL falls back and says why"
134
+
135
+ # ── THE DEFECT ITSELF: decide-and-print must be ONE execution ────────────────
136
+ # An earlier version of this lane block tested only the _show_failure HELPER, in isolation, via eval.
137
+ # An adversarial round then reverted a lane block to the original run-twice form — decide on a
138
+ # discarded run, re-run to capture — and this suite still returned PASS (16/16, measured). The anchor
139
+ # was guarding the thing the repair BUILT and not the thing the repair FIXED. That is
140
+ # [[feedback_anchor_can_be_decorative]] with the reversal actually applied, which is the only check
141
+ # that distinguishes the two.
142
+ # The invariant that discriminates: a suite must be EXECUTED EXACTLY ONCE per selfcheck run. The
143
+ # run-twice form necessarily names its subject twice. Keying on the subject path (not on a variable
144
+ # name or a pipe shape) also removes the earlier grep's escape hatch — renaming `_out` no longer
145
+ # evades it, and adding a fifth lane block does not require editing a hardcoded count.
146
+ for _subj in test_tag_version_lanes test_dispatch_log_lanes test_selfcheck_state_lanes sync_from_be_lanes; do
147
+ _n=$(grep -c "bash scripts/${_subj}\.sh" "$SELFCHECK" || true)
148
+ [ "$_n" -eq 1 ]
149
+ chk $? "${_subj}.sh is executed exactly once (found $_n) — 2 means the run-twice form is back"
150
+ done
151
+
152
+ # WIRING — every lane block must route its captured output through the helper. Secondary to the
153
+ # once-only invariant above (this one IS evadable by renaming), kept because it names the intent.
154
+ _CALLS=$(grep -c '_show_failure "\$_out"' "$SELFCHECK")
155
+ [ "$_CALLS" -ge 4 ]; chk $? "every lane block routes failure output through _show_failure (found $_CALLS, expected ≥4)"
156
+ _TAILS=$(grep -c '"\$_out" | tail -' "$SELFCHECK" || true)
157
+ [ "$_TAILS" -eq 0 ]; chk $? "no lane block still truncates with a raw tail (found $_TAILS, expected 0)"
158
+
159
+ # The one non-lane caller that also destroys its evidence at the CALL SITE (not inside check()).
160
+ # `check "..." bash -c '... >/dev/null'` discards the subject's stdout, and fh-codex-doctor writes
161
+ # 100% of its diagnostics to stdout (measured: 686 B stdout / 0 B stderr) — so a strict-mode failure
162
+ # would print a bare FAIL line with zero diagnosis. Anchored here because the fix is one line at the
163
+ # call site and does NOT require touching check() itself.
164
+ _CD=$(grep -c "fh-codex-doctor.js --strict >/dev/null" "$SELFCHECK" || true)
165
+ [ "$_CD" -eq 0 ]; chk $? "fh-codex-doctor's stdout is not discarded at the call site (found $_CD, expected 0)"
166
+
167
+ # Byte-hostile input: a lane emitting invalid UTF-8 must not be reported as "no output". The
168
+ # `tr -d '[:space:]'` form this guard originally used aborts on BSD with "Illegal byte sequence"
169
+ # and emits nothing, so the emptiness check concluded empty while evidence was present.
170
+ # NOTE ON THIS FIXTURE — it deliberately contains NO ❌. The first draft included one, which routed
171
+ # the call into the failing-lines branch, so the emptiness guard (an `elif`) was never reached and the
172
+ # lane passed against the very defect it was written for. Verified by applying the reversion and
173
+ # confirming the diff landed: the lane went green anyway. A fixture that cannot reach the branch
174
+ # under test measures nothing.
175
+ _BAD=$(printf ' \xff\xfe garbage\n crashed before any lane ran BYTE_EVIDENCE\n')
176
+ _OUT=$(_show_failure "$_BAD")
177
+ printf '%s' "$_OUT" | grep -q 'BYTE_EVIDENCE'; chk $? "invalid UTF-8 in the stream does not swallow the evidence"
178
+ printf '%s' "$_OUT" | grep -qv 'no output captured'; chk $? "…and it is not mis-reported as empty output"
179
+
180
+ # The banner must not be suppressed by a line the reader never saw (a failing line past the head cut
181
+ # that merely quotes the banner text).
182
+ _MANY=$(for i in $(seq 1 26); do echo " ❌ f$i"; done; echo " ❌ f27 quoting ════ lanes: 9 passed · 1 failed ════"; echo "════ lanes: 9 passed · 1 failed ════")
183
+ _OUT=$(_show_failure "$_MANY")
184
+ [ "$(printf '%s\n' "$_OUT" | grep -c '════ lanes: 9 passed')" -ge 1 ]
185
+ chk $? "the summary banner still prints when a truncated-away line quotes it"
186
+
187
+ # ── SCOPE OF THIS ANCHOR — stated so it is not over-trusted ───────────────────
188
+ # These lanes catch REVERSION (the run-twice form coming back, the helper being gutted, the
189
+ # call-site redirect returning). They do NOT catch deliberate EVASION: a cross-family round
190
+ # demonstrated three forms that satisfy every check above while still destroying evidence —
191
+ # `_out=$(bash suite >/dev/null 2>&1)` (executed once, captured nothing), a wrapper function
192
+ # (`run_lane() { bash scripts/X.sh; }`, literal appears once, runs twice), and redirect spellings
193
+ # the greps do not enumerate (`1>/dev/null`, a variable). Chasing those with more patterns is the
194
+ # Grep-Collision Treadmill this repo has already logged as P10 — each added regex relocates the
195
+ # evasion instead of closing it. It is bounded rather than escalated: an evading form still routes
196
+ # through _show_failure, whose empty branch prints "(no output captured)" at runtime, so the failure
197
+ # is loud rather than silent. Regression is anchored; evasion is a named residual, not a solved one.
198
+
96
199
  echo ""
97
200
  if [ "$FAILED" -ne 0 ]; then
98
201
  echo "SELFCHECK STATE LANES: FAIL — a discriminator would mis-route"
@@ -85,6 +85,18 @@ _repo() { # $1=dirname ; makes a git repo with one commit dated $2 (default now
85
85
  else
86
86
  git commit -qm seed
87
87
  fi
88
+ # A real repo HAS an upstream. Without one the close check now (correctly) reports
89
+ # `① UNMEASURED — no upstream`, which is not the state any of these lanes means to express —
90
+ # a fixture with no upstream cannot assert "clean tree, nothing unpushed" because the second
91
+ # half of that sentence is genuinely unknown. Every lane below therefore runs on a pushed
92
+ # baseline; upstream ABSENCE is measured on purpose by its own lane (KP-2).
93
+ git init -q --bare "$TMPROOT/$1.git"
94
+ git remote add origin "$TMPROOT/$1.git"
95
+ # `-c core.hooksPath=` : a fixture push must never execute the HOST's git hooks. This repo sets
96
+ # core.hooksPath locally (so a temp repo does not inherit it) but a machine that sets it GLOBALLY
97
+ # would run FH's own pre-push Destructive-Op gate against a throwaway fixture — the suite's result
98
+ # would then depend on the operator's git config rather than on the code under test.
99
+ git -c core.hooksPath= push -q -u origin HEAD
88
100
  ) >/dev/null 2>&1
89
101
  mkdir -p "$T/tracks/_meta"
90
102
  printf '%s' "$T"
@@ -121,13 +133,10 @@ _line "①-N uncommitted path → ⚠️ fires" 'uncommitted path'
121
133
  _line "①-N uncommitted path → clean line absent" '✅ ① working tree clean' 0 "$OUT"
122
134
  _rc "①-N uncommitted is ADVISORY, not blocking" "$RC" 0
123
135
 
124
- # unpushed: needs a real upstream, so build a bare remote
136
+ # unpushed: _repo already pushed the seed, so one extra commit is exactly one unpushed commit
125
137
  T=$(_repo one_unpushed); _artifacts "$T"
126
138
  (
127
139
  cd "$T" || exit 1
128
- git init -q --bare "$TMPROOT/one_unpushed.git" 2>/dev/null
129
- git remote add origin "$TMPROOT/one_unpushed.git"
130
- git push -q -u origin HEAD 2>/dev/null
131
140
  echo more > second.txt && git add -A && git commit -qm second
132
141
  ) >/dev/null 2>&1
133
142
  _run "$T"
@@ -141,6 +150,72 @@ _gap "① non-repo reports CLEAN" "$_g" \
141
150
  "git is unavailable/not a repo → DIRTY=0, UNPUSHED=0 → the check reports '✅ working tree clean'. \
142
151
  An instrument that could not look is not a clean result (not found ≠ 0). Should say UNSCANNED."
143
152
 
153
+ # ── ① not-found ≠ 0 : the five states where git CANNOT answer ────────────────────
154
+ # Origin: the fix that introduced these five guards (2026-08-06) listed all six known pairs in its
155
+ # COMMIT MESSAGE and shipped none of them as a lane — the code changed, the suite did not, and CI
156
+ # went red on the two lanes the change broke rather than on the five it left unmeasured. Prose in a
157
+ # commit message is not a regression anchor: nothing re-runs it. Each pair below is
158
+ # known-positive (the instrument is blind) + a paired control (the clean line must NOT appear),
159
+ # because "the warning fired" and "the warning fired INSTEAD of a false all-clear" are two claims.
160
+ # KP-1 (the healthy case) is the ①-P lane at the top of this section.
161
+
162
+ # KP-2 upstream absent — `@{u}..` fails, prints 0 lines, and `wc -l` counts that 0 as "nothing
163
+ # unpushed". A commit that never left the machine reads as pushed. `--unset-upstream` (not
164
+ # `remote remove`) keeps a remote present, so the all-branch scan still runs: this isolates the
165
+ # upstream leg instead of quietly testing two things at once.
166
+ T=$(_repo kp2_no_upstream); _artifacts "$T"
167
+ git -C "$T" branch --unset-upstream >/dev/null 2>&1
168
+ _run "$T"
169
+ _line "KP-2 no upstream → UNMEASURED, not zero" 'unpushed count is UNKNOWN' 1 "$OUT"
170
+ _line "KP-2 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
171
+ _rc "KP-2 → advisory, not blocking" "$RC" 0
172
+
173
+ # KP-3 git status itself fails — a corrupt index makes `status` exit non-zero with EMPTY output,
174
+ # and `| wc -l` renders that emptiness as "0 dirty paths" = clean.
175
+ T=$(_repo kp3_broken_index); _artifacts "$T"
176
+ printf 'garbage' > "$T/.git/index"
177
+ _run "$T"
178
+ _line "KP-3 corrupt index → cleanliness UNKNOWN" 'cleanliness is UNKNOWN' 1 "$OUT"
179
+ _line "KP-3 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
180
+ # Measured while writing this lane: a corrupt index makes `ls-files -v` exit 128 too, so the
181
+ # assume-unchanged probe (MASKED) silently reads 0 — the same not-found-≠-0 shape, one layer in.
182
+ # It is NOT a false all-clear (DIRTY_KNOWN=0 already suppresses the clean line), so it is recorded
183
+ # as a residual rather than patched here. `rev-parse @{u}` still exits 0 under a corrupt index,
184
+ # which is what keeps this lane measuring cleanliness and not accidentally re-measuring KP-2.
185
+
186
+ # KP-4 measured scope ≠ claimed scope — `@{u}..` reads the CURRENT branch only, while the message
187
+ # says "nothing unpushed" about the repo. An unpushed commit parked on another local branch is
188
+ # invisible. The current branch stays clean and pushed on purpose: only the other branch is dirty,
189
+ # so a green here would be the exact false all-clear.
190
+ T=$(_repo kp4_other_branch); _artifacts "$T"
191
+ (
192
+ cd "$T" || exit 1
193
+ git checkout -q -b side
194
+ echo side > side.txt && git add -A && git commit -qm side
195
+ git checkout -q -
196
+ ) >/dev/null 2>&1
197
+ _run "$T"
198
+ _line "KP-4 unpushed on ANOTHER branch → ⚠️ fires" 'never pushed anywhere' 1 "$OUT"
199
+ _line "KP-4 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
200
+
201
+ # KP-5 `status.showUntrackedFiles=no` — git succeeds and stays SILENT (exit 0, empty output), so
202
+ # the exit-code guard of KP-3 cannot catch this one. Only `--untracked-files=all` overrides it.
203
+ T=$(_repo kp5_untracked_off); _artifacts "$T"
204
+ git -C "$T" config status.showUntrackedFiles no
205
+ echo hidden > "$T/hidden.txt"
206
+ _run "$T"
207
+ _line "KP-5 showUntrackedFiles=no → still counted" 'uncommitted path' 1 "$OUT"
208
+ _line "KP-5 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
209
+
210
+ # KP-6 assume-unchanged / skip-worktree — edits to a marked TRACKED file never reach porcelain at
211
+ # all, so `-uall` does not help either. A separate instrument (`ls-files -v`) has to surface it.
212
+ T=$(_repo kp6_assume_unchanged); _artifacts "$T"
213
+ git -C "$T" update-index --assume-unchanged unrelated.txt
214
+ echo edited >> "$T/unrelated.txt"
215
+ _run "$T"
216
+ _line "KP-6 assume-unchanged edit → surfaced" 'INVISIBLE here' 1 "$OUT"
217
+ _line "KP-6 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
218
+
144
219
  echo
145
220
  echo "══ ①-b open-PR sweep ══"
146
221
  _ghstub() { # $1=repo $2=stdout $3=exit