@chrono-meta/fh-gate 1.4.85 → 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.
- package/.claude/rules/fh_4axis_gate.md +1 -1
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +8 -0
- package/CLAUDE.md +30 -6
- package/knowledge/shared/harness-core/capability_composition_contract.md +416 -0
- package/knowledge/shared/harness-core/fh_detail_protocols.md +4 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +116 -0
- package/knowledge/shared/rules/auto_project_mapping.md +64 -1
- package/knowledge/shared/rules/operations.md +26 -0
- package/package.json +8 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +25 -0
- package/plugins/fh-meta/skills/prompt-regression/SKILL.md +1 -1
- package/scripts/gate_pathspec_check.sh +85 -0
- package/scripts/package_coverage_check.sh +27 -0
- package/scripts/selfcheck.sh +98 -1
- package/scripts/session_close_check.sh +18 -4
- package/scripts/test_dispatch_log_lanes.sh +33 -0
- package/scripts/test_node_infra_delta_lanes.sh +211 -0
- package/scripts/test_session_close_chain_lanes.sh +349 -0
- package/scripts/test_tag_version_lanes.sh +79 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +202 -0
- package/templates/.claude/rules/session.md +28 -5
- package/templates/.git-hooks/pre-commit +9 -2
- package/templates/.git-hooks/pre-push +56 -0
- package/templates/subagent-tally-hook.json +17 -0
|
@@ -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)"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_node_infra_delta_lanes.sh — lanes for the INFRA-DELTA branch of scripts/fh_node_check.sh.
|
|
3
|
+
#
|
|
4
|
+
# WHY A SEPARATE FILE FROM test_node_check_lanes.sh: that suite covers the floor probes (git hooks,
|
|
5
|
+
# Mode D companion detection, emission model) and never enters the infra-delta branch — every one of
|
|
6
|
+
# its fixtures runs against a state file whose PREV_HEAD is either empty (first session) or equal to
|
|
7
|
+
# HEAD. The branch that reacts to `git pull` moving install-relevant files was unexercised, which is
|
|
8
|
+
# how the four items below survived. Keeping it separate also keeps the two suites' fixture families
|
|
9
|
+
# from colliding: this one must CHANGE HEAD between runs, which breaks the "event once then silent"
|
|
10
|
+
# lane in the other file.
|
|
11
|
+
#
|
|
12
|
+
# CALIBRATION RULE OBSERVED THROUGHOUT: every lane asserting an ABSENCE names its known-positive in
|
|
13
|
+
# the lane title and builds it from the SAME fixture family in the SAME run. Exit codes are read
|
|
14
|
+
# DIRECTLY off the subject, never through a pipe.
|
|
15
|
+
#
|
|
16
|
+
# ⓘ GAP lanes pin behaviour believed WRONG without failing the suite; if the behaviour changes they
|
|
17
|
+
# announce "GAP CLOSED" so the lane gets promoted instead of silently rotting.
|
|
18
|
+
#
|
|
19
|
+
# Exit 0 = every asserting lane calibrated · 1 = the instrument is wrong.
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
CHECK="$SCRIPT_DIR/fh_node_check.sh"
|
|
24
|
+
[ -f "$CHECK" ] || { echo "FAIL subject missing: $CHECK"; exit 1; }
|
|
25
|
+
|
|
26
|
+
TMPROOT=$(mktemp -d "${TMPDIR:-/tmp}/fh_infra_lanes.XXXXXX")
|
|
27
|
+
trap 'rm -rf "$TMPROOT"' EXIT
|
|
28
|
+
FAILED=0; PASSED=0; GAPS=0
|
|
29
|
+
|
|
30
|
+
_pass() { echo "✅ $1"; PASSED=$((PASSED+1)); }
|
|
31
|
+
_fail() { echo "❌ $1"; FAILED=1; }
|
|
32
|
+
_line() { # $1=name $2=pattern $3=expect(0/1) $4=output
|
|
33
|
+
local hit=0
|
|
34
|
+
printf '%s\n' "$4" | grep -q -- "$2" && hit=1
|
|
35
|
+
if [ "$hit" = "$3" ]; then _pass "$1 (hit=$hit, expected=$3)"
|
|
36
|
+
else _fail "$1 — hit=$hit, expected=$3"; printf '%s\n' "$4" | sed 's/^/ │ /'; fi
|
|
37
|
+
}
|
|
38
|
+
_rc() { if [ "$2" = "$3" ]; then _pass "$1 (exit=$2, expected=$3)"; else _fail "$1 — exit=$2, expected=$3"; fi; }
|
|
39
|
+
_gap() { # $1=name $2=observed(1=still open) $3=note
|
|
40
|
+
if [ "$2" = "1" ]; then echo "ⓘ GAP (still open) $1"; echo " ↳ $3"; GAPS=$((GAPS+1))
|
|
41
|
+
else echo "🎉 GAP CLOSED — $1 : behaviour changed, promote this lane to an asserting one"; fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ── fixtures ────────────────────────────────────────────────────────────────────
|
|
45
|
+
# A hub with FH's own gate sentinel in both hooks, so MISS is empty and any output is attributable
|
|
46
|
+
# to the infra-delta branch alone (instrument-discrimination: a lane that cannot tell which leg
|
|
47
|
+
# fired is not measuring that leg).
|
|
48
|
+
_hub() { # $1=name → echoes path
|
|
49
|
+
local d="$TMPROOT/$1"
|
|
50
|
+
mkdir -p "$d/.git" || true
|
|
51
|
+
git -C "$d" init -q 2>/dev/null || { mkdir -p "$d"; git -C "$d" init -q; }
|
|
52
|
+
git -C "$d" config user.email anchor@local; git -C "$d" config user.name anchor
|
|
53
|
+
mkdir -p "$d/.git/hooks"
|
|
54
|
+
printf '#!/bin/sh\n# FH 4-Axis Gate\nexit 0\n' > "$d/.git/hooks/pre-commit"
|
|
55
|
+
cp "$d/.git/hooks/pre-commit" "$d/.git/hooks/pre-push"
|
|
56
|
+
chmod +x "$d/.git/hooks/pre-commit" "$d/.git/hooks/pre-push"
|
|
57
|
+
echo seed > "$d/seed.txt"
|
|
58
|
+
git -C "$d" add -A >/dev/null 2>&1
|
|
59
|
+
git -C "$d" -c commit.gpgsign=false commit -qm seed >/dev/null 2>&1
|
|
60
|
+
printf '%s' "$d"
|
|
61
|
+
}
|
|
62
|
+
_commit() { # $1=hub $2..=paths to create
|
|
63
|
+
local d="$1" p; shift
|
|
64
|
+
for p in "$@"; do mkdir -p "$d/$(dirname "$p")"; echo changed >> "$d/$p"; done
|
|
65
|
+
git -C "$d" add -A >/dev/null 2>&1
|
|
66
|
+
git -C "$d" -c commit.gpgsign=false commit -qm change >/dev/null 2>&1
|
|
67
|
+
}
|
|
68
|
+
_run() { # $1=hub $2=statefile ; sets OUT/RC (RC read directly, no pipe)
|
|
69
|
+
OUT=$(HUB_DIR="$1" FH_NODE_STATE="$2" FH_MACHINE_ID=lanebox bash "$CHECK" 2>&1); RC=$?
|
|
70
|
+
}
|
|
71
|
+
# seed the state so PREV_HEAD is the pre-change commit (this is the ONLY way into the branch)
|
|
72
|
+
_seed_state() { _run "$1" "$2" >/dev/null 2>&1; }
|
|
73
|
+
|
|
74
|
+
NEW='🆕 Install-relevant assets changed'
|
|
75
|
+
HDR='🖥️ \[node\]'
|
|
76
|
+
|
|
77
|
+
echo "══ infra delta: fires on an install-relevant change, silent otherwise ══"
|
|
78
|
+
H=$(_hub p_pos); _seed_state "$H" "$TMPROOT/st_pos"
|
|
79
|
+
_commit "$H" scripts/fh_env_delta_scan.sh
|
|
80
|
+
_run "$H" "$TMPROOT/st_pos"
|
|
81
|
+
_line "ID-P install-relevant file changed → 🆕 block fires" "$NEW" 1 "$OUT"
|
|
82
|
+
_line "ID-P → names the changed path" 'scripts/fh_env_delta_scan.sh' 1 "$OUT"
|
|
83
|
+
_line "ID-P → routes to the wizard" 'Re-run /install-wizard' 1 "$OUT"
|
|
84
|
+
_rc "ID-P detector never gates (exit 0)" "$RC" 0
|
|
85
|
+
|
|
86
|
+
# CONTROL for ID-P, same fixture family, same run-shape: HEAD advances by exactly one commit, but
|
|
87
|
+
# the changed path is outside the install surface. Silence here is only meaningful BECAUSE ID-P
|
|
88
|
+
# above spoke on an otherwise identical fixture.
|
|
89
|
+
H=$(_hub p_ctl); _seed_state "$H" "$TMPROOT/st_ctl"
|
|
90
|
+
_commit "$H" README.md
|
|
91
|
+
_run "$H" "$TMPROOT/st_ctl"
|
|
92
|
+
_line "ID-C non-install path changed → no 🆕 block (over-fire ctl for ID-P)" "$NEW" 0 "$OUT"
|
|
93
|
+
_rc "ID-C → exit 0" "$RC" 0
|
|
94
|
+
|
|
95
|
+
echo
|
|
96
|
+
echo "══ one fixture PER regex alternative (an untested alternative is an untested branch) ══"
|
|
97
|
+
# Deliberately one path per alternative: a single fixture touching several at once would let any
|
|
98
|
+
# alternative be deleted with the suite staying green — the lane4c lesson from test_node_check_lanes.
|
|
99
|
+
i=0
|
|
100
|
+
for p in "templates/.git-hooks/pre-push" \
|
|
101
|
+
"templates/settings.SessionStart.snippet.json" \
|
|
102
|
+
"scripts/fh_session_load.sh" \
|
|
103
|
+
"plugins/fh-meta/skills/install-wizard/SKILL.md" \
|
|
104
|
+
"plugins/fh-meta/skills/install-doctor/SKILL.md"; do
|
|
105
|
+
i=$((i+1))
|
|
106
|
+
H=$(_hub "alt$i"); _seed_state "$H" "$TMPROOT/st_alt$i"
|
|
107
|
+
_commit "$H" "$p"
|
|
108
|
+
_run "$H" "$TMPROOT/st_alt$i"
|
|
109
|
+
_line "ID-R$i alternative covered: $p" "$NEW" 1 "$OUT"
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
# NEGATIVE for the regex: install-adjacent-LOOKING paths that the pattern deliberately does not
|
|
113
|
+
# cover. Without this leg "the regex matches everything" would also pass ID-R1..5.
|
|
114
|
+
H=$(_hub alt_neg); _seed_state "$H" "$TMPROOT/st_altneg"
|
|
115
|
+
_commit "$H" "plugins/fh-meta/skills/harvest-loop/SKILL.md"
|
|
116
|
+
_run "$H" "$TMPROOT/st_altneg"
|
|
117
|
+
_line "ID-Rn non-install skill path → not treated as infra (paired with ID-R4/5)" "$NEW" 0 "$OUT"
|
|
118
|
+
|
|
119
|
+
echo
|
|
120
|
+
echo "══ unreachable PREV_HEAD: UNMEASURED, not silence ══"
|
|
121
|
+
H=$(_hub p_unreach); _commit "$H" scripts/fh_env_delta_scan.sh
|
|
122
|
+
printf 'lanebox|1700000000|deadbee' > "$TMPROOT/st_unreach"
|
|
123
|
+
_run "$H" "$TMPROOT/st_unreach"
|
|
124
|
+
_line "ID-U gc/rebase/shallow → UNMEASURED is stated" 'UNMEASURED' 1 "$OUT"
|
|
125
|
+
_line "ID-U → does NOT claim the 🆕 delta (not found ≠ 0)" "$NEW" 0 "$OUT"
|
|
126
|
+
_rc "ID-U → exit 0" "$RC" 0
|
|
127
|
+
|
|
128
|
+
echo
|
|
129
|
+
echo "══ co-reporting: a missing floor does not suppress the infra delta ══"
|
|
130
|
+
H=$(_hub p_both); _seed_state "$H" "$TMPROOT/st_both"
|
|
131
|
+
rm -f "$H/.git/hooks/pre-commit"
|
|
132
|
+
_commit "$H" scripts/fh_node_check.sh
|
|
133
|
+
_run "$H" "$TMPROOT/st_both"
|
|
134
|
+
_line "ID-B missing floor reported" 'Missing mechanical floor' 1 "$OUT"
|
|
135
|
+
_line "ID-B AND infra delta reported (paired)" "$NEW" 1 "$OUT"
|
|
136
|
+
|
|
137
|
+
echo
|
|
138
|
+
echo "══ ⓘ GAP lanes — believed-wrong behaviour, pinned not asserted ══"
|
|
139
|
+
|
|
140
|
+
# GAP 1 — attribution. Every other emission path prints the 🖥️ [node] header; the infra-only and
|
|
141
|
+
# UNMEASURED-only paths print 4-space-indented CONTINUATION lines under a header that was never
|
|
142
|
+
# printed. This is not cosmetic in situ: SessionStart hooks on one matcher have their outputs
|
|
143
|
+
# delivered as separate, unordered, unlabelled blocks (measured in
|
|
144
|
+
# scripts/test_sessionstart_multihook_lanes.sh), so an unattributed indented block genuinely cannot
|
|
145
|
+
# be traced to the hook that emitted it.
|
|
146
|
+
H=$(_hub g_hdr); _seed_state "$H" "$TMPROOT/st_hdr"
|
|
147
|
+
_commit "$H" scripts/fh_env_delta_scan.sh
|
|
148
|
+
_run "$H" "$TMPROOT/st_hdr"
|
|
149
|
+
_g=0
|
|
150
|
+
{ printf '%s\n' "$OUT" | grep -q "$NEW"; } && ! printf '%s\n' "$OUT" | grep -q "$HDR" && _g=1
|
|
151
|
+
_gap "infra-only emission has NO 🖥️ [node] header" "$_g" \
|
|
152
|
+
"Output is 4-space-indented continuation lines with no parent line. MISS and IDENTITY both print a \
|
|
153
|
+
header; the infra and UNMEASURED paths do not, because they hang off an if/elif that both fail."
|
|
154
|
+
|
|
155
|
+
# GAP 2 — silent truncation. `head -6` with no count. The script's own header argues 'not found ≠ 0'
|
|
156
|
+
# and 'distinguish no-change from could-not-measure'; an omitted-3-of-9 list is the same class of
|
|
157
|
+
# hidden omission, one layer in.
|
|
158
|
+
H=$(_hub g_trunc); _seed_state "$H" "$TMPROOT/st_trunc"
|
|
159
|
+
_commit "$H" scripts/fh_a.sh scripts/fh_b.sh scripts/fh_c.sh scripts/fh_d.sh \
|
|
160
|
+
scripts/fh_e.sh scripts/fh_f.sh scripts/fh_g.sh scripts/fh_h.sh scripts/fh_i.sh
|
|
161
|
+
_run "$H" "$TMPROOT/st_trunc"
|
|
162
|
+
LISTED=$(printf '%s\n' "$OUT" | grep -c '^ - ')
|
|
163
|
+
_g=0; { [ "$LISTED" = "6" ] && ! printf '%s\n' "$OUT" | grep -qiE 'more|[0-9]+ of [0-9]+|truncat'; } && _g=1
|
|
164
|
+
_gap "9 install-relevant files changed → 6 listed, 3 dropped with no count" "$_g" \
|
|
165
|
+
"listed=$LISTED, and no '…and N more'. A big pull (exactly when re-running the wizard matters most) \
|
|
166
|
+
silently hides the tail. Cheap fix: append the residual count."
|
|
167
|
+
|
|
168
|
+
# GAP 3 — the one-shot consumes an UNRESOLVED unknown. State is written UNCONDITIONALLY to HEAD_NOW,
|
|
169
|
+
# including on the run that could not compute the delta. Next session PREV_HEAD == HEAD, so the
|
|
170
|
+
# delta across that pull is never attempted again and never mentioned again. The script's header
|
|
171
|
+
# says events report once and CONDITIONS report until they stop being true — an un-computed delta is
|
|
172
|
+
# an unresolved condition, and it is being treated as a discharged event.
|
|
173
|
+
H=$(_hub g_once); _commit "$H" scripts/fh_env_delta_scan.sh
|
|
174
|
+
printf 'lanebox|1700000000|deadbee' > "$TMPROOT/st_once"
|
|
175
|
+
SPOKE=0
|
|
176
|
+
for _i in 1 2 3; do
|
|
177
|
+
_run "$H" "$TMPROOT/st_once"
|
|
178
|
+
printf '%s\n' "$OUT" | grep -q 'UNMEASURED' && SPOKE=$((SPOKE+1))
|
|
179
|
+
done
|
|
180
|
+
_g=0; [ "$SPOKE" = "1" ] && _g=1
|
|
181
|
+
_gap "UNMEASURED spoken once, then permanently forgotten (state advanced anyway)" "$_g" \
|
|
182
|
+
"spoke on $SPOKE of 3 consecutive runs. Same shape as the failure the state-based design was \
|
|
183
|
+
introduced to fix ('a broken machine reported once then went quiet forever') — the fix reached MISS \
|
|
184
|
+
but not the infra branch. CODE-vs-DOC: fh_node_check.sh's own header claims it distinguishes \
|
|
185
|
+
'no change' from 'could not measure', yet after run 1 the two are indistinguishable forever."
|
|
186
|
+
|
|
187
|
+
# GAP 4 — the event is consumed even when nobody saw it. State advances before/independently of
|
|
188
|
+
# whether the emission reached anyone. Pairs directly with the F11 measurement: a SessionStart hook
|
|
189
|
+
# whose sibling ordering, delivery, or exit handling drops its output loses the notice permanently.
|
|
190
|
+
H=$(_hub g_unseen); _seed_state "$H" "$TMPROOT/st_unseen"
|
|
191
|
+
_commit "$H" scripts/fh_env_delta_scan.sh
|
|
192
|
+
HUB_DIR="$H" FH_NODE_STATE="$TMPROOT/st_unseen" FH_MACHINE_ID=lanebox bash "$CHECK" >/dev/null 2>&1
|
|
193
|
+
_run "$H" "$TMPROOT/st_unseen"
|
|
194
|
+
_g=0; printf '%s\n' "$OUT" | grep -q "$NEW" || _g=1
|
|
195
|
+
_gap "infra event is consumed by a run whose output nobody received" "$_g" \
|
|
196
|
+
"One discarded-stdout run and the notice is gone for good. The write is deliberately unconditional \
|
|
197
|
+
(so the timestamp does not mean 'last time it spoke'), but that argument covers the TIMESTAMP, not \
|
|
198
|
+
the HEAD field. Recording HEAD only when the delta was successfully computed AND emitted would keep \
|
|
199
|
+
the timestamp honest and stop discharging unseen events."
|
|
200
|
+
|
|
201
|
+
echo
|
|
202
|
+
echo "──────────────────────────────────────────────"
|
|
203
|
+
if [ "$FAILED" -ne 0 ]; then
|
|
204
|
+
echo "NODE INFRA-DELTA LANES: FAIL — instrument miscalibrated (do not trust its verdict)"
|
|
205
|
+
echo " asserting lanes passed: $PASSED · open gaps: $GAPS"
|
|
206
|
+
exit 1
|
|
207
|
+
fi
|
|
208
|
+
echo "NODE INFRA-DELTA LANES: PASS ($PASSED asserting lanes) · $GAPS KNOWN GAP(S) open (see ⓘ above)"
|
|
209
|
+
echo " Green covers: fires/does-not-fire, per-alternative regex coverage, UNMEASURED, co-reporting."
|
|
210
|
+
echo " Green does NOT cover the four gaps above."
|
|
211
|
+
exit 0
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_session_close_chain_lanes.sh — known-pair anchors for the close-chain steps that had NO
|
|
3
|
+
# mechanical lane: ① (status snapshot) · ①-b (open-PR sweep) · ④-log (real-time completion log, the
|
|
4
|
+
# only exit-1 FAIL path besides ⑤) · ④-b (npm freshness + BIDIRECTIONAL entry-point drift) ·
|
|
5
|
+
# and the pre-push SURFACE MATCHING (ordinary push advises · FH_SESSION_CLOSE=1 push blocks).
|
|
6
|
+
#
|
|
7
|
+
# Already anchored elsewhere, deliberately NOT duplicated here:
|
|
8
|
+
# ②, ⑤ card-last → scripts/test_session_close_lanes.sh
|
|
9
|
+
# ⑤-b card-drift probe → scripts/test_card_drift_probe.sh (incl. the locale-divergence leg)
|
|
10
|
+
# pre-push stdin/ordering → scripts/test_prepush_stdin_integrity.sh
|
|
11
|
+
#
|
|
12
|
+
# CALIBRATION RULE OBSERVED THROUGHOUT: every lane that asserts an ABSENCE is paired with a
|
|
13
|
+
# known-POSITIVE built from the SAME fixture family, so "passed because the guard worked" is
|
|
14
|
+
# distinguishable from "passed because nothing ran". Exit codes are read DIRECTLY off the
|
|
15
|
+
# subject (never through a pipe — `cmd | tail; echo $?` reads tail's status and has produced
|
|
16
|
+
# false green in this repo four times).
|
|
17
|
+
#
|
|
18
|
+
# ⓘ GAP lanes: places where the subject's CURRENT behaviour is believed WRONG (fail-open, or a
|
|
19
|
+
# spec/code disagreement). They pin the observed behaviour but never fail the suite; if the
|
|
20
|
+
# behaviour changes they announce "GAP CLOSED" so the lane gets promoted instead of silently
|
|
21
|
+
# rotting. They are counted and printed separately — a green summary here does NOT mean the
|
|
22
|
+
# chain is fully guarded.
|
|
23
|
+
#
|
|
24
|
+
# Exit 0 = every asserting lane calibrated · exit 1 = the gate's instrument is wrong.
|
|
25
|
+
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
29
|
+
CHECK="$SCRIPT_DIR/session_close_check.sh"
|
|
30
|
+
HOOK="$ROOT/templates/.git-hooks/pre-push"
|
|
31
|
+
TODAY=$(date +%Y-%m-%d)
|
|
32
|
+
FAILED=0
|
|
33
|
+
PASSED=0
|
|
34
|
+
GAPS=0
|
|
35
|
+
|
|
36
|
+
[ -f "$CHECK" ] || { echo "FAIL subject missing: $CHECK"; exit 1; }
|
|
37
|
+
[ -f "$HOOK" ] || { echo "FAIL subject missing: $HOOK"; exit 1; }
|
|
38
|
+
|
|
39
|
+
TMPROOT=$(mktemp -d "${TMPDIR:-/tmp}/fh_close_lanes.XXXXXX")
|
|
40
|
+
trap 'rm -rf "$TMPROOT"' EXIT
|
|
41
|
+
|
|
42
|
+
_pass() { echo "✅ $1"; PASSED=$((PASSED+1)); }
|
|
43
|
+
_fail() { echo "❌ $1"; FAILED=1; }
|
|
44
|
+
|
|
45
|
+
# assert a line IS / IS NOT present in captured output
|
|
46
|
+
_line() { # $1=name $2=pattern $3=expect(0/1) $4=output
|
|
47
|
+
local hit=0
|
|
48
|
+
printf '%s\n' "$4" | grep -q -- "$2" && hit=1
|
|
49
|
+
if [ "$hit" = "$3" ]; then _pass "$1 (hit=$hit, expected=$3)"
|
|
50
|
+
else _fail "$1 — hit=$hit, expected=$3"; printf '%s\n' "$4" | sed 's/^/ │ /'; fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_rc() { # $1=name $2=actual $3=expected
|
|
54
|
+
if [ "$2" = "$3" ]; then _pass "$1 (exit=$2, expected=$3)"
|
|
55
|
+
else _fail "$1 — exit=$2, expected=$3"; fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# GAP lane: pins believed-wrong behaviour without failing the suite.
|
|
59
|
+
_gap() { # $1=name $2=observed-condition-result(0/1 as evaluated by caller) $3=note
|
|
60
|
+
if [ "$2" = "1" ]; then
|
|
61
|
+
echo "ⓘ GAP (still open) $1"
|
|
62
|
+
echo " ↳ $3"
|
|
63
|
+
GAPS=$((GAPS+1))
|
|
64
|
+
else
|
|
65
|
+
echo "🎉 GAP CLOSED — $1 : behaviour changed, promote this lane to an asserting one"
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ── fixture builders ─────────────────────────────────────────────────────────────
|
|
70
|
+
_repo() { # $1=dirname ; makes a git repo with one commit dated $2 (default now)
|
|
71
|
+
local T="$TMPROOT/$1" when="${2:-}"
|
|
72
|
+
mkdir -p "$T"
|
|
73
|
+
(
|
|
74
|
+
cd "$T" || exit 1
|
|
75
|
+
git init -q . 2>/dev/null
|
|
76
|
+
git config user.email anchor@local
|
|
77
|
+
git config user.name anchor
|
|
78
|
+
echo seed > unrelated.txt
|
|
79
|
+
# tracks/ is gitignored in the real repo too — without this the fixture's own card+log show up
|
|
80
|
+
# as untracked paths and ①'s "clean tree" lane can never be built (self-inflicted dirt).
|
|
81
|
+
printf 'tracks/\nbin/\n' > .gitignore
|
|
82
|
+
git add -A
|
|
83
|
+
if [ -n "$when" ]; then
|
|
84
|
+
GIT_AUTHOR_DATE="$when" GIT_COMMITTER_DATE="$when" git commit -qm seed
|
|
85
|
+
else
|
|
86
|
+
git commit -qm seed
|
|
87
|
+
fi
|
|
88
|
+
) >/dev/null 2>&1
|
|
89
|
+
mkdir -p "$T/tracks/_meta"
|
|
90
|
+
printf '%s' "$T"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_card() { printf '# card\n' > "$1/tracks/_meta/reference_next_session_starter.md"; }
|
|
94
|
+
# ④ + ⑤ satisfied: completion log first, card LAST (card-last ordering) — so any exit 1 in the
|
|
95
|
+
# ① / ①-b lanes is attributable to the leg under test, not to ambient ④/⑤ noise.
|
|
96
|
+
_artifacts() { printf -- '- ✅ x\n' > "$1/tracks/_meta/fh_completed_${TODAY}.md"; _card "$1"; }
|
|
97
|
+
|
|
98
|
+
_run() { # $1=repo ; sets OUT and RC (RC read directly, no pipe)
|
|
99
|
+
# HERMETIC: stub `gh` to "no open PRs" for every lane that is not specifically testing ①-b.
|
|
100
|
+
# Without this the ambient environment leaked in — the ①-P lane passed only while this operator
|
|
101
|
+
# happened to have zero open PRs, and went red the moment a PR was opened mid-session. A lane
|
|
102
|
+
# that depends on the day's PR count is measuring the environment, not the code.
|
|
103
|
+
mkdir -p "$1/bin"
|
|
104
|
+
{ printf '#!/usr/bin/env bash\necho "[]"\nexit 0\n'; } > "$1/bin/gh"
|
|
105
|
+
chmod +x "$1/bin/gh"
|
|
106
|
+
OUT=$(PATH="$1/bin:$PATH" bash "$CHECK" "$1" 2>/dev/null); RC=$?
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
echo "══ ① status snapshot ══"
|
|
110
|
+
T=$(_repo one_clean); _artifacts "$T"
|
|
111
|
+
_run "$T"
|
|
112
|
+
_line "①-P clean tree → ✅ clean line" '✅ ① working tree clean' 1 "$OUT"
|
|
113
|
+
# Pattern must not match `⚠️ ①-b` — a loose `⚠️ ①` conflated two different steps and made
|
|
114
|
+
# this absence assertion answer a question about the OPEN-PR sweep instead of the status snapshot.
|
|
115
|
+
_line "①-P clean tree → no ⚠️ ① line (paired)" '⚠️ ① ' 0 "$OUT"
|
|
116
|
+
_rc "①-P clean tree → exit 0" "$RC" 0
|
|
117
|
+
|
|
118
|
+
T=$(_repo one_dirty); _artifacts "$T"; echo scratch > "$T/uncommitted.txt"
|
|
119
|
+
_run "$T"
|
|
120
|
+
_line "①-N uncommitted path → ⚠️ fires" 'uncommitted path' 1 "$OUT"
|
|
121
|
+
_line "①-N uncommitted path → clean line absent" '✅ ① working tree clean' 0 "$OUT"
|
|
122
|
+
_rc "①-N uncommitted is ADVISORY, not blocking" "$RC" 0
|
|
123
|
+
|
|
124
|
+
# unpushed: needs a real upstream, so build a bare remote
|
|
125
|
+
T=$(_repo one_unpushed); _artifacts "$T"
|
|
126
|
+
(
|
|
127
|
+
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
|
+
echo more > second.txt && git add -A && git commit -qm second
|
|
132
|
+
) >/dev/null 2>&1
|
|
133
|
+
_run "$T"
|
|
134
|
+
_line "①-N unpushed commit → ⚠️ fires" 'unpushed commit' 1 "$OUT"
|
|
135
|
+
_line "①-N unpushed → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
136
|
+
|
|
137
|
+
T="$TMPROOT/one_notrepo"; mkdir -p "$T/tracks/_meta"; _artifacts "$T"
|
|
138
|
+
_run "$T"
|
|
139
|
+
_g=0; printf '%s\n' "$OUT" | grep -q '✅ ① working tree clean' && _g=1
|
|
140
|
+
_gap "① non-repo reports CLEAN" "$_g" \
|
|
141
|
+
"git is unavailable/not a repo → DIRTY=0, UNPUSHED=0 → the check reports '✅ working tree clean'. \
|
|
142
|
+
An instrument that could not look is not a clean result (not found ≠ 0). Should say UNSCANNED."
|
|
143
|
+
|
|
144
|
+
echo
|
|
145
|
+
echo "══ ①-b open-PR sweep ══"
|
|
146
|
+
_ghstub() { # $1=repo $2=stdout $3=exit
|
|
147
|
+
mkdir -p "$1/bin"
|
|
148
|
+
{ printf '#!/usr/bin/env bash\ncat <<'"'"'GHEOF'"'"'\n%s\nGHEOF\nexit %s\n' "$2" "$3"; } > "$1/bin/gh"
|
|
149
|
+
chmod +x "$1/bin/gh"
|
|
150
|
+
}
|
|
151
|
+
_run_with_gh() { # $1=repo
|
|
152
|
+
OUT=$(PATH="$1/bin:$PATH" bash "$CHECK" "$1" 2>/dev/null); RC=$?
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
T=$(_repo b_zero); _artifacts "$T"; _ghstub "$T" '[]' 0
|
|
156
|
+
_run_with_gh "$T"
|
|
157
|
+
_line "①-b-C no open PRs → no ①-b line (over-fire control)" '①-b' 0 "$OUT"
|
|
158
|
+
|
|
159
|
+
T=$(_repo b_one); _artifacts "$T"; _ghstub "$T" '[{"number":227}]' 0
|
|
160
|
+
_run_with_gh "$T"
|
|
161
|
+
_line "①-b-P1 one open PR → sweep line fires" '①-b 1 open PR' 1 "$OUT"
|
|
162
|
+
|
|
163
|
+
# COUNT CONSISTENCY. gh emits compact single-line JSON when piped (measured 2026-08-02:
|
|
164
|
+
# `[{"number":227},{"number":226},{"number":225}]` on ONE line), so a line-counting `grep -c`
|
|
165
|
+
# reports 1 for any non-zero number of PRs. CLAUDE.md ①-b's own origin note pairs this step with
|
|
166
|
+
# count-consistency, so a sweep that says "1 open PR" while three are open is a real defect, not
|
|
167
|
+
# a cosmetic one — the operator classifies what the sweep names.
|
|
168
|
+
T=$(_repo b_three); _artifacts "$T"
|
|
169
|
+
_ghstub "$T" '[{"number":227},{"number":226},{"number":225}]' 0
|
|
170
|
+
_run_with_gh "$T"
|
|
171
|
+
_line "①-b-P3 three open PRs → sweep says 3" '①-b 3 open PR' 1 "$OUT"
|
|
172
|
+
|
|
173
|
+
T=$(_repo b_err); _artifacts "$T"; _ghstub "$T" '' 4
|
|
174
|
+
_run_with_gh "$T"
|
|
175
|
+
_g=0; printf '%s\n' "$OUT" | grep -q '①-b' || _g=1
|
|
176
|
+
_gap "①-b gh ERROR is silent (indistinguishable from zero PRs)" "$_g" \
|
|
177
|
+
"gh present but failing (auth/offline, exit 4) → stderr discarded, count 0, NO line printed. \
|
|
178
|
+
The sweep 'not run' and the sweep 'found nothing' look identical. Advisory surface, so not \
|
|
179
|
+
fail-closed — but it should print 'sweep UNAVAILABLE' rather than nothing."
|
|
180
|
+
|
|
181
|
+
echo
|
|
182
|
+
echo "══ ④-log real-time completion log (the exit-1 FAIL path) ══"
|
|
183
|
+
# NOTE the label: this is NOT CLAUDE.md's ④ (memory hygiene, deliberately unmechanized — see
|
|
184
|
+
# CLAUDE.md §Session Wrap-up). The block was renamed 2026-08-02 because a green "④" implied memory
|
|
185
|
+
# hygiene had been verified when nothing checked it. These lanes follow the renamed label; if they
|
|
186
|
+
# ever go green against the OLD string again, the grep has stopped matching and the pair below is
|
|
187
|
+
# passing vacuously.
|
|
188
|
+
# The card is present and NEWEST in both ④ lanes on purpose: otherwise an exit 1 could be ⑤'s,
|
|
189
|
+
# and the lane would not discriminate which invariant fired (instrument-discrimination rule).
|
|
190
|
+
T=$(_repo four_missing); _card "$T" # commit today, fh_completed absent
|
|
191
|
+
_run "$T"
|
|
192
|
+
_line "④-N commits today + no fh_completed → ❌ fires" "❌ ④-log commits landed today" 1 "$OUT"
|
|
193
|
+
_line "④-N ⑤ still holds (failure attributable to ④)" '✅ ⑤ card is the newest' 1 "$OUT"
|
|
194
|
+
_rc "④-N → exit 1 (BLOCKS a close push)" "$RC" 1
|
|
195
|
+
|
|
196
|
+
T=$(_repo four_present)
|
|
197
|
+
printf -- '- ✅ something — done\n' > "$T/tracks/_meta/fh_completed_${TODAY}.md"
|
|
198
|
+
_card "$T" # card written LAST → card-last holds
|
|
199
|
+
_run "$T"
|
|
200
|
+
_line "④-P fh_completed present → no ❌ ④-log" '❌ ④-log' 0 "$OUT"
|
|
201
|
+
_line "④-P ⑤ ✅ present (known-positive)" '✅ ⑤ card is the newest' 1 "$OUT"
|
|
202
|
+
_rc "④-P → exit 0" "$RC" 0
|
|
203
|
+
|
|
204
|
+
T=$(_repo four_old "3 days ago"); _card "$T" # no commits today, no fh_completed
|
|
205
|
+
_run "$T"
|
|
206
|
+
_line "④-C no commits today → no ④ line at all (over-fire control)" '④-log' 0 "$OUT"
|
|
207
|
+
_rc "④-C → exit 0" "$RC" 0
|
|
208
|
+
|
|
209
|
+
T="$TMPROOT/four_notrepo"; mkdir -p "$T/tracks/_meta"; _card "$T"
|
|
210
|
+
_run "$T"
|
|
211
|
+
_g=0; { printf '%s\n' "$OUT" | grep -q '❌ ④' || true; } ; printf '%s\n' "$OUT" | grep -q '❌ ④' || _g=1
|
|
212
|
+
_gap "④ instrument-down degrades to PASS on a BLOCKING leg" "$_g" \
|
|
213
|
+
"No git → COMMITS_TODAY=0 → the ④ requirement silently evaporates and the check exits 0. \
|
|
214
|
+
④ is one of only two legs that can block a close push; its trigger failing open means the block \
|
|
215
|
+
is only as reliable as git being readable. Should distinguish 'no commits' from 'could not count'."
|
|
216
|
+
|
|
217
|
+
echo
|
|
218
|
+
echo "══ ④-b npm freshness + BIDIRECTIONAL entry-point drift ══"
|
|
219
|
+
_tagged() { # $1=name $2..=paths to change AFTER the tag
|
|
220
|
+
local name="$1"; shift
|
|
221
|
+
local T="$TMPROOT/$name" p
|
|
222
|
+
mkdir -p "$T"
|
|
223
|
+
(
|
|
224
|
+
cd "$T" || exit 1
|
|
225
|
+
git init -q . 2>/dev/null
|
|
226
|
+
git config user.email anchor@local && git config user.name anchor
|
|
227
|
+
echo '{"name":"fx","version":"1.0.0"}' > package.json
|
|
228
|
+
echo base > CLAUDE.md; echo base > AGENTS.md
|
|
229
|
+
mkdir -p docs knowledge/shared/harness-core knowledge/shared/learnings
|
|
230
|
+
echo base > docs/codex-compat.md
|
|
231
|
+
echo base > knowledge/shared/harness-core/x.md
|
|
232
|
+
echo base > knowledge/shared/learnings/log.yaml
|
|
233
|
+
git add -A && git commit -qm base && git tag v1.0.0
|
|
234
|
+
for p in "$@"; do mkdir -p "$(dirname "$p")"; echo changed >> "$p"; done
|
|
235
|
+
git add -A && git commit -qm change
|
|
236
|
+
) >/dev/null 2>&1
|
|
237
|
+
mkdir -p "$T/tracks/_meta"
|
|
238
|
+
printf -- '- ✅ x\n' > "$T/tracks/_meta/fh_completed_${TODAY}.md"
|
|
239
|
+
_card "$T"
|
|
240
|
+
printf '%s' "$T"
|
|
241
|
+
}
|
|
242
|
+
SHIP='④-b npm-shipped assets changed'
|
|
243
|
+
DRIFT_CC='drift candidate (CC→Codex)'
|
|
244
|
+
DRIFT_CX='drift candidate (Codex→CC)'
|
|
245
|
+
|
|
246
|
+
T=$(_tagged bb_cc CLAUDE.md); _run "$T"
|
|
247
|
+
_line "④-b-1 CLAUDE.md changed → republish reminder" "$SHIP" 1 "$OUT"
|
|
248
|
+
_line "④-b-1 → CC→Codex drift fires" "$DRIFT_CC" 1 "$OUT"
|
|
249
|
+
_line "④-b-1 → Codex→CC does NOT fire (paired)" "$DRIFT_CX" 0 "$OUT"
|
|
250
|
+
_rc "④-b-1 drift is ADVISORY (exit 0)" "$RC" 0
|
|
251
|
+
|
|
252
|
+
T=$(_tagged bb_cx AGENTS.md); _run "$T"
|
|
253
|
+
_line "④-b-2 AGENTS.md changed → republish reminder" "$SHIP" 1 "$OUT"
|
|
254
|
+
_line "④-b-2 → Codex→CC drift fires (the 07-19 miss)" "$DRIFT_CX" 1 "$OUT"
|
|
255
|
+
_line "④-b-2 → CC→Codex does NOT fire (paired)" "$DRIFT_CC" 0 "$OUT"
|
|
256
|
+
|
|
257
|
+
T=$(_tagged bb_cx2 docs/codex-compat.md); _run "$T"
|
|
258
|
+
_line "④-b-3 docs/codex-compat.md → Codex→CC fires" "$DRIFT_CX" 1 "$OUT"
|
|
259
|
+
_line "④-b-3 → CC→Codex does NOT fire (paired)" "$DRIFT_CC" 0 "$OUT"
|
|
260
|
+
|
|
261
|
+
T=$(_tagged bb_kn knowledge/shared/harness-core/x.md); _run "$T"
|
|
262
|
+
_line "④-b-4 shipped knowledge/ counts as a CC entry point" "$DRIFT_CC" 1 "$OUT"
|
|
263
|
+
|
|
264
|
+
T=$(_tagged bb_both CLAUDE.md AGENTS.md); _run "$T"
|
|
265
|
+
_line "④-b-5 both sides changed → republish reminder still" "$SHIP" 1 "$OUT"
|
|
266
|
+
_line "④-b-5 both sides → CC→Codex silent (over-fire ctrl)" "$DRIFT_CC" 0 "$OUT"
|
|
267
|
+
_line "④-b-5 both sides → Codex→CC silent (over-fire ctrl)" "$DRIFT_CX" 0 "$OUT"
|
|
268
|
+
|
|
269
|
+
T=$(_tagged bb_unshipped knowledge/shared/learnings/log.yaml); _run "$T"
|
|
270
|
+
_line "④-b-6 UNshipped path → no republish reminder" "$SHIP" 0 "$OUT"
|
|
271
|
+
_line "④-b-6 UNshipped path → no drift candidate" "$DRIFT_CC" 0 "$OUT"
|
|
272
|
+
_line "④-b-6 UNshipped path → ④-b is not silent-broken (control below)" '④-b' 0 "$OUT"
|
|
273
|
+
|
|
274
|
+
# no-tag fixture: identical CLAUDE.md change, but no version tag exists
|
|
275
|
+
T="$TMPROOT/bb_notag"; mkdir -p "$T"
|
|
276
|
+
(
|
|
277
|
+
cd "$T" || exit 1
|
|
278
|
+
git init -q . 2>/dev/null
|
|
279
|
+
git config user.email anchor@local && git config user.name anchor
|
|
280
|
+
echo '{"name":"fx","version":"1.0.0"}' > package.json
|
|
281
|
+
echo base > CLAUDE.md && git add -A && git commit -qm base
|
|
282
|
+
echo changed >> CLAUDE.md && git add -A && git commit -qm change
|
|
283
|
+
) >/dev/null 2>&1
|
|
284
|
+
mkdir -p "$T/tracks/_meta"; printf -- '- ✅ x\n' > "$T/tracks/_meta/fh_completed_${TODAY}.md"; _card "$T"
|
|
285
|
+
_run "$T"
|
|
286
|
+
_g=0; printf '%s\n' "$OUT" | grep -q '④-b' || _g=1
|
|
287
|
+
_gap "④-b whole step vanishes when no version tag is reachable" "$_g" \
|
|
288
|
+
"LAST_TAG empty → the republish reminder AND both drift candidates are skipped silently. \
|
|
289
|
+
CLAUDE.md ④-b conditions the drift check on 'npm-shipped assets changed', not on a tag existing; \
|
|
290
|
+
a shallow/tagless clone or a pre-first-release state therefore runs a close with the entry-point \
|
|
291
|
+
drift check simply absent, and nothing says so."
|
|
292
|
+
|
|
293
|
+
echo
|
|
294
|
+
echo "══ pre-push SURFACE MATCHING (advise vs block) ══"
|
|
295
|
+
# A stubbed session_close_check lets these lanes control _SC_RC exactly, which is the variable
|
|
296
|
+
# under test. The real script's verdict logic is anchored above; what is anchored HERE is that
|
|
297
|
+
# the hook routes verdict → advisory or block by surface, and nothing else.
|
|
298
|
+
_hookfx() { # $1=name $2=stub exit code → echoes repo path
|
|
299
|
+
local T="$TMPROOT/$1"
|
|
300
|
+
mkdir -p "$T/scripts"
|
|
301
|
+
(
|
|
302
|
+
cd "$T" || exit 1
|
|
303
|
+
git init -q . 2>/dev/null
|
|
304
|
+
git config user.email anchor@local && git config user.name anchor
|
|
305
|
+
printf '#!/usr/bin/env bash\necho "❌ ⑤ card-last violated — synthetic"\nexit %s\n' "$2" \
|
|
306
|
+
> scripts/session_close_check.sh
|
|
307
|
+
chmod +x scripts/session_close_check.sh
|
|
308
|
+
git add -A && git commit -qm seed
|
|
309
|
+
) >/dev/null 2>&1
|
|
310
|
+
cp "$HOOK" "$T/pre-push-under-test"
|
|
311
|
+
printf '%s' "$T"
|
|
312
|
+
}
|
|
313
|
+
_hookrun() { # $1=repo $2=FH_SESSION_CLOSE value ("" = unset)
|
|
314
|
+
local T="$1" v="$2" sha
|
|
315
|
+
sha=$(git -C "$T" rev-parse HEAD)
|
|
316
|
+
# ordinary NEW-BRANCH push: local_sha real, remote_sha zero → not a delete, not a force,
|
|
317
|
+
# not the integration branch → the hook must fall through to exit 0 unless a leg blocks.
|
|
318
|
+
OUT=$(cd "$T" && printf 'refs/heads/feat/x %s refs/heads/feat/x %s\n' "$sha" "0000000000000000000000000000000000000000" \
|
|
319
|
+
| env ${v:+FH_SESSION_CLOSE=$v} bash ./pre-push-under-test origin https://example.invalid/x.git 2>&1)
|
|
320
|
+
RC=$? # command substitution: this is the pipeline's status under pipefail, i.e. the HOOK's
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
T=$(_hookfx hook_adv 1); _hookrun "$T" ""
|
|
324
|
+
_rc "PP-1 ordinary push + violation → exit 0 (ADVISORY)" "$RC" 0
|
|
325
|
+
_line "PP-1 → the ❌ line is surfaced, not swallowed" '❌ ⑤ card-last violated' 1 "$OUT"
|
|
326
|
+
_line "PP-1 → says it is advisory + names the enforcing form" 'FH_SESSION_CLOSE=1' 1 "$OUT"
|
|
327
|
+
|
|
328
|
+
T=$(_hookfx hook_block 1); _hookrun "$T" 1
|
|
329
|
+
_rc "PP-2 close push + violation → exit 1 (BLOCKS)" "$RC" 1
|
|
330
|
+
_line "PP-2 → prints the enforcing banner" '⛔ FH Session-Close Check' 1 "$OUT"
|
|
331
|
+
|
|
332
|
+
T=$(_hookfx hook_ok 0); _hookrun "$T" 1
|
|
333
|
+
_rc "PP-3 close push + CLEAN → exit 0 (blocks on the VERDICT, not on the flag)" "$RC" 0
|
|
334
|
+
_line "PP-3 → prints the consistent line" '✅ FH Session-Close Check' 1 "$OUT"
|
|
335
|
+
|
|
336
|
+
T=$(_hookfx hook_ok2 0); _hookrun "$T" ""
|
|
337
|
+
_rc "PP-4 ordinary push + CLEAN → exit 0" "$RC" 0
|
|
338
|
+
_line "PP-4 → no advisory noise on a healthy push" 'close invariant(s) violated' 0 "$OUT"
|
|
339
|
+
|
|
340
|
+
echo
|
|
341
|
+
echo "──────────────────────────────────────────────"
|
|
342
|
+
if [ "$FAILED" -ne 0 ]; then
|
|
343
|
+
echo "CLOSE-CHAIN LANES: FAIL — the close-chain instrument is miscalibrated (do not trust its verdict)"
|
|
344
|
+
echo " asserting lanes passed: $PASSED · open gaps: $GAPS"
|
|
345
|
+
exit 1
|
|
346
|
+
fi
|
|
347
|
+
echo "CLOSE-CHAIN LANES: PASS ($PASSED asserting lanes) · $GAPS KNOWN GAP(S) still open (see ⓘ above)"
|
|
348
|
+
echo " A green line here covers ① ①-b ④ ④-b and the pre-push advise/block split — NOT the gaps."
|
|
349
|
+
exit 0
|