feat: evict closed issues from the local store #26
Reference in New Issue
Block a user
Delete Branch "feat/evict-closed-issues"
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?
Closes #20.
What
skills/issue/scripts/issue_evict.py— one command, norm:It removes
<id>.mdand every sidecar under that slug (<id>.comments.mdtoday) for an issue that is
state: closedand carries anorigin:naming a tracker, then rebuilds
INDEX.md— and only when something actuallywent, so a no-op run writes nothing.
origin: local is never evicted
Two conditions, and the second one is the whole safety argument. An
origin: localissue is the work: there is no other copy, so it is kept inevery state, including closed and including when named on the command line —
reported with the reason, never removed. The only files that go are ones whose
own metadata says
pull.py <n>brings them back, which is the tradepush.pyalready makes when it drops a file the tracker just confirmed.
Which layer owns eviction
The domain (
skills/issue), as issue #20 argued it should: the two inputsare
state:andorigin:, both domain-owned fields, and the answer is alreadyon disk. No network, no login, no
tea. The domain layer stays stdlib-only —tests/test_store_path.py::TestLayeringstill passes.skills/sync/scripts/evict.pyis the bridge form, and it does not own thedecision: a local
state:is only as fresh as the last pull, so an issue closedin the web UI still reads
openhere. It refreshesstate:from Gitea and thencalls
issue_evict.run— one implementation of "what may be evicted", in thelayer that owns the fields it reads. Without it the observed workflow needs
pull.py 11 12 13 14 15first, which writes the five closed files back to diskbefore anything can remove them.
The gate, one step earlier than push's
evict.pyfetches every candidate's state before removing anything. Eachanswer must be an object carrying the number that was asked about and a state
the domain recognizes (
confirmed_state, written as the counterpart ofpush.confirmed_number). Ateathat will not run, a non-2xx, an answer foranother issue, an unknown state: the run stops and nothing is evicted — not even
the candidates whose answers had already arrived, and no refreshed
state:iswritten back either. A candidate is an issue with a
gitea:handle;origin: localhas none, is never asked about, and is never removed.One deviation from the acceptance criteria, stated deliberately
The criterion asks that
INDEX.mdand.remote.jsonmatch the directoryafter a cleanup.
INDEX.mddoes..remote.jsonis deliberately not pruned:since #19 it is the number → slug ledger, not an index over the files, and its
entries are supposed to outlive the files they name — that is what makes
pull.py <n>land on the same slug after a push deleted the file. An evictedissue is in exactly the state a pushed one is, so there is nothing to reconcile
and nothing to fix up by hand, which is what the criterion is after.
Docs
AGENTS.md— the rule the tracker side never wrote down: pull by numberfetches an issue in any state, a number is a number. Eviction does not
revoke it; a closed issue pulled after a cleanup is on disk again, and that is
the tracker answering what it was asked, not a regression. Plus the ownership
decision and the ledger rule above.
skills/issue/SKILL.md,skills/sync/SKILL.md— command tables and a sectioneach.
skills/issue/references/format.md— theorigin:table gains a "whateviction does to it" column.
agents/tea-runner.md— its "delete nothing" rule now names the two localdeletions it may cause, and requires
--dry-runfirst.Tests
tests/test_evict_closed.py, 51 cases, stdlibunittest, temp stores, nonetwork anywhere. The dangerous half is asserted path by path:
origin: localkept (unnamed, named, with sidecars), open issues kept,
--dry-runwritingnothing including
INDEX.md, and every tracker failure leaving the whole storeon disk — a raised transport, a real non-2xx through
_gitea.apiwithteaexiting 1, an answer for another issue, an answer with no state, an empty
answer.