Run one scheduled codebase-maintenance job (dead code, useless tests, stale flags, duplicate abstractions, coverage gaps, docs freshness, flaky tests, dependency bumps, tracker reconciliation) as an unattended read-only sweep that ends in a sanitized issue draft awaiting approval. Use when an automation or user asks to run a named maintenance job for this repository.
Resources
2Install
npx skillscat add picoduck/wollipog/codebase-maintenance Install via the SkillsCat registry.
Codebase Maintenance
Each job is a narrow, evidence-anchored sweep that runs on a schedule with no human watching it
start. The prompt that launches a job is one sentence; this file and the job file are the harness
that make the result trustworthy.
Read jobs/<job-id>.md for the job you were asked to run. That file defines the scope, the tools
that produce ground truth, and the gate a finding must pass. This file defines everything the jobs
share.
Phase 1: Report Only
Every job currently runs in report-only mode. A job investigates and drafts; it never changes the
repository and never publishes.
Hard rules:
Do not edit, create, or delete any file in the workspace. Do not commit, branch, stage, stash,
push, or open a pull request.Do not run installs, migrations, formatters, code generators, or any command whose purpose is to
change the tree. Analysis tooling that writes only to caches,node_modules/.cache, or the
scratch directory below is allowed. One exception: the preflight install in the ground-truth
section below —pnpm install --frozen-lockfilecannot change anything git tracks (it refuses
to run rather than rewrite the lockfile, andnode_modulesis ignored), and it may run only
there, before the first test or analysis command. Put every scratch file under one run-scoped directory,~/.cache/wollipog-maintenance/<job-id>-<YYYY-MM-DD>/(create it withmkdir -p) — date-keyed
rather than random, so a resumed run can re-derive its own path. Two hard-won properties of
this location: it survives a reboot, unlike/tmp, which this machine wipes at boot — so the
raw evidence behind a report stays inspectable until the report is reviewed; and it is
namespaced per run, because a session that loses its conversation context can otherwise find
another session's files and mistake them for its own — which happened, and turned one lost
session into a false "already published" claim. At the start of each run, delete this job's
own scratch directories older than 30 days, as a standalone command scoped to the job's
prefix —find ~/.cache/wollipog-maintenance -mindepth 1 -maxdepth 1 -type d -name '<job-id>-*' -mtime +30 -exec rm -rf {} +— never a sweep of the whole location chained onto
other commands. Each job cleans only what it wrote; nothing else cleans this location. The
auto-mode classifier has denied the location-wide sweep as a "shared scratch sweep" while
passing the identical command the week before; if the scoped prune is still denied, list what
would have been removed, record it as anenvironmentitem, and continue.Before finishing, run
git status --porcelainandgit stash list. The working tree must be
exactly as you found it. If anything changed, say so explicitly at the top of your report and
name the files rather than quietly reverting.Do not publish a GitHub issue. Drafting and publication are separated by the gate in
.github/ISSUE_REPORTING.md, and an unattended run cannot satisfy it.The agent memory directory outside the repository is shared by every session on this machine
and is not part of the workspace, so rule 1 does not cover it. You MAY correct a memory there,
but only one this run has verified stale against the tracker or the code, only by recording
the verified fact (never speculation or planning notes), and only with a one-line disclosure in
the report naming the memory and the correction. A stale memory left standing is how a
reconciled issue gets filed a third time; an undisclosed edit is how a shared memory drifts.Keep every Bash call a single, inspectable command line. Anything multi-line or looping — a
heredoc, a multi-linepython3 -c, afor … do … doneover shell-variable paths — goes into
a script file in the run's scratch directory (write it with the file tool) and runs from
there. The managed-worktree guard cannot inspect those shapes and refuses them with a message
that blames runner-owned guard state (#1632); sweeps on 2026-09-23 and 2026-09-28 each lost a
retry to it. The rule applies to the sweep itself, not only to follow-up work.
Resolve a scratch script's directory to an absolute path before anycdin the script:scratch_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" || exit 1.
Use that absolute directory for every input and output, including copied scripts. Resolvingdirname "$0"after changing directories can redirect scratch output into the repository
when the script was launched through a relative path.Follow-up work in this session. A human may reply to the report with "publish", then "claim
and fix" — that is their call, and it converts this session into a fixing session for the
rest of its life. Three things must happen before any fix work starts:- Rule 6 applies here too: loops and multi-line scripts go in scratch-directory files.
- Never print the process environment to find anything — no
env,printenv,set, orcat /proc/self/environ, filtered or not. Agent sessions inherit the runner's environment,
and on 2026-09-17 anenv | grep -i tokenwrote the live runner credential into a session
transcript. The budget is stated in the prompt or readable through theget_sessiontool;
a value that is in neither is not yours to look for. - State the remaining budget first, on its own line: this session's
costBudgetUsdcaps the
WHOLE session, sweep and follow-up alike, and a sweep budget is not sized for a fix. "$6.33
of $8.00 remaining" at the top of the turn is what lets the human raise it before the
guardrail pauses a half-finished fix (which is how a session was stranded once). - Attach a control-plane-tracked worktree with
wollipog worktree createbefore touching any
file — never a rawgit worktree add. The control plane must know the worktree: that is
what makes "Hand Off to Another Agent" available for the rest of the session, what the
hygiene job'swollipog worktree discardretirement assumes, and what the worktree-per-issue
rule requires. (One session used raw git for its fix; its hand-off control stayed disabled
and its worktree was invisible to the control plane.)
Promotion to Phase 2 (opening pull requests) is a deliberate change, not a judgment call a job may
make on its own. See "Promotion Criteria" below.
Anchor Every Finding in Ground Truth
Start with timeout 60 git fetch origin main (it touches only .git refs, never the working
tree). The origin remote is SSH, and port 22 to GitHub intermittently hangs on this machine
rather than failing, even in batch mode. If the fetch times out, fetch the same ref over HTTPS
with the GitHub CLI's credentials and say so in Tree State:git -c credential.helper='!gh auth git-credential' fetch https://github.com/picoduck/wollipog.git +refs/heads/main:refs/remotes/origin/main.
Then run git rev-list --left-right --count HEAD...origin/main. If HEAD is behind, say so at the top of
the report and evaluate every candidate against origin/main — git show origin/main:<path>,git diff --stat HEAD origin/main — so a file deleted or rewritten upstream is dropped, not
reported. One sweep ran two commits behind and produced three findings in a file the tip ofmain no longer contained; the "differs from origin/main" exclusion also silently removed
twelve files that had merely moved on. The primary checkout is not fast-forwarded on a schedule,
and Phase 1 forbids this run from pulling it.
Check install freshness before any job that executes code (tests, coverage, knip, type checks):
compare the installed lockfile with the checkout's wanted lockfile usingcmp -s node_modules/.pnpm/lock.yaml pnpm-lock.yaml. A missing installed lockfile or a content
mismatch requires pnpm install --frozen-lockfile --prefer-offline as preflight, before the first
test or analysis command. Do not use an install timestamp versus a lockfile commit timestamp:
a checkout can receive an older-dated lockfile commit after its last install, so that comparison
can call a stale install fresh. Record the content/version mismatch and packages linked in Tree
State. Recheck the lockfile comparison after installation; an unexplained remaining mismatch or
an unreadable lockfile is an environment item, not evidence that the install is fresh.
This is the only install Phase 1 permits, and only at this point: never after a test or analysis
run has started, and never in response to HEAD moving mid-sweep (that case is handled by re-running
at the new baseline, not by reinstalling under a running suite). Do not change a shared install
while another test or install command is using it. If the install fails, or if ERR_MODULE_NOT_FOUND
/ "Cannot find package" failures still appear, treat them as environmental: they are not broken
tests and not findings, and they go in Recommended Actions as one environment item naming the
missing package and the install error. A stale installed package version or missing required patch
is handled the same way. One flaky-test sweep ran the unit suite three times against shell-quote
1.9.0 after the checkout required 1.11.0; content comparison catches that mismatch before testing.
A finding that rests only on reading code is a guess. Each job file names the tool that proves its
category — static analysis, coverage data, test runs, git log, gh. Run it, and cite what it
returned.
For every finding, record:
- the exact file and line range;
- the tool output or command that demonstrates it, not a paraphrase;
- what you checked to rule out the obvious false positive named in the job file.
Prefer reporting three findings you verified over twenty you inferred. A job that reports nothing
has succeeded, and is far more useful than one that manufactures work.
Do Not Fight Work in Progress
Skip anything that a human is already touching:
- files changed by an open pull request (
gh pr list --state openthengh pr diff); - files with uncommitted changes or that differ from
origin/main; - LINES added or modified in the last 7 days, which are usually deliberate and unfinished rather
than abandoned. Judge this per finding withgit blameon the exact candidate lines, not per
file: in an active repository,git log --since='7 days ago' --name-onlyexcludes most of the
tree because unrelated lines in the same files moved. (The first two runs of these jobs each
hit that independently — one lost 363 files, the other 28 of 76 documents — and both fell back
to per-line blame; that fallback is now the rule.) The file-level listing remains useful as the
cheap first pass for spotting where recent work is concentrated.
Say what you excluded and why. Silent exclusion reads as coverage you did not have.
Do Not Re-Report
These jobs run every week and will rediscover the same things.
- Search existing issues before drafting:
gh issue list --label maintenance --state all --limit 100,
plus a term search over open and closed issues for the specific symbol, file, or dependency. - If an open issue already covers a finding, do not draft a new one. Note the issue number and
move on. Add materially new evidence to your report only when the evidence changes the picture. - If a closed issue rejected a finding, treat that as a decision. Do not resurface it unless the
surrounding code changed since the issue closed, and say what changed.
Persist Candidate Verdicts
Before finishing, write <scratch>/verdicts.md in this job's own run-scoped scratch directory.
Record the baseline commit, each examined candidate and its code or evidence references, the
verdict (verified, already_tracked, dropped, or deferred), its reason, and any issue or PR
reference. Include the exact issue drafts and recommended actions from the final report so later
runs can compare pending recommendations without relying on session-list access.
Use only sanitized content. This file records findings and recommendations, not publication or
authorization. When considering an earlier verdict, recheck its code and tracker references; a
scratch draft never proves an issue was published and is never approval to publish it.
Scope to the Budget
Every automation has a cost and tool-call ceiling. If the full sweep will not fit, narrow it
deliberately — one package, one directory, the highest-signal slice — and state plainly which parts
of the codebase you did not examine. Never let a partial sweep read as a complete one.
Output
End with a single report. Structure it as:
- Job and scope — which job ran, what it covered, what it deliberately skipped.
- Tree state — the result of the
git status --porcelaincheck. - Findings — each with its evidence, or an explicit "no findings" line.
- Already tracked — findings suppressed as duplicates, with issue numbers.
- Issue drafts — for genuinely new findings, the exact sanitized title, body, and labels,
following the format and public-content rules in.github/ISSUE_REPORTING.md. Use themaintenancelabel plusbugorenhancement. Title each draftMaintenance: <summary>. - Recommended actions — a ranked decision list, one item per line, each traceable to a
finding above or to a concrete friction this run hit. Never speculative improvements: if the
run did not experience it or prove it, it does not belong here. Type every item so the reader
knows who can execute it on approval:publish— publish draft N. Executable in this session by replying with approval.amend-skill— change this skill or a job file (a broken command, a rule the run had to
override, a false-positive class worth naming). NOT executable here: Phase 1 forbids this
session from editing the tree, so approval hands it to a separate session.adjust-automation— change this job's schedule, budget, or config. Also not executable
here; state the exact proposed values.environment— machine-level changes (a missing tool, a stale service). Not executable
here; name the command or change precisely.tracker— comment on or close an existing issue this run's evidence affects, citing the
issue number.
Deduplicate before listing: an action already covered by an open issue, an open pull request,
or a previous run's still-pending recommendation is noted with its reference, not repeated as
new. Writing "No recommended actions" is a valid and often correct entry; omitting the
section is not. The reader's approve/reject decisions on these items are the acceptance
record the Promotion Criteria below are judged against, so a padded list damages exactly the
record it is trying to build.
Present each draft using the Approval Preview Formatting rules in .github/ISSUE_REPORTING.md.
Never wrap a body that may contain code fences in a fixed three-backtick fence.
Then stop. Publishing happens only when a human replies with approval; at that point follow.agents/skills/log-github-issue/SKILL.md to create and verify the issue.
Sanitize before drafting, not after. These runs read a local checkout on a personal machine, so
home-directory paths, hostnames, and device names will appear in tool output. Replace them with<private-path> and <host>.
Promotion Criteria
A job may move to Phase 2 only after its category has a sustained record: findings from at least two
weeks of runs reviewed, and roughly 70% or more accepted as real. Below that, the job is a noise
source and should be retuned or disabled instead of promoted.
Phase 2 adds, per job: an isolated worktree, one concern per pull request, a diff cap of about
150-300 lines, the full test suite green, cross-model review through the cross-model-review skill for
convergence jobs, and the Definition of Done from the issue-workflow skill. No job auto-merges.
The Deletion and Generation Jobs Must Agree
useless-test-deletion removes tests and coverage-gap-fill adds them. They share one definition of
a worthwhile test, stated in both job files: a test earns its place only if it would fail when the
behavior it covers is broken. If you find yourself proposing a test that the other job would delete,
or deleting one that the other would recreate, report the conflict instead of acting on it.