@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.
Files changed (35) hide show
  1. package/.claude/rules/fh_4axis_gate.md +45 -1
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CATALOG.md +23 -1
  4. package/CHEATSHEET.md +1 -1
  5. package/CLAUDE.md +21 -7
  6. package/knowledge/shared/harness-core/field_harness_diagnostic.md +1 -0
  7. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +10 -0
  8. package/knowledge/shared/harness-core/harness_verification_core_extended.md +167 -0
  9. package/knowledge/shared/learnings/subagent_invocations_log.yaml +206 -0
  10. package/knowledge/shared/rules/operational_adaptation.md +35 -9
  11. package/package.json +3 -1
  12. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  13. package/plugins/fh-commons/skills/convergence-loop/SKILL.md +25 -10
  14. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/agents/challenger.md +1 -1
  16. package/plugins/fh-meta/skills/phantom-quench/SKILL.md +1 -1
  17. package/plugins/fh-meta/skills/steel-quench/SKILL.md +78 -10
  18. package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +3 -3
  19. package/scripts/consent_registry_check.sh +187 -15
  20. package/scripts/destructive_pre_gate.sh +32 -5
  21. package/scripts/package_coverage_check.sh +10 -0
  22. package/scripts/pipe_verdict_guard.sh +43 -3
  23. package/scripts/selfcheck.sh +117 -2
  24. package/scripts/session_close_check.sh +103 -1
  25. package/scripts/stale_clone_guard.sh +49 -6
  26. package/scripts/substrate_jump_detector.sh +21 -0
  27. package/scripts/test_consent_registry.sh +341 -42
  28. package/scripts/test_destructive_pre_gate_lanes.sh +42 -0
  29. package/scripts/test_dispatch_log_lanes.sh +99 -0
  30. package/scripts/test_pipe_verdict_guard_lanes.sh +71 -0
  31. package/scripts/test_selfcheck_state_lanes.sh +102 -0
  32. package/scripts/test_session_close_lanes.sh +187 -15
  33. package/scripts/test_stale_clone_guard_lanes.sh +54 -0
  34. package/templates/.git-hooks/pre-push +10 -1
  35. package/templates/consent_classes.yaml.example +18 -0
@@ -33,7 +33,13 @@ UNPUSHED=$(git -C "$FH" log --oneline @{u}.. 2>/dev/null | wc -l | tr -d ' ')
33
33
 
34
34
  # ①-b open-PR sweep (surface-not-auto — requires gh; skip silently offline)
35
35
  if command -v gh >/dev/null 2>&1; then
36
- PRS=$(gh pr list --author "@me" --state open --json number 2>/dev/null | grep -c '"number"' || true)
36
+ # `gh --json` emits COMPACT single-line JSON when piped, so counting LINES returned 1 for ANY
37
+ # non-zero number of open PRs — the sweep hid every PR after the first, in the exact step CLAUDE.md
38
+ # pairs with count consistency. Count OCCURRENCES instead. Measured 2026-08-02 with a known pair:
39
+ # `[{"number":227},{"number":226},{"number":225}]` → old 1, new 3; `[]` → 0 both ways — which is
40
+ # why an environment with zero open PRs can never surface this, and why the 0-case alone is not a
41
+ # calibration. Observed live the same day: 2 open PRs reported as 1.
42
+ PRS=$(gh pr list --author "@me" --state open --json number 2>/dev/null | grep -o '"number"' | wc -l | tr -d ' ' || true)
37
43
  [ "${PRS:-0}" -gt 0 ] && echo "⚠️ ①-b $PRS open PR(s) by you — classify: self-mergeable vs awaiting-external"
38
44
  fi
39
45
 
@@ -111,6 +117,57 @@ if [ -n "$LAST_TAG" ] && [ -f "$FH/package.json" ]; then
111
117
  fi
112
118
  fi
113
119
 
120
+ # ④-e DISPATCH-LOG RECONCILIATION — mechanical, because prose failed completely.
121
+ # CLAUDE.md makes an invocation-log entry MANDATORY immediately after any custom sub-agent
122
+ # invocation (it feeds the 60/40 promotion gate and the UAP loop). Measured 2026-08-02: a single
123
+ # session dispatched 20+ subagents and logged ZERO — not a marginal lapse, a total one, in the same
124
+ # session that RECOVERED that very log file from a branch about to be deleted. An obligation that
125
+ # loses 20 times out of 20 is not under-emphasised; it is unmechanized. Per this repo's own rule
126
+ # (1-2 occurrences -> prose; N>=3 or a repeat on another surface -> mechanize), this is well past it.
127
+ #
128
+ # The check is a RECONCILIATION, not an auto-writer. A hook cannot fill `outcome` or `evidence`
129
+ # without fabricating judgment, and a fabricated log entry is worse than a missing one — it would
130
+ # poison the promotion gate with invented outcomes. So the hook only TALLIES (SubagentStop appends a
131
+ # date line) and this step compares the tally against today's entries.
132
+ #
133
+ # It deliberately does NOT demand 1:1. Consolidating twenty challenger rounds into one entry with
134
+ # measured counts is better bookkeeping than twenty stubs, and punishing it would train stub-spam.
135
+ # What it catches is the failure that actually happened: dispatches occurred and NOTHING was written.
136
+ # ABSENCE IS NOT ZERO. The tally comes from a SubagentStop hook configured in `.claude/settings.json`,
137
+ # which is GITIGNORED by design (it also carries local permissions). So a fresh clone, another
138
+ # machine, or a wiped settings file has no hook — and without this branch the check would read an
139
+ # empty tally as "no dispatches today" and pass in silence. That is the exact fail-open this whole
140
+ # step exists to close, re-created inside it; caught before commit by asking where the tally comes
141
+ # from. Installable snippet: templates/subagent-tally-hook.json.
142
+ TALLY="$FH/tracks/_meta/.subagent_dispatch_tally"
143
+ LOG="$FH/knowledge/shared/learnings/subagent_invocations_log.yaml"
144
+ HOOK_OK=0
145
+ if [ -f "$FH/.claude/settings.json" ]; then
146
+ grep -q '"SubagentStop"' "$FH/.claude/settings.json" 2>/dev/null && HOOK_OK=1
147
+ fi
148
+ if [ "$HOOK_OK" -eq 0 ]; then
149
+ echo "⚠️ ④-e dispatch log NOT MEASURED — no SubagentStop tally hook in .claude/settings.json"
150
+ echo " (that file is gitignored, so a fresh clone has none). An unmeasured dispatch count is"
151
+ echo " NOT a count of zero. Install: templates/subagent-tally-hook.json → .claude/settings.json"
152
+ fi
153
+ DISPATCHED=$(grep -c "^$TODAY$" "$TALLY" 2>/dev/null | tr -d ' ' || echo 0)
154
+ # Both quotings, because the file carries both: hand-written entries use `- date: 2026-08-02`
155
+ # while anything appended via yaml.dump renders `- date: '"'"'2026-08-02'"'"'`. Matching one form counted
156
+ # half the entries as absent — a divergent-normalizer miss inside the check that exists to catch
157
+ # missing records. Known-pair calibrated below in test_dispatch_log_lanes.sh.
158
+ LOGGED=$(grep -cE "^- date: *'?$TODAY'?" "$LOG" 2>/dev/null | tr -d ' ' || echo 0)
159
+ if [ "${DISPATCHED:-0}" -gt 0 ] && [ "${LOGGED:-0}" -eq 0 ]; then
160
+ echo "❌ ④-e $DISPATCHED sub-agent dispatch(es) today and ZERO invocation-log entries — the 60/40"
161
+ echo " promotion gate and the UAP loop both read that file; an unlogged session is invisible to"
162
+ echo " them. Append to knowledge/shared/learnings/subagent_invocations_log.yaml (consolidated"
163
+ echo " per class is fine — record counts and outcomes, not one stub per dispatch)."
164
+ FAIL=1
165
+ elif [ "${DISPATCHED:-0}" -gt 0 ]; then
166
+ echo "✅ ④-e dispatch log: $DISPATCHED dispatch(es) today, $LOGGED log entr(ies) recorded"
167
+ elif [ "$HOOK_OK" -eq 1 ]; then
168
+ echo "✅ ④-e dispatch log: no sub-agent dispatches tallied today"
169
+ fi
170
+
114
171
  # ⑤ CARD-LAST invariant — the card must be the NEWEST close artifact. A card older than
115
172
  # fh_completed / signal files written this session = ⑤ ran before ①–④ finished (the bug class).
116
173
  if [ -f "$CARD" ]; then
@@ -134,6 +191,51 @@ if [ -f "$CARD" ]; then
134
191
  else
135
192
  echo "✅ ⑤ card is the newest close artifact (card-last holds)"
136
193
  fi
194
+
195
+ # ⑤ tie probe (ADVISORY — never changes the verdict, never blocks).
196
+ # `-newer` is a STRICT comparison, so an artifact whose mtime exactly equals the card's is invisible
197
+ # to the check above: ⑤ reports card-last holds when the ordering was never actually established.
198
+ # That is the same blind spot that made the ⑤-N *lane* flake in CI on 2026-08-02, one layer down —
199
+ # and the lane repair hardened the fixture, not this production path, which still runs against
200
+ # ordinary session writes with no deterministic separation.
201
+ # Why advisory and not a verdict: tightening ⑤ to treat a tie as a violation would BLOCK a healthy
202
+ # close whose two writes happened to land in one clock tick, and an over-blocking close gate trains
203
+ # the override reflex that disarms it (the same reasoning the ⑤-P lane already encodes). Whether
204
+ # real closes ever tie is UNMEASURED — so this line measures it instead of guessing at a fix.
205
+ # Diagnosis, not prevention; promote it to a verdict only on evidence that ties actually occur.
206
+ #
207
+ # COST, measured before shipping (this runs on every push via the pre-push hook, so an unbounded
208
+ # per-file cost is a live regression, not a design risk). `! -newer "$CARD"` does the only safe
209
+ # narrowing: strictly-newer files are already reported by ⑤ above, so the in-loop "is it newer?"
210
+ # check is redundant and dropped — one fork per candidate, not two.
211
+ # A `-mtime -1` bound was tried and REMOVED: it scopes the window to *now*, but the invariant is
212
+ # anchored to the *card*, and this script runs on every push — routinely against a card written
213
+ # days ago. Two artifacts tied with an old card then fall outside the window and ⑤ reports
214
+ # card-last holds on an ordering never established, with the probe scoped out of seeing it. Speed
215
+ # that hides the thing being measured is not speed. Measured cost of the correct form on the
216
+ # current corpus: 140 candidates, 0.57s. Linear in tracks/_meta; revisit if that reaches thousands.
217
+ #
218
+ # Both emitted lines start with the same literal `⚠️ ⑤ tie` on purpose — the pre-push hook greps
219
+ # for it, and when the two lines carried different prefixes the hook surfaced the warning while
220
+ # dropping the sentence that says what to do about it, leaving a reader with only "⑤'s strict
221
+ # comparison cannot see it" — precisely the misreading the summary line exists to prevent.
222
+ # Keep the shared prefix; test_session_close_lanes.sh ⑤-T asserts the hook's exact pattern.
223
+ TIES=0
224
+ while IFS= read -r f; do
225
+ [ -n "$f" ] || continue
226
+ [ -n "$(find "$CARD" -newer "$f" 2>/dev/null)" ] && continue # strictly older → fine
227
+ TIES=$((TIES + 1))
228
+ echo "⚠️ ⑤ tie: ${f#"$FH"/} shares the card's exact mtime — ⑤'s strict comparison cannot see it"
229
+ done <<EOF
230
+ $(find "$FH/tracks/_meta" -maxdepth 1 -type f \( -name "fh_completed_*.md" -o -name "fh_signal_*.md" \) ! -newer "$CARD" 2>/dev/null)
231
+ EOF
232
+ # What a recurring tie would actually license (cross-family correction, 2026-08-02): NOT making ⑤'s
233
+ # comparison non-strict. Ties do not show the comparison is too strict — they show **mtime is not a
234
+ # reliable witness of close ordering on this machine**. A non-strict ⑤ would convert every same-tick
235
+ # healthy close into a false block, which is the failure mode ⑤-P exists to prevent. The fix that
236
+ # ties would justify is an explicit ordering record (the card writing a marker ①–④ can be compared
237
+ # against) or a deterministic close write sequence — not a looser comparison.
238
+ [ "$TIES" -gt 0 ] && echo "⚠️ ⑤ tie → $TIES artifact(s) ordering-ambiguous. If this recurs, mtime is not a sound ordering witness here; the fix is an explicit ordering record, NOT making ⑤ non-strict (that would false-block every healthy same-tick close)."
137
239
  else
138
240
  echo "❌ ⑤ session card missing: $CARD"
139
241
  FAIL=1
@@ -41,7 +41,7 @@ import json,sys
41
41
  try: d = json.load(sys.stdin)
42
42
  except Exception: sys.exit(0)
43
43
  if d.get("tool_name") != "Write": sys.exit(0)
44
- sys.stdout.write(d.get("tool_input", {}).get("file_path", "") or "")
44
+ sys.stdout.buffer.write((d.get("tool_input", {}).get("file_path", "") or "").encode("utf-8"))
45
45
  ' 2>/dev/null) || FILE=""
46
46
  [ -n "$FILE" ] || exit 0
47
47
 
@@ -68,13 +68,55 @@ MARKER="$MARKER_DIR/$(printf '%s' "$ROOT" | cksum | cut -d' ' -f1)-$(date +%Y%m%
68
68
  [ -e "$MARKER" ] && exit 0
69
69
 
70
70
  UPSTREAM=$(git -C "$ROOT" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || exit 0
71
- REMOTE="${UPSTREAM%%/*}" # fetch the remote @{u} actually tracks, not a hardcoded origin (GPT pass)
71
+ # Remote name resolution (leg-C LOW, 2026-08-01): `${UPSTREAM%%/*}` truncates a remote whose NAME
72
+ # carries `/` (git accepts them via config even though `git remote add` rejects them) — `a/b/main`
73
+ # became remote `a`, the fetch failed, and the guard went silently inert on exactly that clone.
74
+ # `%(upstream:remotename)` is git's own parse; the cut stays as fallback for git versions without it.
75
+ _HEAD_REF=$(git -C "$ROOT" symbolic-ref -q HEAD 2>/dev/null) || _HEAD_REF=""
76
+ REMOTE=""
77
+ [ -n "$_HEAD_REF" ] && REMOTE=$(git -C "$ROOT" for-each-ref --format='%(upstream:remotename)' "$_HEAD_REF" 2>/dev/null | head -1)
78
+ [ -n "$REMOTE" ] || REMOTE="${UPSTREAM%%/*}" # fallback; never a hardcoded origin (GPT pass)
72
79
  [ -n "$REMOTE" ] || exit 0
73
80
 
74
81
  if [ "${FH_STALE_CLONE_NO_FETCH:-0}" != "1" ]; then
75
- # Bounded fetch: GIT_TERMINAL_PROMPT=0 kills the credential-prompt hang class; the hook-level
76
- # timeout is the hard bound for wedged transports (macOS ships no GNU timeout).
77
- GIT_TERMINAL_PROMPT=0 git -C "$ROOT" fetch -q "$REMOTE" 2>/dev/null || exit 0
82
+ # Internally-bounded fetch (leg-C MED, 2026-08-01): the previous hard bound was the RUNNER's
83
+ # hook timeout (20s), which kills the whole guard preempting the exit-0 contract AND skipping
84
+ # the marker write, so a wedged transport re-stalled EVERY Write for the rest of the day
85
+ # (20s × N calls, a session-killer). The guard now owns its bound: fetch in the background,
86
+ # poll, and on expiry kill the fetch, ARM the day-throttle, exit 0. Arming on timeout is a
87
+ # deliberate trade: one silent day on a broken-network machine (fail-open, the documented
88
+ # degrade direction) beats a stall on every file creation. This is NOT the attempt-marker the
89
+ # GPT pass rejected — that armed on every early exit; this arms only after a full budget spent
90
+ # against a wedged transport. GIT_TERMINAL_PROMPT=0 still kills the credential-prompt class.
91
+ # Budget: tenths of a second; default 150 (15s) stays under the snippet's 20s runner timeout.
92
+ # CAPPED at 150 (terra round, 2026-08-01): an env-supplied budget > the runner timeout would
93
+ # restore the exact runner-preemption this bound exists to remove. (No boundary lane on purpose
94
+ # — it would idle its full 15s by construction; the cap is these three lines.)
95
+ # LENGTH-first, then value (terra round 2, 2026-08-01): an all-digit literal wider than the
96
+ # shell's integer width makes `[ "$x" -gt 150 ]` itself an ERROR ("integer expression expected" /
97
+ # "number truncated"), so the numeric cap never ran and the value stayed unnormalized. Digits are
98
+ # capped at 3 (max 999) before any arithmetic touches the value — safe in bash 3.2 and zsh alike.
99
+ _BUDGET="${FH_STALE_CLONE_FETCH_BUDGET_TENTHS:-150}"
100
+ case "$_BUDGET" in ''|*[!0-9]*) _BUDGET=150 ;; esac
101
+ [ "${#_BUDGET}" -gt 3 ] && _BUDGET=150
102
+ [ "$_BUDGET" -gt 150 ] && _BUDGET=150
103
+ # stdout AND stderr to /dev/null: the child inherits this hook's stdout pipe, and a
104
+ # still-running child holding it open would stall the hook runner past our own exit.
105
+ GIT_TERMINAL_PROMPT=0 git -C "$ROOT" fetch -q "$REMOTE" >/dev/null 2>&1 &
106
+ _FPID=$!
107
+ _t=0
108
+ while kill -0 "$_FPID" 2>/dev/null && [ "$_t" -lt "$_BUDGET" ]; do
109
+ sleep 0.1; _t=$((_t+1))
110
+ done
111
+ if kill -0 "$_FPID" 2>/dev/null; then
112
+ # No wait after the kill: a shell-script transport defers TERM until its foreground child
113
+ # exits (measured: the lane's wedge shim held a wait for the full 30s hang). The child is
114
+ # reparented at our exit; TERM is best-effort cleanup, the BOUND is the contract.
115
+ kill "$_FPID" 2>/dev/null || true
116
+ : > "$MARKER" 2>/dev/null || true
117
+ exit 0
118
+ fi
119
+ wait "$_FPID" 2>/dev/null || exit 0
78
120
  fi
79
121
 
80
122
  BEHIND=$(git -C "$ROOT" rev-list --count 'HEAD..@{u}' 2>/dev/null) || exit 0
@@ -85,7 +127,8 @@ case "$BEHIND" in ''|*[!0-9]*) exit 0 ;; esac
85
127
  MSG=" ⚠️ STALE-CLONE $ROOT is behind $UPSTREAM by $BEHIND commit(s).
86
128
  Building on a stale clone manufactures duplicates of work that already exists upstream
87
129
  (measured: a 46-PR-behind clone rebuilt an existing lane, 2026-07-31).
88
- Before creating new files here: git -C $ROOT pull --ff-only (or rebase your branch).
130
+ Advisory timing: this notice arrives with the Write already evaluated reconcile NOW,
131
+ before the next file: git -C '$ROOT' pull --ff-only (or rebase your branch).
89
132
  found→extend applies at repo level too — check what upstream already has."
90
133
 
91
134
  if json_out=$(printf '%s' "$MSG" | PYTHONIOENCODING=utf-8 python3 -c '
@@ -55,6 +55,27 @@ EOF
55
55
  echo " → run the shed/advance pass: re-check capability-compensating scaffolding against the new"
56
56
  echo " substrate (sonnet_floor_doctrine.md §durable-mechanization — shed what the model no longer"
57
57
  echo " needs, advance what the new substrate enables). Removals go through the 4-axis gate."
58
+ # Until 2026-08-02 the line above WAS the whole pass: an instruction with no instrument behind it, so
59
+ # nothing measured whether a rule still earned its residency. It now hands off to something runnable.
60
+ if [ ! -f "$FH/scripts/probe_scope_check.sh" ]; then
61
+ echo " (probe-scope check NOT run — scripts/probe_scope_check.sh absent. Expected in the npm"
62
+ echo " package, which ships neither the script nor its probe set; in a SOURCE checkout it is an"
63
+ echo " instrument gap, not a skip — selfcheck fails on exactly that state.)"
64
+ else
65
+ echo ""
66
+ echo " ── probe Scope resolution (bash scripts/probe_scope_check.sh) ──"
67
+ # Capture, THEN branch on rc — a pipe here would report the pipe's status, and a `sed` window keyed
68
+ # on a line the tool no longer prints would swallow the whole result silently.
69
+ _ps_out=$(bash "$FH/scripts/probe_scope_check.sh" 2>&1); _ps_rc=$?
70
+ if [ "$_ps_rc" -ne 0 ]; then
71
+ echo " ⚠️ probe-scope check FAILED (exit $_ps_rc) — a probe points at a section that is not there."
72
+ printf '%s\n' "$_ps_out" | sed 's/^/ /'
73
+ else
74
+ printf '%s\n' "$_ps_out" | grep -E 'control B|✅' | sed 's/^/ /'
75
+ fi
76
+ echo " This says the probe set still points at real sections — NOT how much of the asset is"
77
+ echo " defended. Coverage measurement is unsolved; do not read a green check as 'fully probed'."
78
+ fi
58
79
 
59
80
  printf '%s\n' "$CURRENT" > "$STATE"
60
81
  exit 0