FR-4.5 stability gate silently degrades to "unchecked" when the snapshot store has no history for the target #17
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The FR-4.5 evidence-stability gate depends entirely on the local snapshot store having prior history for the target, and there is no safeguard for the case where that history is missing. When it is missing the gate does not warn — it reports
unchecked — no prior window to compareand every candidate files normally.Observed on the 2026-07-30 anxious run:
--store, and were never persisted to the default store (~/.cache/inference-arbitrage/wiki) nor published to the Gitea wiki. The wiki had noData/anxious/orAudits/anxious/pages at all.bin/stability-classifyreturnedsnapshots_available: 0,checked: 0,downgraded: 0, andanxious/agent-wip/release-policy-derivationcame backuncheckedwith verdictfile. It would have been filed on the target's repo.audit-snapshot importfrom those real prior runs) did the gate fire and correctly downgrade it — the instability tracked as kotkan/claude-plugin-inference-arbitrage#15.So the gate that exists to stop window-dependent evidence reaching a tracker is a no-op exactly when history is absent, and absence is indistinguishable (to the gate's output) from "checked and stable". The skill documents seeding as a manual step conditional on the operator noticing the store is cold, which makes the strongest gate in the pipeline dependent on operator vigilance.
Worth considering:
offload-auditstep 4b could refuse to proceed to filing whensnapshots_available == 0and the wiki has pages for the target — i.e. detect the cold-store-but-history-exists case mechanically rather than asking the operator to spot it.--storerun (dev/acceptance) produces real history that is then invisible to production runs; that asymmetry is the root cause here.Note this is distinct from #15: #15 records that one candidate's evidence was unstable. This issue is that the gate meant to catch such candidates can silently not run at all.
Investigation: kotkan/claude-plugin-inference-arbitrage#17
Verdict: real, fixable bug. Root cause confirmed, not yet fixed by any commit made today.
What's actually happening (root cause)
bin/stability-classifybehaves correctly for what it can see:gate()inbin/stability-classify(around line 142) treatsnot historyasUNCHECKED_NO_HISTORYand lets the candidate file. That is deliberatelycorrect for a genuine first-ever audit (FR-2.4) -- see test case (c) in
tests/stability.test.sh, which asserts exactly this and passes today.The bug is one level up: the script only ever reads the local filesystem
store (
--store, default~/.cache/inference-arbitrage/wiki) -- by design,per
bin/audit-snapshot's module docstring, this plugin holds no wikicredentials and makes no MCP call from any
bin/script. The durablesource of truth is the Gitea wiki (
Data/<target>/snapshots.jsonl, seecmd_pages/cmd_importinbin/audit-snapshot), and nothing in thepipeline keeps the local store synced with it before a gate run.
skills/offload-audit/SKILL.mdstep 4b (lines 125-132) documents this gapbut only as a conditional, manual aside:
That's exactly the "operator vigilance" dependency the issue describes --
nothing forces the check, and
stability-classify's output for "genuinely nohistory anywhere" and "local cache is cold but the wiki has real history" is
byte-for-byte identical (
unchecked -- no prior window to compare,snapshots_available: 0). A downstream reader (human oroffload-auditorchestration) cannot tell the two apart from the gate's own output.
Evidence
This is the exact code path a cold-but-not-actually-empty store falls into --
confirmed by re-reading
bin/stability-classifylines 132-144 (gate()),which branches on
if not historywith no distinction for why history isempty.
Only mention in the whole skill -- confirms the seeding step is documented as
conditional/manual, not enforced anywhere in the 7-step orchestration (steps
1-4b are otherwise all scripts or explicit "never do by hand" instructions).
Is the issue body still accurate / was it touched by today's other fixes?
Checked
git login this repo: two commits landed after this issue wasfiled (03:40:34+03:00) --
6b634fd(04:35:42, "audit-snapshot: fix wrong decline reason for astability-downgraded candidate") -- fixes kotkan/claude-plugin-inference-arbitrage#16,
a different bug (
decline_reason()mis-attributing why a candidate wasdeclined). It touches
bin/audit-snapshot'sdecline_reason/render_memory_judgmentsonly -- does not touchbin/stability-classifyorthe cold-store detection logic at all.
ba445cf(04:43:02, "Phase 8: publish") -- README/publish-scope only.So this issue's diagnosis is not stale and was not incidentally fixed.
bin/stability-classifyis byte-identical in behavior to what the issuedescribes;
SKILL.mdstep 4b is unchanged. Confirmed by running the currenttest suite (
tests/run-all.sh) -- all existing cases pass, including theFR-2.4 "no history" case, which is the exact ambiguous state this issue is
about.
Proposed fix (plan, not yet implemented)
The correct fix is procedural + a small backstop in the script, not a change
to
gate()'s FR-2.4 behavior (which is right for genuine first audits):skills/offload-audit/SKILL.mdstep 4b: promote the wiki-seed stepfrom a conditional aside ("if the store is cold... seed it first") to an
unconditional, mandatory pre-step, run every audit regardless of
whether the local store looks populated:
wiki_read(viacluster:gitea-agent) the pageData/<target>/snapshots.jsonlfor the target.$IA/bin/audit-snapshot import --target <name> --snapshots <fetched file>beforestability-classifyruns.importalready dedupes by
run_id(seetests/memory.test.sh"re-importskipped" case), so running it unconditionally on every audit is safe and
idempotent -- no conditional judgment required from the operator.
unchecked -- no prior window to compareactually trustworthy.Backstop in
bin/stability-classify(optional but recommended,makes the fix testable/mechanical rather than doc-only): add a required
--wiki-checked {empty,imported}flag. If omitted whilehistoryisempty, exit non-zero with an explicit error instead of silently emitting
unchecked. This forces whatever orchestration calls the gate (SKILL.md'sstep 4b, or any future caller) to make an explicit, recorded attestation
instead of defaulting silently -- consistent with how
boundary-classify'sFR-4.2 hard gates already work in this plugin (mechanical, not advisory).
Add regression cases to
tests/stability.test.sh: (g) cold store, no--wiki-checked-> script errors; (h) cold store,--wiki-checked=empty-> unchecked as today; (i) cold store,
--wiki-checked=importedafter areal
import-> gate runs against the imported history normally.Bump
plugin.jsonversion (patch, per this repo's convention) and recordthe fix the same way
6b634fdrecorded kotkan/claude-plugin-inference-arbitrage#16in its commit body.
This is a real, actionable code+docs fix, distinct from kotkan/claude-plugin-inference-arbitrage#15
(which is a measurement/evidence question about one candidate, not a defect
in this gate).
Fixed in kotkan/claude-plugin-inference-arbitrage#19 (merged into
main).Root cause
bin/stability-classify'sunchecked — no prior window to comparestate is correct FR-2.4 behavior for a genuine first audit, but the script only ever reads the local filesystem snapshot store — never the wiki, by design (nobin/script holds wiki credentials).skills/offload-audit/SKILL.mdstep 4b documented seeding the local store from the wiki (Data/<target>/snapshots.jsonl) as a conditional, manual aside ("if the store is cold... seed it first") rather than an enforced step. Because the gate's output is identical whether history is genuinely absent everywhere or merely absent from a cold local cache, a cold-but-not-actually-empty store silently degraded to "unchecked" and filed unstable candidates — exactly as observed on the 2026-07-30 anxious run. Not stale or misdiagnosed — matches what I found in the code.Fix
skills/offload-audit/SKILL.mdstep 4b now makes fetching the wiki page mandatory, every run —wiki_read Data/<target>/snapshots.jsonlviacluster:gitea-agent, thenbin/audit-snapshot importif it exists — beforestability-classifyruns.importdedupes byrun_idso this is safe/idempotent to always run.bin/stability-classifynow takes a required--wiki-checked {empty,imported}flag and hard-errors when the local store is empty for the target and the flag is omitted, instead of silently emittingunchecked. This makes the safeguard mechanical rather than doc-only.plugin.jsonbumped 0.6.2 → 0.6.3.Evidence — commands and actual output
New regression cases (g)/(h)/(i) added to
tests/stability.test.sh, run against the fixed script:Full local test run (
for f in tests/*.test.sh; do bash "$f"; done) — every one ofclassify.test.sh,filing.test.sh,inventory.test.sh,memory.test.sh,scan.test.sh,snapshot.test.sh,stability.test.shexits 0, including the pre-existing cases (a)–(f).Concretely, this is what used to be silently possible and now is not: running
stability-classifyagainst a target with an empty local--storeand no--wiki-checkedflag (the exact "cold cache, wiki has real history" scenario from this issue) now refuses:PR: kotkan/claude-plugin-inference-arbitrage#19 (merged).
Fixed via PR #19 (commit
69ff443+713469d, merged) — bin/stability-classify now requires an explicit --wiki-checked {empty,imported} flag and hard-errors instead of silently emitting 'unchecked' when the local store has no history for the target; skills/offload-audit/SKILL.md step 4b now makes the wiki history check an unconditional mandatory pre-step rather than a doc-only aside. Verified: full test suite passes on current origin/main, including new regression cases (g)/(h)/(i) in tests/stability.test.sh.