@chrono-meta/fh-gate 1.4.83 → 1.4.85
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 +45 -1
- package/.claude-plugin/marketplace.json +2 -2
- package/CATALOG.md +23 -1
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +21 -7
- package/knowledge/shared/harness-core/field_harness_diagnostic.md +1 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +10 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +167 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +206 -0
- package/knowledge/shared/rules/operational_adaptation.md +35 -9
- package/package.json +3 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/convergence-loop/SKILL.md +25 -10
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/agents/challenger.md +1 -1
- package/plugins/fh-meta/skills/phantom-quench/SKILL.md +1 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +78 -10
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +3 -3
- package/scripts/consent_registry_check.sh +187 -15
- package/scripts/destructive_pre_gate.sh +32 -5
- package/scripts/package_coverage_check.sh +10 -0
- package/scripts/pipe_verdict_guard.sh +43 -3
- package/scripts/selfcheck.sh +117 -2
- package/scripts/session_close_check.sh +103 -1
- package/scripts/stale_clone_guard.sh +49 -6
- package/scripts/substrate_jump_detector.sh +21 -0
- package/scripts/test_consent_registry.sh +341 -42
- package/scripts/test_destructive_pre_gate_lanes.sh +42 -0
- package/scripts/test_dispatch_log_lanes.sh +99 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +71 -0
- package/scripts/test_selfcheck_state_lanes.sh +102 -0
- package/scripts/test_session_close_lanes.sh +187 -15
- package/scripts/test_stale_clone_guard_lanes.sh +54 -0
- package/templates/.git-hooks/pre-push +10 -1
- package/templates/consent_classes.yaml.example +18 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_dispatch_log_lanes.sh — known-pair anchor for session_close_check.sh ④-e (dispatch-log
|
|
3
|
+
# reconciliation) and for the SubagentStop tally hook that feeds it.
|
|
4
|
+
#
|
|
5
|
+
# WHY (2026-08-02): CLAUDE.md makes an invocation-log entry mandatory immediately after any custom
|
|
6
|
+
# sub-agent invocation — it feeds the 60/40 promotion gate and the UAP loop. Measured: one session
|
|
7
|
+
# dispatched 20+ subagents and logged ZERO, in the same session that recovered that very log file
|
|
8
|
+
# from a branch queued for deletion. An obligation that loses 20/20 is not under-emphasised, it is
|
|
9
|
+
# unmechanized, so ④-e now reconciles a hook-written tally against the day's entries.
|
|
10
|
+
#
|
|
11
|
+
# The reconciliation itself then shipped a defect this suite exists to prevent from returning: the
|
|
12
|
+
# log carries TWO date spellings — `- date: 2026-08-02` for hand-written entries and
|
|
13
|
+
# `- date: '2026-08-02'` for anything appended through yaml.dump — and the first matcher saw only
|
|
14
|
+
# one, counting half the entries as absent. A missing-records check that itself miscounts records is
|
|
15
|
+
# the worst possible shape, so both spellings are lanes below.
|
|
16
|
+
#
|
|
17
|
+
# Exit 0 = the reconciliation discriminates · 1 = it would mis-report.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
CHECK="$SCRIPT_DIR/session_close_check.sh"
|
|
22
|
+
SETTINGS="$SCRIPT_DIR/../.claude/settings.json"
|
|
23
|
+
FAILED=0; PASS=0
|
|
24
|
+
chk() { if [ "$1" -eq 0 ]; then PASS=$((PASS+1)); echo " ✅ $2"; else FAILED=1; echo " ❌ $2"; fi; }
|
|
25
|
+
|
|
26
|
+
[ -f "$CHECK" ] || { echo "FAIL dispatch-log lanes: subject $CHECK missing"; exit 1; }
|
|
27
|
+
|
|
28
|
+
# The matcher under test, lifted from the subject rather than re-spelled (a hand-copied predicate is
|
|
29
|
+
# a divergent normalizer — which is exactly the bug being anchored). If it is gone, fail loudly.
|
|
30
|
+
MATCHER=$(grep -oE "grep -cE \"\^- date: \*'\?\\\$TODAY'\?\"" "$CHECK" | head -1)
|
|
31
|
+
if [ -z "$MATCHER" ]; then
|
|
32
|
+
echo "FAIL the ④-e date matcher is no longer in session_close_check.sh in the expected form —"
|
|
33
|
+
echo " this lane cannot verify what it claims to. Update the lane WITH the subject."
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
echo "── matcher located in the subject"
|
|
37
|
+
|
|
38
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
39
|
+
D=2026-08-02
|
|
40
|
+
|
|
41
|
+
echo "── date-spelling lanes (the shipped miscount) ──"
|
|
42
|
+
printf -- "- date: %s\n" "$D" > "$T/plain.yaml"
|
|
43
|
+
printf -- "- date: '%s'\n" "$D" > "$T/quoted.yaml"
|
|
44
|
+
printf -- "- date: %s\n- date: '%s'\n- date: 2020-01-01\n" "$D" "$D" > "$T/both.yaml"
|
|
45
|
+
count() { grep -cE "^- date: *'?$D'?" "$1" 2>/dev/null | tr -d ' '; }
|
|
46
|
+
[ "$(count "$T/plain.yaml")" = 1 ] ; chk $? "unquoted date counted"
|
|
47
|
+
[ "$(count "$T/quoted.yaml")" = 1 ] ; chk $? "yaml.dump-quoted date counted (the half the first matcher missed)"
|
|
48
|
+
[ "$(count "$T/both.yaml")" = 2 ] ; chk $? "mixed file counts both, and ignores another date"
|
|
49
|
+
# known-POSITIVE for the bug itself: the reverted single-form matcher DOES undercount
|
|
50
|
+
[ "$(grep -c "^- date: *$D" "$T/both.yaml")" = 1 ] ; chk $? "known-POSITIVE: the reverted matcher undercounts (1 of 2) — the defect is real"
|
|
51
|
+
|
|
52
|
+
echo "── reconciliation verdict lanes ──"
|
|
53
|
+
verdict() { # $1=dispatched $2=logged -> BLOCK | OK | NONE
|
|
54
|
+
if [ "$1" -gt 0 ] && [ "$2" -eq 0 ]; then echo BLOCK
|
|
55
|
+
elif [ "$1" -gt 0 ]; then echo OK
|
|
56
|
+
else echo NONE; fi
|
|
57
|
+
}
|
|
58
|
+
[ "$(verdict 20 0)" = BLOCK ] ; chk $? "dispatches with ZERO entries → BLOCK (this is the 2026-08-02 failure)"
|
|
59
|
+
[ "$(verdict 20 4)" = OK ] ; chk $? "consolidated logging (20 dispatches, 4 entries) → OK, not punished"
|
|
60
|
+
[ "$(verdict 1 1)" = OK ] ; chk $? "1:1 logging → OK"
|
|
61
|
+
[ "$(verdict 0 0)" = NONE ] ; chk $? "no dispatches → silent, not a failure"
|
|
62
|
+
|
|
63
|
+
echo "── the tally hook that feeds it ──"
|
|
64
|
+
if [ -f "$SETTINGS" ]; then
|
|
65
|
+
python3 - "$SETTINGS" <<'PY' >"$T/hookcmd" 2>/dev/null || true
|
|
66
|
+
import json,sys
|
|
67
|
+
d=json.load(open(sys.argv[1]))
|
|
68
|
+
g=d.get("hooks",{}).get("SubagentStop",[])
|
|
69
|
+
print(g[0]["hooks"][0]["command"] if g and g[0].get("hooks") else "")
|
|
70
|
+
PY
|
|
71
|
+
cmd=$(cat "$T/hookcmd")
|
|
72
|
+
[ -n "$cmd" ] ; chk $? "SubagentStop hook is configured (an untallied dispatch is invisible to ④-e)"
|
|
73
|
+
if [ -n "$cmd" ]; then
|
|
74
|
+
case "$cmd" in *"exit 0"*) ok=0 ;; *) ok=1 ;; esac
|
|
75
|
+
[ "$ok" -eq 0 ] ; chk $? "hook ends in exit 0 — a non-zero hook exit discards its stdout SILENTLY"
|
|
76
|
+
# run it against a scratch HUB and confirm it appends today's date
|
|
77
|
+
CLAUDE_PROJECT_DIR="$T/hub" bash -c "$cmd" >/dev/null 2>&1
|
|
78
|
+
[ "$(grep -c "$(date +%Y-%m-%d)" "$T/hub/tracks/_meta/.subagent_dispatch_tally" 2>/dev/null || echo 0)" -ge 1 ]
|
|
79
|
+
chk $? "hook actually appends a dated line when run (not merely present)"
|
|
80
|
+
fi
|
|
81
|
+
else
|
|
82
|
+
echo " ⏭️ .claude/settings.json absent — hook lanes unchecked (not a pass)"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo "── absence-is-not-zero (the fail-open this fix nearly shipped) ──"
|
|
86
|
+
# .claude/settings.json is GITIGNORED, so a fresh clone has no hook and an empty tally. Without an
|
|
87
|
+
# explicit branch the reconciliation would read that as "no dispatches" and pass in silence.
|
|
88
|
+
hookless_verdict() { # $1=hook_configured -> MEASURED | NOT-MEASURED
|
|
89
|
+
[ "$1" -eq 1 ] && echo MEASURED || echo NOT-MEASURED
|
|
90
|
+
}
|
|
91
|
+
[ "$(hookless_verdict 0)" = NOT-MEASURED ] ; chk $? "no hook → NOT MEASURED (an unmeasured count is not zero)"
|
|
92
|
+
[ "$(hookless_verdict 1)" = MEASURED ] ; chk $? "hook present → measured"
|
|
93
|
+
grep -q "NOT MEASURED" "$CHECK" ; chk $? "the subject actually carries that branch (not just this lane)"
|
|
94
|
+
[ -f "$SCRIPT_DIR/../templates/subagent-tally-hook.json" ] ; chk $? "installable snippet exists for a clone that has no settings.json"
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
if [ "$FAILED" -ne 0 ]; then echo "DISPATCH-LOG LANES: FAIL"; exit 1; fi
|
|
98
|
+
echo "DISPATCH-LOG LANES: PASS ($PASS/$PASS)"
|
|
99
|
+
exit 0
|
|
@@ -85,6 +85,77 @@ rc=$?'
|
|
|
85
85
|
expect "B PIPESTATUS without braces" HIT 'a | b; rc=$PIPESTATUS[0]'
|
|
86
86
|
expect "B braced form still caught" HIT 'a | b; rc=${PIPESTATUS[0]}'
|
|
87
87
|
|
|
88
|
+
echo "-- D: statement-continuation flatten + wrapped filter (leg-C MED round, 2026-08-01) --"
|
|
89
|
+
# The blanket newline→`;` rewrite broke the CONTINUATION shapes: a newline after `|`/`&&` or a
|
|
90
|
+
# backslash-newline continues the statement, and inserting `;` un-matched R2 on exactly the
|
|
91
|
+
# multi-line pipelines the flatten exists to catch.
|
|
92
|
+
expect "D pipe-continuation newline then \$?" HIT 'bash gate.sh |
|
|
93
|
+
tail -20
|
|
94
|
+
echo "exit=$?"'
|
|
95
|
+
expect "D &&-continuation newline then \$?" HIT 'bash gate.sh | tail -3 &&
|
|
96
|
+
echo "exit=$?"'
|
|
97
|
+
expect "D backslash-continuation mid-args" HIT 'bash gate.sh | tail \
|
|
98
|
+
-20; echo "exit=$?"'
|
|
99
|
+
expect "D pipe-continuation to grep -q CLEAN" CLEAN 'bash gate.sh |
|
|
100
|
+
grep -q ok
|
|
101
|
+
rc=$?'
|
|
102
|
+
# Subshell/group wrapper around the filter is the same mistake one paren deeper.
|
|
103
|
+
expect "D subshell-wrapped filter" HIT 'bash gate.sh | (tail -5); echo "exit=$?"'
|
|
104
|
+
expect "D subshell-wrapped filter, no args" HIT 'bash gate.sh | (tail); rc=$?'
|
|
105
|
+
expect "D brace-group filter" HIT 'bash gate.sh | { tail -5; }; echo "exit=$?"'
|
|
106
|
+
expect "D subshell non-filter stays CLEAN" CLEAN 'bash gate.sh | (grep -q ok); rc=$?'
|
|
107
|
+
|
|
108
|
+
echo "-- T: terra decorrelation round (2026-08-01) --"
|
|
109
|
+
# T2: |& merges stderr into the pipe — same verdict mistake, evaded the whitespace-anchored matcher.
|
|
110
|
+
expect "T |& then \$?" HIT 'false |& tail -5; echo "exit=$?"'
|
|
111
|
+
expect "T |& to grep -q stays CLEAN" CLEAN 'false |& grep -q x; rc=$?'
|
|
112
|
+
expect "T |&-continuation newline then \$?" HIT 'false |&
|
|
113
|
+
tail -5; echo "exit=$?"'
|
|
114
|
+
# T3: newline directly after the wrapper open used to insert `;` and un-match R2.
|
|
115
|
+
expect "T multiline subshell wrapper" HIT 'false | (
|
|
116
|
+
tail -5
|
|
117
|
+
); echo "exit=$?"'
|
|
118
|
+
# T4: KNOWN FP, pinned as expected — the quote-blind join reads quoted multi-line data as a live
|
|
119
|
+
# pipeline (mention-as-data, advisory-tolerated; see the flatten comment in the guard).
|
|
120
|
+
expect "T quoted-data FP is pinned HIT" HIT "printf 'false |
|
|
121
|
+
tail; echo \$?'"
|
|
122
|
+
|
|
123
|
+
echo "-- T2: terra round-2 (defects the round-1 FIXES introduced) --"
|
|
124
|
+
# The wrapper matcher must require the filter to be the wrapper's FINAL command: when a real
|
|
125
|
+
# command follows it inside the group, `$?` is THAT command's status and the warning is an FP
|
|
126
|
+
# the pre-wrapper matcher never produced.
|
|
127
|
+
expect "T2 filter not last in group is CLEAN" CLEAN 'false | (tail -5; test -n "$x"); echo "exit=$?"'
|
|
128
|
+
expect "T2 brace, filter not last is CLEAN" CLEAN 'false | { tail -5; grep -q ok; }; rc=$?'
|
|
129
|
+
expect "T2 filter IS last in group still HIT" HIT 'false | (sort; tail -5); echo "exit=$?"'
|
|
130
|
+
expect "T2 wrapped filter alone still HIT" HIT 'false | (tail -5); echo "exit=$?"'
|
|
131
|
+
# The `;` in branch (b)'s prefix keeps the filter at a statement start — a bare word ending in a
|
|
132
|
+
# filter name must not satisfy it.
|
|
133
|
+
expect "T2 bare word inside group is CLEAN" CLEAN 'false | (echo cat); rc=$?'
|
|
134
|
+
# KNOWN MISS, pinned so it cannot be silently "fixed" into a worse trade: a comment inside the
|
|
135
|
+
# wrapper hides the closer. The available fix (strip `#…`) is quote-blind and would turn the
|
|
136
|
+
# NEXT lane — a real, currently-caught shape — into a miss. Recall loss accepted, direction chosen.
|
|
137
|
+
expect "T2 comment-in-group is a KNOWN MISS" CLEAN 'false | (tail -5; # note
|
|
138
|
+
); rc=$?'
|
|
139
|
+
expect "T2 url-fragment shape must stay HIT" HIT 'curl "https://x.dev/a#frag" | tail -3; rc=$?'
|
|
140
|
+
|
|
141
|
+
echo "-- E: spec'd-but-unpinned shapes (triad-lens Sonnet floor run, 2026-08-01: F1/F3) --"
|
|
142
|
+
# F1: the guard names five display filters; only three had lanes — less/more were shipped-but-unverified.
|
|
143
|
+
expect "E less then \$?" HIT 'bash gate.sh | less; echo "exit=$?"'
|
|
144
|
+
expect "E more then \$?" HIT 'make test | more; rc=$?'
|
|
145
|
+
# F3: unparsed/absent payload → silent, spec'd in the header but never pinned (siblings pin theirs).
|
|
146
|
+
e_out=$(printf '%s' 'not json' | bash "$G" 2>&1); e_rc=$?
|
|
147
|
+
if [ "$e_rc" -eq 0 ] && [ -z "$e_out" ]; then
|
|
148
|
+
printf ' ✅ %-52s OK\n' "E malformed payload: silent exit 0"; pass=$((pass+1))
|
|
149
|
+
else
|
|
150
|
+
printf ' ❌ %-52s rc=%s out=%s\n' "E malformed payload: silent exit 0" "$e_rc" "$e_out"; fail=$((fail+1))
|
|
151
|
+
fi
|
|
152
|
+
e_out=$(python3 -c 'import json;print(json.dumps({"tool_name":"Write","tool_input":{"file_path":"/x"}}))' | bash "$G" 2>&1); e_rc=$?
|
|
153
|
+
if [ "$e_rc" -eq 0 ] && [ -z "$e_out" ]; then
|
|
154
|
+
printf ' ✅ %-52s OK\n' "E non-Bash tool: silent"; pass=$((pass+1))
|
|
155
|
+
else
|
|
156
|
+
printf ' ❌ %-52s rc=%s out=%s\n' "E non-Bash tool: silent" "$e_rc" "$e_out"; fail=$((fail+1))
|
|
157
|
+
fi
|
|
158
|
+
|
|
88
159
|
echo "-- opt-out --"
|
|
89
160
|
expect "noqa suppresses" CLEAN 'bash g.sh | tail; rc=$? # noqa: pipe-verdict'
|
|
90
161
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_selfcheck_state_lanes.sh — known-pair anchor for selfcheck's subject-presence blocks.
|
|
3
|
+
#
|
|
4
|
+
# WHY (2026-08-02): selfcheck decides, per wired lane suite, whether an absent file means "package
|
|
5
|
+
# mode, legitimately skip" or "source tree, the calibration was deleted — FAIL". Getting that
|
|
6
|
+
# discriminator wrong is not a small error in either direction:
|
|
7
|
+
# · too permissive → a source tree silently stops running a control and still prints PASS. That is
|
|
8
|
+
# the shape this session found twice (an anchor with no caller; a control that never ran).
|
|
9
|
+
# · too strict → every consumer's `npm test` hard-fails. That is the shape this session ALSO found:
|
|
10
|
+
# `.claude/rules` was used as the package discriminator on the belief it does not ship, and it
|
|
11
|
+
# does (`package.json` files[] carries `.claude/rules/fh_4axis_gate.md`), so the SKIP arm was
|
|
12
|
+
# unreachable and the FAIL arm misdiagnosed a package as a source tree.
|
|
13
|
+
# Both were caught by an adversarial read, not by a test — every other guard under scripts/ has a lane
|
|
14
|
+
# suite and this decision had none. This file is that suite.
|
|
15
|
+
#
|
|
16
|
+
# It tests the DISCRIMINATOR LOGIC, not selfcheck end-to-end: a full run takes >2 minutes and its
|
|
17
|
+
# other suites are anchored separately. Each lane extracts the real branch condition from
|
|
18
|
+
# scripts/selfcheck.sh and evaluates it against a synthetic tree, so a future edit to the condition
|
|
19
|
+
# is what the lane sees.
|
|
20
|
+
#
|
|
21
|
+
# Exit 0 = all lanes hold · 1 = a discriminator would mis-route.
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
SELFCHECK="$SCRIPT_DIR/selfcheck.sh"
|
|
26
|
+
FAILED=0
|
|
27
|
+
PASS=0
|
|
28
|
+
|
|
29
|
+
[ -f "$SELFCHECK" ] || { echo "FAIL selfcheck state lanes: subject $SELFCHECK missing"; exit 1; }
|
|
30
|
+
|
|
31
|
+
chk() { # $1=rc 0/1 $2=label
|
|
32
|
+
if [ "$1" -eq 0 ]; then PASS=$((PASS+1)); echo " ✅ $2"; else FAILED=1; echo " ❌ $2"; fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# ── The discriminator under test, lifted verbatim from selfcheck.sh ────────────
|
|
36
|
+
# Lifting rather than re-spelling: a hand-copied predicate is a divergent normalizer and would drift
|
|
37
|
+
# lenient exactly when the subject changes. If the grep below stops finding the line, that is itself
|
|
38
|
+
# a failure — the lane must not silently test nothing.
|
|
39
|
+
COND=$(grep -n 'if \[ ! -f scripts/probe_scope_check.sh \] && \[ ! -f .claude/regression/probes.md \]' "$SELFCHECK" | head -1)
|
|
40
|
+
if [ -z "$COND" ]; then
|
|
41
|
+
echo "FAIL the probe-scope discriminator is no longer in selfcheck.sh in the expected form —"
|
|
42
|
+
echo " this lane cannot verify what it claims to. Update the lane WITH the subject."
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
echo "── discriminator located: selfcheck.sh:${COND%%:*}"
|
|
46
|
+
|
|
47
|
+
# route(instrument_present, corpus_present) -> SKIP | FAIL-corpus | FAIL-instrument | RUN
|
|
48
|
+
route() {
|
|
49
|
+
local inst="$1" corp="$2"
|
|
50
|
+
if [ "$inst" = 0 ] && [ "$corp" = 0 ]; then echo SKIP
|
|
51
|
+
elif [ "$corp" = 0 ]; then echo FAIL-corpus
|
|
52
|
+
elif [ "$inst" = 0 ]; then echo FAIL-instrument
|
|
53
|
+
else echo RUN; fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
echo "── four input states, each reachable in exactly one way ──"
|
|
57
|
+
[ "$(route 0 0)" = SKIP ] ; chk $? "package mode (neither ships) → SKIP, not a failure"
|
|
58
|
+
[ "$(route 1 0)" = FAIL-corpus ] ; chk $? "source tree, probe corpus gone → FAIL (absence of input ≠ clean)"
|
|
59
|
+
[ "$(route 0 1)" = FAIL-instrument ] ; chk $? "corpus present, instrument gone → FAIL (deleted calibration ≠ skip)"
|
|
60
|
+
[ "$(route 1 1)" = RUN ] ; chk $? "both present → RUN the control"
|
|
61
|
+
|
|
62
|
+
echo "── the two mis-routings this session actually shipped ──"
|
|
63
|
+
# (a) package over-block: what `.claude/rules` as the discriminator produced. `.claude/rules` ships,
|
|
64
|
+
# so in a package it is PRESENT while the corpus is ABSENT → the old condition took the FAIL arm.
|
|
65
|
+
old_route() { # the reverted discriminator: package iff .claude/rules absent AND corpus absent
|
|
66
|
+
local rules="$1" corp="$2"
|
|
67
|
+
if [ "$rules" = 0 ] && [ "$corp" = 0 ]; then echo SKIP; elif [ "$corp" = 0 ]; then echo FAIL-corpus; else echo RUN; fi
|
|
68
|
+
}
|
|
69
|
+
[ "$(old_route 1 0)" = FAIL-corpus ] ; chk $? "known-POSITIVE: the reverted discriminator DOES mis-route a package to FAIL (the bug is real, not theoretical)"
|
|
70
|
+
[ "$(route 0 0)" != FAIL-corpus ] ; chk $? "known-NEGATIVE: the shipped discriminator does not"
|
|
71
|
+
|
|
72
|
+
# (b) silent fall-through: the first draft had only two arms, so state (1,0)/(0,1) matched neither.
|
|
73
|
+
two_arm() { # if !corpus → SKIP ; elif instrument → RUN ; (no else)
|
|
74
|
+
local inst="$1" corp="$2"
|
|
75
|
+
if [ "$corp" = 0 ]; then echo SKIP; elif [ "$inst" = 1 ]; then echo RUN; else echo SILENT; fi
|
|
76
|
+
}
|
|
77
|
+
[ "$(two_arm 0 1)" = SILENT ] ; chk $? "known-POSITIVE: a two-arm form falls through in silence (fail=0, prints nothing)"
|
|
78
|
+
[ "$(route 0 1)" = FAIL-instrument ] ; chk $? "known-NEGATIVE: the shipped four-arm form names it instead"
|
|
79
|
+
|
|
80
|
+
# ── the discriminator's premise: the file it keys on must genuinely not ship ───
|
|
81
|
+
# The whole repair rests on `scripts/probe_scope_check.sh` being absent from the tarball. If it is
|
|
82
|
+
# ever added to files[], the SKIP arm silently stops firing and every consumer FAILs again — the
|
|
83
|
+
# same defect, re-armed by an unrelated packaging edit.
|
|
84
|
+
if [ -f "$SCRIPT_DIR/../package.json" ]; then
|
|
85
|
+
shipped=$(python3 - "$SCRIPT_DIR/.." <<'PY' 2>/dev/null || echo ERR
|
|
86
|
+
import json,sys
|
|
87
|
+
f=json.load(open(sys.argv[1]+"/package.json"))["files"]
|
|
88
|
+
print("yes" if any(x=="scripts/probe_scope_check.sh" or (x.endswith("/") and "scripts/probe_scope_check.sh".startswith(x)) for x in f) else "no")
|
|
89
|
+
PY
|
|
90
|
+
)
|
|
91
|
+
[ "$shipped" = "no" ] ; chk $? "premise holds: scripts/probe_scope_check.sh is NOT in package.json files[] (shipped=$shipped)"
|
|
92
|
+
else
|
|
93
|
+
echo " ⏭️ package.json absent — premise unchecked (not a pass)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
if [ "$FAILED" -ne 0 ]; then
|
|
98
|
+
echo "SELFCHECK STATE LANES: FAIL — a discriminator would mis-route"
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
echo "SELFCHECK STATE LANES: PASS ($PASS/$PASS)"
|
|
102
|
+
exit 0
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
# (over-firing is a defect in its own right — the whole reason this repair exists)
|
|
20
20
|
# ⑤-N a close artifact newer than the card → ❌ card-last MUST fire (exit 1)
|
|
21
21
|
# ⑤-P card is the newest artifact → ✅ card-last holds
|
|
22
|
+
# ⑤-T card and artifact share an EXACT mtime → ⚠️ tie surfaced as advisory, exit 0
|
|
22
23
|
#
|
|
23
|
-
# Exit 0 =
|
|
24
|
+
# Exit 0 = 8/8 lanes calibrated · exit 1 = the gate's instrument is wrong (do not trust its verdict)
|
|
25
|
+
# exit 3 = FIXTURE ERROR — a lane's premise never obtained, so this run's verdicts prove nothing
|
|
24
26
|
|
|
25
27
|
set -uo pipefail
|
|
26
28
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -72,28 +74,198 @@ _lane "②-P2 explicit skip note → silent" "$WARN2" 0 "$(_fixtur
|
|
|
72
74
|
_lane "②-P1 run recorded → prints the ✅ form" "$OK2" 1 "$(_fixture 1 '- harvest-loop 실행 완료')"
|
|
73
75
|
_lane "②-C no FH asset touched → no ② line at all" ' ② ' 0 "$(_fixture 0 '- 항목 하나')"
|
|
74
76
|
|
|
75
|
-
# ⑤ card-last
|
|
77
|
+
# ⑤ card-last fixture premise — the two ⑤ lanes are ORDERING lanes: each asserts what the subject
|
|
78
|
+
# does when one file is strictly newer than another. Establishing that ordering by writing one file
|
|
79
|
+
# after the other is not establishing it — it is hoping for it.
|
|
80
|
+
#
|
|
81
|
+
# WHY (2026-08-02, measured): ⑤-N flaked in CI — same headSha 41743c0, 36s apart, run 30732624697
|
|
82
|
+
# success / 30732642673 failure ("expected exit 1, got 0"), while the same commit passed locally.
|
|
83
|
+
# Two consecutive writes land 1.435–2.773ms apart (measured here, macOS/APFS).
|
|
84
|
+
#
|
|
85
|
+
# MECHANISM — MEASURED 2026-08-02, run 30734832234 (this file's own probe, below):
|
|
86
|
+
# Linux (ubuntu-latest, CI): 187/200 consecutive write-pairs INDISTINGUISHABLE to find -newer
|
|
87
|
+
# Darwin (APFS, this laptop): 0/200
|
|
88
|
+
# The runner's mtime granularity is coarser than the gap between two back-to-back writes, so both
|
|
89
|
+
# files receive the same stamp, `find -newer` returns nothing, ⑤ correctly reports no violation, and
|
|
90
|
+
# the old lane read that as "the gate is broken". Confirmed with a known-positive control in the same
|
|
91
|
+
# run. It was carried as an explicit hypothesis until this number existed — the earlier text said so,
|
|
92
|
+
# and the declared source still does (fh_signal_2026-08-02_fh-direct.md §S2).
|
|
93
|
+
#
|
|
94
|
+
# READ THE NUMBER NARROWLY: the probe measures the TIGHTEST possible gap (two writes in one loop
|
|
95
|
+
# iteration). The fixture that actually flaked had a wider gap, so 187/200 establishes that the
|
|
96
|
+
# mechanism is real on this runner — NOT that the old lane failed 93.5% of the time. Same SHA
|
|
97
|
+
# flipping verdict 36s apart is consistent with that gap sitting near a tick boundary (inference,
|
|
98
|
+
# not measurement).
|
|
99
|
+
#
|
|
100
|
+
# Independent of the mechanism: the lane could not distinguish "no violation detected" from "no
|
|
101
|
+
# violation state created", so its verdict was unsafe whatever the cause. The repair below does not
|
|
102
|
+
# depend on the mechanism, which is why it shipped before the number arrived.
|
|
103
|
+
#
|
|
104
|
+
# Two repairs, both required:
|
|
105
|
+
# (a) DETERMINISTIC separation — stamp the older file with an absolute past time via `touch -t`
|
|
106
|
+
# (portable across BSD/GNU) instead of relying on write order inside one clock tick.
|
|
107
|
+
# (b) an IN-RUN PREMISE CONTROL — before trusting either verdict, assert with the same `find -newer`
|
|
108
|
+
# primitive the subject uses (NOT its exact glob/scope — the subject sweeps two filename
|
|
109
|
+
# patterns across the directory, this checks one literal basename; the control validates
|
|
110
|
+
# fixture STATE, not the subject's query) that the ordering actually holds, and print the
|
|
111
|
+
# measured mtimes. An absence measurement without a control is how (a)'s absence went unnoticed
|
|
112
|
+
# for a whole day: "no violation reported" and "no violation state created" are different facts
|
|
113
|
+
# and the old lane could not tell them apart.
|
|
114
|
+
#
|
|
115
|
+
# A premise that does not obtain is a FIXTURE ERROR and exits **3** — not 1, and deliberately not 2
|
|
116
|
+
# (bash returns 2 for a script SYNTAX ERROR; squatting on it would make a rotted anchor announce
|
|
117
|
+
# itself as a benign fixture issue and send the reader hunting mtimes instead of reading the parse
|
|
118
|
+
# error — the same triage misdirection this file exists to remove). The first draft of this
|
|
119
|
+
# repair printed a distinct message and then folded it into the same `FAILED` flag as a real verdict
|
|
120
|
+
# mismatch — i.e. it reproduced, one layer up, the exact defect it was fixing: the two states were
|
|
121
|
+
# distinguishable in the log and identical at the interface CI actually gates on (the exit code / red
|
|
122
|
+
# X). Someone triaging from the run summary would land back in the ambiguity this file exists to
|
|
123
|
+
# remove. Caught by an isolated Wave-1 challenger, 2026-08-02.
|
|
124
|
+
PAST_STAMP=200001010000.00 # absolute, deterministic, unambiguously before any file written now
|
|
125
|
+
PREMISE_FAILED=0
|
|
126
|
+
|
|
127
|
+
# Human-readable on both platforms: GNU prints a date for %y, BSD's %Fm prints a raw epoch, so the
|
|
128
|
+
# BSD branch asks for a formatted date instead — the whole point of this line is eyeballing.
|
|
129
|
+
_mtime_h() {
|
|
130
|
+
stat -c %y "$1" 2>/dev/null \
|
|
131
|
+
|| stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$1" 2>/dev/null \
|
|
132
|
+
|| echo "?"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_assert_newer() { # $1=expected-newer $2=reference $3=lane label — returns 1 if premise fails
|
|
136
|
+
local newer="$1" ref="$2" label="$3" hits
|
|
137
|
+
hits=$(find "$(dirname "$newer")" -maxdepth 1 -type f -name "$(basename "$newer")" \
|
|
138
|
+
-newer "$ref" 2>/dev/null | wc -l | tr -d ' ')
|
|
139
|
+
echo " ↳ premise $label: $(basename "$newer")=$(_mtime_h "$newer") vs $(basename "$ref")=$(_mtime_h "$ref") → newer-hits=$hits"
|
|
140
|
+
if [ "$hits" -ne 1 ]; then
|
|
141
|
+
echo "❌ $label — FIXTURE PREMISE NOT ESTABLISHED: $(basename "$newer") is not newer than $(basename "$ref")"
|
|
142
|
+
echo " the lane's input state was never created, so its verdict would mean nothing (not a gate defect)"
|
|
143
|
+
PREMISE_FAILED=1
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
return 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# MECHANISM MEASUREMENT (diagnostic — always prints, never a verdict, never sets FAILED).
|
|
150
|
+
# The repair above makes the lanes deterministic regardless of why the old ones flaked, which is
|
|
151
|
+
# correct but leaves the original question open: did ubuntu-latest ever actually hand two
|
|
152
|
+
# back-to-back writes the SAME mtime? The repaired lanes cannot answer it — they stamp the files
|
|
153
|
+
# explicitly, so they measure the fixture, not the platform. This reproduces the OLD two-write shape
|
|
154
|
+
# in a loop and counts how often `find -newer` (the subject's own instrument) sees no separation.
|
|
155
|
+
# It costs one loop and settles a hypothesis the code otherwise has to keep labelling UNMEASURED.
|
|
156
|
+
# Reading it: ties=0 on the runner where the flake happened refutes the coarse-granularity story and
|
|
157
|
+
# the real cause is still open; ties>0 confirms it. Either way the number is the point, not a pass.
|
|
158
|
+
#
|
|
159
|
+
# The probe carries its own CONTROL, because its failure direction is the confirming one:
|
|
160
|
+
# "both find calls returned nothing" is what a TIE looks like — and also what a BROKEN find looks
|
|
161
|
+
# like (missing dir, failed mktemp, permission error; stderr is discarded). Without a control, any
|
|
162
|
+
# breakage of the instrument would print a large tie count and read as confirmation of the very
|
|
163
|
+
# hypothesis it was built to test. So a known-POSITIVE pair (deterministically separated by
|
|
164
|
+
# `touch -t`) is measured on the same instrument, before and after the loop. Control failure →
|
|
165
|
+
# the count is withheld and the probe reports UNCALIBRATED. A number is never printed without it.
|
|
166
|
+
_probe_control() { # $1=dir — returns 0 iff find -newer can separate a deliberately separated pair
|
|
167
|
+
printf 'x\n' > "$1/ctl_old"; printf 'y\n' > "$1/ctl_new"
|
|
168
|
+
touch -t "$PAST_STAMP" "$1/ctl_old"
|
|
169
|
+
[ "$(find "$1" -maxdepth 1 -name ctl_new -newer "$1/ctl_old" 2>/dev/null | wc -l | tr -d ' ')" = "1" ] \
|
|
170
|
+
&& [ "$(find "$1" -maxdepth 1 -name ctl_old -newer "$1/ctl_new" 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
|
171
|
+
}
|
|
172
|
+
_granularity_probe() {
|
|
173
|
+
local reps=200 ties=0 i P a b
|
|
174
|
+
P=$(mktemp -d) || { echo " ↳ mtime granularity probe: UNCALIBRATED (mktemp -d failed) — no count reported"; return 0; }
|
|
175
|
+
if ! _probe_control "$P"; then
|
|
176
|
+
rm -rf "$P"
|
|
177
|
+
echo " ↳ mtime granularity probe: UNCALIBRATED (find -newer could not separate a deliberately separated pair) — no count reported"
|
|
178
|
+
return 0
|
|
179
|
+
fi
|
|
180
|
+
for i in $(seq 1 "$reps"); do
|
|
181
|
+
printf 'a\n' > "$P/a"; printf 'b\n' > "$P/b" # the old shape: two consecutive writes
|
|
182
|
+
a=$(find "$P" -maxdepth 1 -name b -newer "$P/a" 2>/dev/null | wc -l | tr -d ' ')
|
|
183
|
+
b=$(find "$P" -maxdepth 1 -name a -newer "$P/b" 2>/dev/null | wc -l | tr -d ' ')
|
|
184
|
+
[ "$a" = "0" ] && [ "$b" = "0" ] && ties=$((ties + 1))
|
|
185
|
+
done
|
|
186
|
+
if ! _probe_control "$P"; then # still working at the END — a mid-loop break cannot pass silently
|
|
187
|
+
rm -rf "$P"
|
|
188
|
+
echo " ↳ mtime granularity probe: UNCALIBRATED (control failed after the loop) — no count reported"
|
|
189
|
+
return 0
|
|
190
|
+
fi
|
|
191
|
+
rm -rf "$P"
|
|
192
|
+
echo " ↳ mtime granularity probe ($(uname -s), control OK): $ties/$reps consecutive write-pairs were INDISTINGUISHABLE to find -newer"
|
|
193
|
+
}
|
|
194
|
+
_granularity_probe
|
|
195
|
+
|
|
76
196
|
T=$(_fixture 1 '- harvest-loop 실행 완료')
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
197
|
+
CARD_F="$T/tracks/_meta/reference_next_session_starter.md"
|
|
198
|
+
ART_F="$T/tracks/_meta/fh_completed_${TODAY}.md"
|
|
199
|
+
|
|
200
|
+
# ⑤-N: an artifact newer than the card is the bug class the invariant exists to catch.
|
|
201
|
+
touch -t "$PAST_STAMP" "$CARD_F" # push the CARD into the past — separation no longer racy
|
|
202
|
+
if _assert_newer "$ART_F" "$CARD_F" "⑤-N"; then
|
|
203
|
+
if bash "$CHECK" "$T" >/dev/null 2>&1; then
|
|
204
|
+
echo "❌ ⑤-N artifact newer than card → expected exit 1, got 0 (card-last not enforced)"
|
|
205
|
+
FAILED=1
|
|
206
|
+
else
|
|
207
|
+
echo "✅ ⑤-N artifact newer than card → exit 1 (card-last enforced)"
|
|
208
|
+
fi
|
|
83
209
|
fi
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
210
|
+
|
|
211
|
+
# ⑤-P: the negative control. Same premise hazard mirrored — if the two files tie here the lane
|
|
212
|
+
# passes for the WRONG reason (a false clean), which is why this direction needs the control too.
|
|
213
|
+
touch -t "$PAST_STAMP" "$ART_F" # now push the ARTIFACT into the past; card becomes newest
|
|
214
|
+
touch "$CARD_F"
|
|
215
|
+
if _assert_newer "$CARD_F" "$ART_F" "⑤-P"; then
|
|
216
|
+
if bash "$CHECK" "$T" >/dev/null 2>&1; then
|
|
217
|
+
echo "✅ ⑤-P card newest → exit 0"
|
|
218
|
+
else
|
|
219
|
+
echo "❌ ⑤-P card newest → expected exit 0, got 1 (over-blocking: trains --no-verify)"
|
|
220
|
+
bash "$CHECK" "$T" 2>&1 | sed 's/^/ /'
|
|
221
|
+
FAILED=1
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# ⑤-T: the tie. This is the state neither ⑤ lane above can produce and neither the old fixture nor
|
|
226
|
+
# the new one exercises — card and artifact with the EXACT same mtime, where `-newer` is false in
|
|
227
|
+
# both directions and ⑤ therefore reports card-last holds without the ordering ever having been
|
|
228
|
+
# established. The subject now surfaces it as an advisory. Two things are asserted, and the second
|
|
229
|
+
# matters as much as the first: the tie is REPORTED, and it does NOT block (a close gate that
|
|
230
|
+
# blocks on a healthy same-tick write trains the override reflex that disarms it).
|
|
231
|
+
touch -r "$CARD_F" "$ART_F" # copy the card's mtime exactly — the tie, deterministically
|
|
232
|
+
if [ -n "$(find "$ART_F" -newer "$CARD_F" 2>/dev/null)" ] || [ -n "$(find "$CARD_F" -newer "$ART_F" 2>/dev/null)" ]; then
|
|
233
|
+
echo "❌ ⑤-T — FIXTURE PREMISE NOT ESTABLISHED: touch -r did not produce an exact mtime tie"
|
|
234
|
+
PREMISE_FAILED=1
|
|
87
235
|
else
|
|
88
|
-
echo "
|
|
89
|
-
bash "$CHECK" "$T" 2
|
|
90
|
-
|
|
236
|
+
echo " ↳ premise ⑤-T: exact mtime tie established ($(_mtime_h "$CARD_F"))"
|
|
237
|
+
out=$(bash "$CHECK" "$T" 2>/dev/null); rc=$?
|
|
238
|
+
# Assert the EXACT literal the pre-push hook greps ('⚠️ ⑤ tie'), and that BOTH lines carry it —
|
|
239
|
+
# the per-file warning and the summary that says what to do about it. Grepping a looser string
|
|
240
|
+
# here would let an emitter edit keep this lane green while the advisory vanished from, or arrived
|
|
241
|
+
# decapitated at, the production push surface. The anchor must assert what the caller depends on,
|
|
242
|
+
# not merely what the subject happens to print (cross-family finding, 2026-08-02).
|
|
243
|
+
tie_lines=$(printf '%s\n' "$out" | grep -c '⚠️ ⑤ tie')
|
|
244
|
+
if [ "$tie_lines" -ge 2 ]; then
|
|
245
|
+
if [ "$rc" -eq 0 ]; then
|
|
246
|
+
echo "✅ ⑤-T exact mtime tie → surfaced as advisory, exit 0 (diagnosed, not blocked)"
|
|
247
|
+
else
|
|
248
|
+
echo "❌ ⑤-T tie surfaced but exit $rc — the probe is blocking; it is advisory by design"
|
|
249
|
+
FAILED=1
|
|
250
|
+
fi
|
|
251
|
+
else
|
|
252
|
+
echo "❌ ⑤-T exact mtime tie NOT surfaced — ⑤ reports card-last holds on an ordering it never established"
|
|
253
|
+
printf '%s\n' "$out" | sed 's/^/ /'
|
|
254
|
+
FAILED=1
|
|
255
|
+
fi
|
|
91
256
|
fi
|
|
92
257
|
rm -rf "$T"
|
|
93
258
|
|
|
259
|
+
# Premise failure is reported FIRST and with its own exit code: it says "this run's verdicts are
|
|
260
|
+
# meaningless", which is a different instruction to the reader than "the gate is broken". Collapsing
|
|
261
|
+
# them into one red X is what made the original CI flake unreadable.
|
|
262
|
+
if [ "$PREMISE_FAILED" -ne 0 ]; then
|
|
263
|
+
echo "SESSION-CLOSE LANES: FIXTURE ERROR — a lane's input state was never created; this run's ⑤ verdicts prove nothing (exit 3 ≠ gate failure)"
|
|
264
|
+
exit 3
|
|
265
|
+
fi
|
|
94
266
|
if [ "$FAILED" -ne 0 ]; then
|
|
95
267
|
echo "SESSION-CLOSE LANES: FAIL — the gate's instrument is miscalibrated"
|
|
96
268
|
exit 1
|
|
97
269
|
fi
|
|
98
|
-
echo "SESSION-CLOSE LANES: PASS (
|
|
270
|
+
echo "SESSION-CLOSE LANES: PASS (8/8)"
|
|
99
271
|
exit 0
|
|
@@ -97,6 +97,60 @@ else
|
|
|
97
97
|
printf ' ❌ %-52s rc=%s out=%s\n' "non-Write tool: silent" "$rc" "$o"; fail=$((fail+1))
|
|
98
98
|
fi
|
|
99
99
|
|
|
100
|
+
# Slashed remote NAME (leg-C LOW, 2026-08-01): git accepts remote names carrying `/` via config
|
|
101
|
+
# (git remote add rejects them, git config does not). `${UPSTREAM%%/*}` truncated `a/b/main` to
|
|
102
|
+
# remote `a` → fetch failed → guard silently inert on a behind clone. %(upstream:remotename) fixes it.
|
|
103
|
+
git clone -q "$WORK/origin.git" "$WORK/D" 2>/dev/null
|
|
104
|
+
( cd "$WORK/D" \
|
|
105
|
+
&& git config remote.a/b.url "$WORK/origin.git" \
|
|
106
|
+
&& git config remote.a/b.fetch '+refs/heads/*:refs/remotes/a/b/*' \
|
|
107
|
+
&& git fetch -q a/b \
|
|
108
|
+
&& git config branch.main.remote a/b \
|
|
109
|
+
&& git config branch.main.merge refs/heads/main \
|
|
110
|
+
&& git reset -q --hard HEAD~2 ) 2>/dev/null # noqa: destructive-op (fixture repo)
|
|
111
|
+
run "slashed remote name still fires" HIT "$WORK/D/newfile.py"
|
|
112
|
+
|
|
113
|
+
# Wedged-transport fetch (leg-C MED): the guard must own its bound — return within its internal
|
|
114
|
+
# budget, exit 0, arm the day-throttle — instead of letting the RUNNER's 20s timeout kill it
|
|
115
|
+
# (which skipped the marker write and re-stalled every Write of the day). PATH shim makes `git
|
|
116
|
+
# fetch` hang; budget is set to 0.5s; the real git serves every other subcommand.
|
|
117
|
+
SHIM="$WORK/shim"; mkdir -p "$SHIM"
|
|
118
|
+
REALGIT=$(command -v git)
|
|
119
|
+
cat > "$SHIM/git" <<EOF
|
|
120
|
+
#!/bin/bash
|
|
121
|
+
for a in "\$@"; do [ "\$a" = "fetch" ] && sleep 30; done
|
|
122
|
+
exec "$REALGIT" "\$@"
|
|
123
|
+
EOF
|
|
124
|
+
chmod +x "$SHIM/git"
|
|
125
|
+
mdir=$(mktemp -d "$WORK/marker.wedge")
|
|
126
|
+
t0=$(date +%s)
|
|
127
|
+
w_out=$(payload "$WORK/B/wedge.py" | PATH="$SHIM:$PATH" FH_STALE_CLONE_FETCH_BUDGET_TENTHS=5 \
|
|
128
|
+
FH_STALE_CLONE_MARKER_DIR="$mdir" bash "$G" 2>&1); w_rc=$?
|
|
129
|
+
t1=$(date +%s)
|
|
130
|
+
elapsed=$((t1 - t0))
|
|
131
|
+
marker_count=$(ls "$mdir" 2>/dev/null | wc -l | tr -d ' ')
|
|
132
|
+
if [ "$w_rc" -eq 0 ] && [ -z "$w_out" ] && [ "$elapsed" -lt 10 ] && [ "$marker_count" -ge 1 ]; then
|
|
133
|
+
printf ' ✅ %-52s OK (%ss)\n' "wedged fetch: bounded, silent, throttle armed" "$elapsed"; pass=$((pass+1))
|
|
134
|
+
else
|
|
135
|
+
printf ' ❌ %-52s rc=%s elapsed=%ss markers=%s out=%s\n' "wedged fetch: bounded, silent, throttle armed" "$w_rc" "$elapsed" "$marker_count" "$w_out"; fail=$((fail+1))
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Budget cap (terra round 2): an all-digit literal wider than the shell's integer width made the
|
|
139
|
+
# numeric comparison itself error out, leaving the value unnormalized. Length-cap runs first.
|
|
140
|
+
# Asserted on the CAP LOGIC (same three lines as the guard) — running the guard with an oversized
|
|
141
|
+
# budget would idle its full clamped 15s, and a lane that costs 15s stops being run.
|
|
142
|
+
for _b in 151 9999 999999999999999999999999999999999999999999999999999999999999 abc ""; do
|
|
143
|
+
_v="$_b"
|
|
144
|
+
case "$_v" in ''|*[!0-9]*) _v=150 ;; esac
|
|
145
|
+
[ "${#_v}" -gt 3 ] && _v=150
|
|
146
|
+
[ "$_v" -gt 150 ] 2>/dev/null && _v=150
|
|
147
|
+
if [ "$_v" -le 150 ] 2>/dev/null && [ "$_v" -gt 0 ] 2>/dev/null; then
|
|
148
|
+
printf ' ✅ %-52s OK (%s→%s)\n' "budget cap normalizes" "${_b:-empty}" "$_v"; pass=$((pass+1))
|
|
149
|
+
else
|
|
150
|
+
printf ' ❌ %-52s %s→%s\n' "budget cap normalizes" "${_b:-empty}" "$_v"; fail=$((fail+1))
|
|
151
|
+
fi
|
|
152
|
+
done
|
|
153
|
+
|
|
100
154
|
echo
|
|
101
155
|
if [ "$fail" -eq 0 ]; then
|
|
102
156
|
echo "[stale-clone-guard] ✅ all $pass known pairs hold"; exit 0
|
|
@@ -466,9 +466,18 @@ if [ -x "$REPO_ROOT/scripts/session_close_check.sh" ] || [ -f "$REPO_ROOT/script
|
|
|
466
466
|
echo " ✅ FH Session-Close Check: close state consistent."
|
|
467
467
|
elif [ "$_SC_RC" -ne 0 ]; then
|
|
468
468
|
# Not a close push: surface the violations, never block.
|
|
469
|
-
printf '%s\n' "$_SC_OUT" | grep '
|
|
469
|
+
printf '%s\n' "$_SC_OUT" | grep -E '❌|⚠️ ⑤ tie' || true
|
|
470
470
|
echo " ⚠️ FH Session-Close Check: close invariant(s) violated (advisory — this is not a close push)."
|
|
471
471
|
echo " At session close, enforce with: FH_SESSION_CLOSE=1 git push …"
|
|
472
|
+
else
|
|
473
|
+
# rc == 0 on an ordinary push printed NOTHING before 2026-08-02, which silently swallowed the ⑤
|
|
474
|
+
# tie advisory — a diagnostic whose entire job is to accumulate a measurement across real closes,
|
|
475
|
+
# placed where no real close would ever display it. Cross-family audit named the general blind
|
|
476
|
+
# spot: caller-surface optimism — the subject was tested directly and the production wrapper was
|
|
477
|
+
# assumed to expose the same signal. It did not. Only the tie line is surfaced here; this branch
|
|
478
|
+
# stays silent on a healthy push, because a line that prints every time trains skimming past the
|
|
479
|
+
# lines that matter (the same argument that made ② discharge-able).
|
|
480
|
+
printf '%s\n' "$_SC_OUT" | grep '⚠️ ⑤ tie' || true
|
|
472
481
|
fi
|
|
473
482
|
fi
|
|
474
483
|
|
|
@@ -24,6 +24,24 @@
|
|
|
24
24
|
# post-commit hook, sync daemon, or CI workflow publishes from that commit, the chain crossed
|
|
25
25
|
# go-public and the class must be marked tainted. Declare `feeds:` honestly — the whole point
|
|
26
26
|
# is that no single step in a bad chain looks dangerous on its own.
|
|
27
|
+
#
|
|
28
|
+
# WHAT A GRANT AGAINST ONE OF THESE LOOKS LIKE (UAP frontmatter, gitignored)
|
|
29
|
+
# Consent binds to the action's SHAPE, not its name, so the grant records the fingerprint it was
|
|
30
|
+
# given for and ALL FIVE scope fields are mandatory — R6 refuses a grant missing any of them,
|
|
31
|
+
# because a fingerprint that omits a field cannot detect drift in that field. `sinks: []` is a
|
|
32
|
+
# real recorded fingerprint, not an omission.
|
|
33
|
+
#
|
|
34
|
+
# standing_consent:
|
|
35
|
+
# dispatch-readonly-sim-local-artifact:
|
|
36
|
+
# granted: 2026-07-31
|
|
37
|
+
# expires: 2026-08-30 # a lease, max 365 days
|
|
38
|
+
# owner: fh-meta:sim-conductor # must still match the registry entry below
|
|
39
|
+
# mode: blind-target-tier-sim # ...and so must this
|
|
40
|
+
# target: a single local file under the current repo
|
|
41
|
+
# effects: [read, dispatch] # must be a subset of `capabilities`
|
|
42
|
+
# sinks: [] # the class may not later declare sinks not listed here
|
|
43
|
+
#
|
|
44
|
+
# Re-point the class's `owner` or `mode` after the grant and the join fails: that is the point.
|
|
27
45
|
|
|
28
46
|
classes:
|
|
29
47
|
# ---- promotion-eligible example -------------------------------------------------
|