picoduck

codebase-maintenance

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.

picoduck 3 4 Updated 2w ago

Resources

2
GitHub

Install

npx skillscat add picoduck/wollipog/codebase-maintenance

Install via the SkillsCat registry.

SKILL.md

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:

  1. Do not edit, create, or delete any file in the workspace. Do not commit, branch, stage, stash,
    push, or open a pull request.

  2. 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-lockfile cannot change anything git tracks (it refuses
    to run rather than rewrite the lockfile, and node_modules is 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 with mkdir -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 an environment item, and continue.

  3. Before finishing, run git status --porcelain and git 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.

  4. 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.

  5. 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.

  6. Keep every Bash call a single, inspectable command line. Anything multi-line or looping — a
    heredoc, a multi-line python3 -c, a for … do … done over 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 any cd in the script:
    scratch_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" || exit 1.
    Use that absolute directory for every input and output, including copied scripts. Resolving
    dirname "$0" after changing directories can redirect scratch output into the repository
    when the script was launched through a relative path.

  7. 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, or
      cat /proc/self/environ, filtered or not. Agent sessions inherit the runner's environment,
      and on 2026-09-17 an env | grep -i token wrote the live runner credential into a session
      transcript. The budget is stated in the prompt or readable through the get_session tool;
      a value that is in neither is not yours to look for.
    • State the remaining budget first, on its own line: this session's costBudgetUsd caps 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 create before touching any
      file — never a raw git 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's wollipog worktree discard retirement 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 of
main 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 using
cmp -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 open then gh 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 with git blame on the exact candidate lines, not per
    file: in an active repository, git log --since='7 days ago' --name-only excludes 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.

  1. 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.
  2. 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.
  3. 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:

  1. Job and scope — which job ran, what it covered, what it deliberately skipped.
  2. Tree state — the result of the git status --porcelain check.
  3. Findings — each with its evidence, or an explicit "no findings" line.
  4. Already tracked — findings suppressed as duplicates, with issue numbers.
  5. 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 the
    maintenance label plus bug or enhancement. Title each draft Maintenance: <summary>.
  6. 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.