@chrono-meta/fh-gate 1.4.86 → 1.4.88
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 +128 -0
- package/knowledge/shared/rules/auto_project_mapping.md +64 -1
- package/knowledge/shared/rules/operations.md +26 -0
- package/package.json +6 -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/skills/prompt-regression/SKILL.md +1 -1
- package/scripts/degrade_direction_scan.sh +34 -2
- package/scripts/gate_pathspec_check.sh +91 -1
- package/scripts/package_coverage_check.sh +27 -0
- package/scripts/selfcheck.sh +112 -1
- package/scripts/session_close_check.sh +18 -4
- package/scripts/test_degrade_scan_shell_probes.sh +11 -2
- package/scripts/test_dispatch_log_lanes.sh +41 -1
- package/scripts/test_node_infra_delta_lanes.sh +211 -0
- package/scripts/test_session_close_chain_lanes.sh +349 -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/degrade_direction_scan.sh +34 -2
- package/templates/regression_guard.sh +25 -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
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_wizard_snippet_merge_lanes.sh — lanes for the SessionStart snippet → settings.json merge.
|
|
3
|
+
#
|
|
4
|
+
# SUBJECT: the python block in plugins/fh-meta/skills/install-wizard/SKILL_detail.md
|
|
5
|
+
# §Step4-Baseline-Bash that reads templates/settings.SessionStart.snippet.json and merges its
|
|
6
|
+
# `project_settings_json` entry into the user's .claude/settings.json.
|
|
7
|
+
#
|
|
8
|
+
# The subject is EXTRACTED FROM THE SHIPPED FILE at run time, never retyped. A retyped copy is the
|
|
9
|
+
# divergent-copy class this repo has already paid for: the lane would keep validating the OLD merge
|
|
10
|
+
# while the shipped one drifted, and stay green throughout. Extraction failure is a FAIL, not a skip.
|
|
11
|
+
#
|
|
12
|
+
# WHY THIS MATTERS MORE THAN AN ORDINARY PARSER LANE: the hook being registered here is
|
|
13
|
+
# fh_node_check.sh — the one thing that tells an operator at turn 0 that their machine's floors are
|
|
14
|
+
# missing. If the registration silently does not happen, the detector that would have said so is the
|
|
15
|
+
# detector that did not get installed. That loop has to be broken from outside, and the lanes below
|
|
16
|
+
# measure whether anything does.
|
|
17
|
+
#
|
|
18
|
+
# CALIBRATION: every absence lane names its known-positive and builds it in the same run.
|
|
19
|
+
# ⓘ GAP lanes pin behaviour believed wrong without failing the suite.
|
|
20
|
+
# Exit 0 = calibrated · 1 = the instrument (or the extraction) is wrong.
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
25
|
+
WIZ="$ROOT/plugins/fh-meta/skills/install-wizard/SKILL_detail.md"
|
|
26
|
+
DOC="$ROOT/plugins/fh-meta/skills/install-doctor/SKILL.md"
|
|
27
|
+
SNIPPET="$ROOT/templates/settings.SessionStart.snippet.json"
|
|
28
|
+
|
|
29
|
+
TMPROOT=$(mktemp -d "${TMPDIR:-/tmp}/fh_snippet_lanes.XXXXXX")
|
|
30
|
+
trap 'rm -rf "$TMPROOT"' EXIT
|
|
31
|
+
FAILED=0; PASSED=0; GAPS=0
|
|
32
|
+
_pass() { echo "✅ $1"; PASSED=$((PASSED+1)); }
|
|
33
|
+
_fail() { echo "❌ $1"; FAILED=1; }
|
|
34
|
+
_rc() { if [ "$2" = "$3" ]; then _pass "$1 (exit=$2, expected=$3)"; else _fail "$1 — exit=$2, expected=$3"; fi; }
|
|
35
|
+
_eq() { if [ "$2" = "$3" ]; then _pass "$1 ($2)"; else _fail "$1 — got [$2], expected [$3]"; fi; }
|
|
36
|
+
_gap() { if [ "$2" = "1" ]; then echo "ⓘ GAP (still open) $1"; echo " ↳ $3"; GAPS=$((GAPS+1));
|
|
37
|
+
else echo "🎉 GAP CLOSED — $1 : behaviour changed, promote this lane"; fi; }
|
|
38
|
+
|
|
39
|
+
for f in "$WIZ" "$DOC" "$SNIPPET"; do
|
|
40
|
+
[ -f "$f" ] || { echo "❌ subject missing: $f"; exit 1; }
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
echo "══ extraction (the instrument itself) ══"
|
|
44
|
+
MERGE="$TMPROOT/merge.py"
|
|
45
|
+
awk '/^python3 - "\$FH_DIR" <<.PY.$/{f=1;next} f&&/^PY$/{exit} f' "$WIZ" > "$MERGE"
|
|
46
|
+
N=$(wc -l < "$MERGE" | tr -d ' ')
|
|
47
|
+
if [ "$N" -lt 10 ]; then
|
|
48
|
+
_fail "EX-1 could not extract the merge block from SKILL_detail.md (got $N lines) — NOT a pass"
|
|
49
|
+
echo "──────────────────────────────────────────────"; echo "SNIPPET MERGE LANES: FAIL (extraction)"; exit 1
|
|
50
|
+
fi
|
|
51
|
+
_pass "EX-1 merge block extracted from the shipped SKILL_detail.md ($N lines)"
|
|
52
|
+
grep -q 'project_settings_json' "$MERGE" && _pass "EX-2 extracted block reads project_settings_json" \
|
|
53
|
+
|| _fail "EX-2 extracted block does not mention project_settings_json — wrong region captured"
|
|
54
|
+
|
|
55
|
+
DOCPY="$TMPROOT/doctor.py"
|
|
56
|
+
# NOTE the leading-whitespace tolerance: this block is indented inside an `if` in the SKILL, and a
|
|
57
|
+
# ^-anchored pattern extracted 0 lines. That failure surfaced as a red lane rather than a green
|
|
58
|
+
# vacuous one only because EX-3 treats an empty extraction as FAIL — keep it that way.
|
|
59
|
+
awk '/^[[:space:]]*python3 - "\$FH" <<.PY.$/{f=1;next} f&&/^[[:space:]]*PY$/{exit} f' "$DOC" > "$DOCPY"
|
|
60
|
+
DN=$(wc -l < "$DOCPY" | tr -d ' ')
|
|
61
|
+
[ "$DN" -ge 5 ] && _pass "EX-3 install-doctor registration check extracted ($DN lines)" \
|
|
62
|
+
|| _fail "EX-3 could not extract install-doctor's registration check (got $DN lines) — NOT a pass"
|
|
63
|
+
|
|
64
|
+
# ── fixture ─────────────────────────────────────────────────────────────────────
|
|
65
|
+
_hub() { # $1=name $2=snippet-content ("@SHIPPED" = the real tracked file) → echoes path
|
|
66
|
+
local h="$TMPROOT/$1"
|
|
67
|
+
mkdir -p "$h/templates" "$h/.claude"
|
|
68
|
+
if [ "$2" = "@SHIPPED" ]; then cp "$SNIPPET" "$h/templates/settings.SessionStart.snippet.json"
|
|
69
|
+
else printf '%s' "$2" > "$h/templates/settings.SessionStart.snippet.json"; fi
|
|
70
|
+
printf '%s' "$h"
|
|
71
|
+
}
|
|
72
|
+
_merge() { OUT=$(python3 "$MERGE" "$1" 2>&1); RC=$?; } # RC read directly off python, no pipe
|
|
73
|
+
_registered() { # 1 = fh_node_check present in the written settings.json
|
|
74
|
+
[ -f "$1/.claude/settings.json" ] && grep -q 'fh_node_check' "$1/.claude/settings.json" && echo 1 || echo 0
|
|
75
|
+
}
|
|
76
|
+
_claims_ok() { printf '%s\n' "$OUT" | grep -q 'hook registered' && echo 1 || echo 0; }
|
|
77
|
+
|
|
78
|
+
echo
|
|
79
|
+
echo "══ the happy path (known-positive for every absence lane below) ══"
|
|
80
|
+
H=$(_hub good @SHIPPED); _merge "$H"
|
|
81
|
+
_rc "MG-P shipped snippet → exit 0" "$RC" 0
|
|
82
|
+
_eq "MG-P → fh_node_check actually present in settings.json" "$(_registered "$H")" 1
|
|
83
|
+
_eq "MG-P → success line printed" "$(_claims_ok)" 1
|
|
84
|
+
|
|
85
|
+
# the documented merge promise (SKILL_detail.md: 'Merge at HOOK level, not group level') —
|
|
86
|
+
# a foreign hook sharing a group with the FH one must survive.
|
|
87
|
+
H=$(_hub coexist @SHIPPED)
|
|
88
|
+
cat > "$H/.claude/settings.json" <<'J'
|
|
89
|
+
{"hooks":{"SessionStart":[{"matcher":"","hooks":[
|
|
90
|
+
{"type":"command","command":"bash my_telemetry.sh"},
|
|
91
|
+
{"type":"command","command":"bash $CLAUDE_PROJECT_DIR/scripts/fh_node_check.sh"}]}]}}
|
|
92
|
+
J
|
|
93
|
+
_merge "$H"
|
|
94
|
+
grep -q 'my_telemetry' "$H/.claude/settings.json" && _pass "MG-P2 foreign hook in the FH group survives the merge" \
|
|
95
|
+
|| _fail "MG-P2 foreign hook was dropped — the documented hook-level merge is not happening"
|
|
96
|
+
_eq "MG-P2 → and fh_node_check is (re)registered" "$(_registered "$H")" 1
|
|
97
|
+
DUP=$(grep -c 'fh_node_check' "$H/.claude/settings.json")
|
|
98
|
+
_eq "MG-P2 → old FH entry replaced, not duplicated" "$DUP" 1
|
|
99
|
+
|
|
100
|
+
echo
|
|
101
|
+
echo "══ malformed snippet: fails loudly, writes nothing ══"
|
|
102
|
+
# These four are the F10 question as posed (malformed / truncated / empty). The merge turns out to
|
|
103
|
+
# be fail-CLOSED here — worth pinning precisely so a future 'robustness' refactor that swallows the
|
|
104
|
+
# exception is caught as the regression it would be.
|
|
105
|
+
SHIPPED_TXT=$(cat "$SNIPPET")
|
|
106
|
+
i=0
|
|
107
|
+
for name in empty notjson truncated missingkey; do
|
|
108
|
+
i=$((i+1))
|
|
109
|
+
case "$name" in
|
|
110
|
+
empty) body="" ;;
|
|
111
|
+
notjson) body="oops not json at all" ;;
|
|
112
|
+
truncated) body=$(printf '%s' "$SHIPPED_TXT" | head -c 400) ;;
|
|
113
|
+
missingkey) body='{"hooks":{"SessionStart":[]}}' ;;
|
|
114
|
+
esac
|
|
115
|
+
H=$(_hub "bad_$name" "$body"); _merge "$H"
|
|
116
|
+
_rc "MG-N$i $name snippet → non-zero exit" "$RC" 1
|
|
117
|
+
_eq "MG-N$i $name → nothing registered (paired with MG-P)" "$(_registered "$H")" 0
|
|
118
|
+
_eq "MG-N$i $name → no success line" "$(_claims_ok)" 0
|
|
119
|
+
done
|
|
120
|
+
|
|
121
|
+
echo
|
|
122
|
+
echo "══ malformed EXISTING settings.json: the user's file is not destroyed ══"
|
|
123
|
+
H=$(_hub badtarget @SHIPPED); printf '{ oops' > "$H/.claude/settings.json"
|
|
124
|
+
_merge "$H"
|
|
125
|
+
_rc "MG-T unparsable target → non-zero exit" "$RC" 1
|
|
126
|
+
_eq "MG-T → the user's file is left byte-identical" "$(cat "$H/.claude/settings.json")" '{ oops'
|
|
127
|
+
[ -f "$H/.claude/settings.json.prewizard" ] \
|
|
128
|
+
&& _fail "MG-T → a .prewizard backup of an unread file was written (misleading artifact)" \
|
|
129
|
+
|| _pass "MG-T → no misleading backup artifact left behind"
|
|
130
|
+
|
|
131
|
+
echo
|
|
132
|
+
echo "══ ⓘ GAP lanes ══"
|
|
133
|
+
|
|
134
|
+
# GAP 1 — the ONLY validation is what `kept + entry` incidentally requires: that entry is a list.
|
|
135
|
+
# Any list-shaped value is written to the user's real settings.json and reported as success. The
|
|
136
|
+
# success message is not conditioned on what was actually written.
|
|
137
|
+
BADSCHEMA_HITS=0; BADSCHEMA_TOTAL=0; DETAIL=""
|
|
138
|
+
for name in emptylist nocommand wrongscript juststring; do
|
|
139
|
+
case "$name" in
|
|
140
|
+
emptylist) body='{"project_settings_json":{"hooks":{"SessionStart":[]}}}' ;;
|
|
141
|
+
nocommand) body='{"project_settings_json":{"hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command"}]}]}}}' ;;
|
|
142
|
+
wrongscript) body='{"project_settings_json":{"hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"bash /some/other/script.sh"}]}]}}}' ;;
|
|
143
|
+
juststring) body='{"project_settings_json":{"hooks":{"SessionStart":["not even an object"]}}}' ;;
|
|
144
|
+
esac
|
|
145
|
+
H=$(_hub "schema_$name" "$body"); _merge "$H"
|
|
146
|
+
BADSCHEMA_TOTAL=$((BADSCHEMA_TOTAL+1))
|
|
147
|
+
if [ "$RC" = 0 ] && [ "$(_claims_ok)" = 1 ] && [ "$(_registered "$H")" = 0 ]; then
|
|
148
|
+
BADSCHEMA_HITS=$((BADSCHEMA_HITS+1)); DETAIL="$DETAIL $name"
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
151
|
+
_g=0; [ "$BADSCHEMA_HITS" -gt 0 ] && _g=1
|
|
152
|
+
_gap "valid-JSON / invalid-SCHEMA snippet → written, reported as success, node hook absent" "$_g" \
|
|
153
|
+
"$BADSCHEMA_HITS of $BADSCHEMA_TOTAL cases:$DETAIL. exit 0 and 'node-check SessionStart hook \
|
|
154
|
+
registered ->' are printed while fh_node_check is NOT in the file that was just written. The success \
|
|
155
|
+
message is unconditional — it does not re-read what it wrote. One-line fix: after the write, assert \
|
|
156
|
+
fh_node_check appears in the serialized result, else exit non-zero."
|
|
157
|
+
|
|
158
|
+
# GAP 2 — the corruption from GAP 1 is LATENT: the bad value lands in the user's settings.json and
|
|
159
|
+
# detonates on the NEXT wizard run, in the kept-loop, far from where it was introduced.
|
|
160
|
+
H=$(_hub latent '{"project_settings_json":{"hooks":{"SessionStart":["not even an object"]}}}')
|
|
161
|
+
_merge "$H"; FIRST_RC=$RC
|
|
162
|
+
_merge "$H"; SECOND_RC=$RC
|
|
163
|
+
_g=0; { [ "$FIRST_RC" = 0 ] && [ "$SECOND_RC" != 0 ]; } && _g=1
|
|
164
|
+
_gap "the bad write detonates on the NEXT run, not the run that made it" "$_g" \
|
|
165
|
+
"run1 exit=$FIRST_RC (reported success), run2 exit=$SECOND_RC (AttributeError in the kept-loop: \
|
|
166
|
+
g.get on a str). The user's settings.json is now un-mergeable and the traceback points at the \
|
|
167
|
+
survivor filter, not at the snippet that caused it."
|
|
168
|
+
|
|
169
|
+
# GAP 3 — INSTRUMENT COVERAGE, measured not asserted. Build the exact post-failure state (companion
|
|
170
|
+
# hook registered, node hook absent) and run install-doctor's registration check on it. The
|
|
171
|
+
# known-positive is in the same run: the companion line must fire, proving the check executed.
|
|
172
|
+
H="$TMPROOT/doctor_state"; mkdir -p "$H/.claude"
|
|
173
|
+
cat > "$H/.claude/settings.local.json" <<'J'
|
|
174
|
+
{"hooks":{"SessionStart":[{"matcher":"","hooks":[
|
|
175
|
+
{"type":"command","command":"bash $CLAUDE_PROJECT_DIR/scripts/fh_session_load.sh"}]}]}}
|
|
176
|
+
J
|
|
177
|
+
printf '{}' > "$H/.claude/settings.json"
|
|
178
|
+
DOUT=$(python3 "$DOCPY" "$H" 2>&1); DRC=$?
|
|
179
|
+
if printf '%s\n' "$DOUT" | grep -q 'companion-load SessionStart registered'; then
|
|
180
|
+
_pass "DR-P install-doctor's check RAN on this fixture (known-positive: companion line fired, exit=$DRC)"
|
|
181
|
+
else
|
|
182
|
+
_fail "DR-P install-doctor's check did not fire at all — the GAP lane below would pass vacuously"
|
|
183
|
+
fi
|
|
184
|
+
_g=0; printf '%s\n' "$DOUT" | grep -qi 'node_check\|node check' || _g=1
|
|
185
|
+
_gap "nothing downstream verifies that fh_node_check is registered" "$_g" \
|
|
186
|
+
"The node hook is absent from this fixture and install-doctor's registration check says nothing \
|
|
187
|
+
about it — it only greps for fh_session_load. install-doctor's ① checks the git hook FILES, not the \
|
|
188
|
+
SessionStart REGISTRATION of the script that reports on them. So the wizard's unconditional success \
|
|
189
|
+
message (GAP 1) is never contradicted by anything, and the check whose job is announcing an unwired \
|
|
190
|
+
machine can itself be unwired silently."
|
|
191
|
+
|
|
192
|
+
echo
|
|
193
|
+
echo "──────────────────────────────────────────────"
|
|
194
|
+
if [ "$FAILED" -ne 0 ]; then
|
|
195
|
+
echo "SNIPPET MERGE LANES: FAIL — instrument miscalibrated (do not trust its verdict)"
|
|
196
|
+
echo " asserting lanes passed: $PASSED · open gaps: $GAPS"
|
|
197
|
+
exit 1
|
|
198
|
+
fi
|
|
199
|
+
echo "SNIPPET MERGE LANES: PASS ($PASSED asserting lanes) · $GAPS KNOWN GAP(S) open (see ⓘ above)"
|
|
200
|
+
echo " Green covers: happy path, hook-level merge promise, malformed-snippet fail-closed,"
|
|
201
|
+
echo " unparsable-target preservation. Green does NOT cover the three gaps above."
|
|
202
|
+
exit 0
|
|
@@ -79,7 +79,10 @@ EOF
|
|
|
79
79
|
|
|
80
80
|
### Automatic Response to Issues
|
|
81
81
|
|
|
82
|
-
<!-- [CUSTOMIZE]
|
|
82
|
+
<!-- [CUSTOMIZE] DOMAIN-SCOPED — delete this whole section if the project has no test-report
|
|
83
|
+
artifact to analyse. The old wording here said "adjust report tool/path", which assumes the
|
|
84
|
+
tool exists and reads as a config note rather than a delete instruction; the same measurement
|
|
85
|
+
that found the two sections below inert found this one inert for the same reason. -->
|
|
83
86
|
|
|
84
87
|
#### Automatic Check Trigger
|
|
85
88
|
|
|
@@ -98,9 +101,27 @@ When the user mentions a problem, **automatically** locate and analyze the lates
|
|
|
98
101
|
|
|
99
102
|
### Code Writing Principles
|
|
100
103
|
|
|
101
|
-
<!-- [CUSTOMIZE] Adjust to match your project's coding conventions.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
<!-- [CUSTOMIZE] Adjust to match your project's coding conventions.
|
|
105
|
+
THREE of the five below are universal (#1 #2 #5). TWO are DOMAIN-SCOPED and belong only to
|
|
106
|
+
UI/mobile-QA projects (#3 locator stability, #4 flakiness) — DELETE them outright if this
|
|
107
|
+
project has no UI automation. Do not keep them "just in case": measured 2026-08-03 across
|
|
108
|
+
THREE repos that inherited this file. One (a bash/python wiki engine) carried both for its
|
|
109
|
+
entire 11-day life before being pruned; the other two are byte-identical to this template
|
|
110
|
+
right now and still carry them.
|
|
111
|
+
If you delete a section, also fix any sentence that counts them — the wiki engine shipped
|
|
112
|
+
"all 5 principles" over three for an hour before an outside reviewer caught it.
|
|
113
|
+
What the fix here does and does NOT do, measured as a before/after pair (Sonnet, reps=3).
|
|
114
|
+
The gain depends on how explicitly the asker names what the project lacks:
|
|
115
|
+
* question naming no-UI/no-mobile only -> before: #3 3/3, #4 **1/3**; after: both 3/3
|
|
116
|
+
* question also naming no-test-reports -> before: 8 of 9 cells; after: 9 of 9
|
|
117
|
+
So the honest claim is narrow: the old wording is mostly adequate for someone who spells out
|
|
118
|
+
every absence, and loses a section for someone who does not. Both numbers are recorded because
|
|
119
|
+
citing only the first would overstate this edit.
|
|
120
|
+
THE FIELD CAUSE IS STILL OPEN, and it is a different moment: the wiki engine did not prune
|
|
121
|
+
either section, which means nobody was asked. `light-harness init` copies this file whole,
|
|
122
|
+
with no pruning step. A marker only works on a reader who has stopped to read it. -->
|
|
123
|
+
|
|
124
|
+
Be conscious of every principle below **before** writing code — directly reduces back-and-forth where Claude rushes to create something and the user has to correct it.
|
|
104
125
|
|
|
105
126
|
#### 1. Reference Existing Code (Consistency First)
|
|
106
127
|
|
|
@@ -116,7 +137,7 @@ Be conscious of all 5 principles **before** writing code — directly reduces ba
|
|
|
116
137
|
|
|
117
138
|
#### 3. Locator and Identifier Stability (UI code only)
|
|
118
139
|
|
|
119
|
-
<!-- [CUSTOMIZE]
|
|
140
|
+
<!-- [CUSTOMIZE] DOMAIN-SCOPED — delete this whole section for non-UI projects (see the note above #1) -->
|
|
120
141
|
|
|
121
142
|
- Do not depend on dynamically generated attributes (auto-generated id, timestamps in content-desc)
|
|
122
143
|
- Avoid absolute XPath — fragile to structural changes
|
|
@@ -125,6 +146,8 @@ Be conscious of all 5 principles **before** writing code — directly reduces ba
|
|
|
125
146
|
|
|
126
147
|
#### 4. Flakiness Risk Management
|
|
127
148
|
|
|
149
|
+
<!-- [CUSTOMIZE] DOMAIN-SCOPED — delete this whole section for projects without UI automation -->
|
|
150
|
+
|
|
128
151
|
- **No `time.sleep`** — use explicit waits (implicit/explicit wait) + condition-based polling
|
|
129
152
|
- No unbounded waits without a timeout
|
|
130
153
|
- Allow tolerance in screenshot-based assertions
|
|
@@ -706,8 +706,15 @@ run_universal_guards
|
|
|
706
706
|
# not a reversible-surface concern — it disables the commit gate itself.
|
|
707
707
|
GATE_IMPL=$(echo "$STAGED" \
|
|
708
708
|
| grep -E '(templates/\.git-hooks/pre-commit|templates/regression_guard\.sh|\.claude/rules/fh_4axis_gate\.md|scripts/gate_pathspec_check\.sh)' || true)
|
|
709
|
-
|
|
710
|
-
|
|
709
|
+
# CLAUDE.md carries a canonical asset list that gate_pathspec_check §5 verifies, so editing it must
|
|
710
|
+
# RUN the anchor. It is deliberately NOT added to GATE_IMPL: that variable also feeds $LOADBEARING,
|
|
711
|
+
# which demands a cross-family acknowledgment line in the marker. Enrolling every CLAUDE.md prose
|
|
712
|
+
# edit into cross-family review is a different job from path coverage, and over-blocking is how an
|
|
713
|
+
# override becomes muscle memory (CLAUDE.md §Destructive-Op — the same reasoning that keeps the
|
|
714
|
+
# session-close check advisory on ordinary pushes). Separate trigger, same anchor.
|
|
715
|
+
ASSETLIST_IMPL=$(echo "$STAGED" | grep -E '^CLAUDE\.md$' || true)
|
|
716
|
+
if [ -n "$GATE_IMPL" ] || [ -n "$ASSETLIST_IMPL" ]; then
|
|
717
|
+
echo "[Gate] gate implementation or asset list staged — path-coverage known-pair anchor..."
|
|
711
718
|
PSCHECK="$REPO_ROOT/scripts/gate_pathspec_check.sh"
|
|
712
719
|
if [ ! -f "$PSCHECK" ]; then
|
|
713
720
|
echo " ❌ FAIL — scripts/gate_pathspec_check.sh missing while a gate file is being changed."
|