@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +36 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +37 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +125 -0
- package/package.json +4 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/degrade_direction_scan.sh +34 -2
- package/scripts/gate_pathspec_check.sh +6 -1
- package/scripts/prepush_guard_check.sh +15 -0
- package/scripts/psa_scan_lib.sh +36 -0
- package/scripts/selfcheck.sh +170 -17
- package/scripts/session_close_check.sh +55 -5
- package/scripts/test_degrade_scan_shell_probes.sh +11 -2
- package/scripts/test_dispatch_log_lanes.sh +8 -1
- package/scripts/test_selfcheck_state_lanes.sh +103 -0
- package/scripts/test_session_close_chain_lanes.sh +79 -4
- package/scripts/test_version_lockstep_lanes.sh +82 -0
- package/scripts/universal_guard_check.sh +19 -6
- package/scripts/version_lockstep_check.sh +80 -0
- package/templates/.git-hooks/pre-commit +8 -0
- package/templates/.git-hooks/pre-push +14 -2
- package/templates/degrade_direction_scan.sh +34 -2
- package/templates/regression_guard.sh +25 -0
package/scripts/selfcheck.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
178
|
-
|
|
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
|
-
|
|
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:
|
|
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
|