Simplify
This file is /simplify in this repository. An operator's own skill pack may hold another
/simplify; where the two disagree, this one governs here. The method is reading: why the code
exists, which owner already does its job, what can go. reduce-complexity
is the instrument and owns the two scripts below, and commit-r owns a
rewrite from scratch.
First
- Ask why the code exists before asking how to shrink it. Read the code, the function around
it, every caller, the owning test and the head comment, which says what defect the code answers
(
AGENTS.md, section 13). Most answers lie outside the diff: an owner that already does the job,
a helper with one caller, a field nothing reads, a guard an earlier refusal makes impossible.
- Fix it at the highest layer that already owns it. "Can't we go one layer higher?" is the
operator's most expensive correction. Before editing a leaf, name the owner that already holds
the decision, the reading or the refusal.
- Budgets are hard. The defaults are below; a budget the operator states ("max 5 prod LoC",
"remove one import to add one") replaces them. Measure it; do not estimate it.
- Land on the pull request it simplifies, as a commit there when the pull request is your own.
Another session's pull request is never yours to change (stack-pr).
You are improving code that works. A correctness bug found on the way is fixed under the task's
authority and reported apart.
Read the invocation
The scope comes from the arguments: pasted code, a worktree, "uncommitted diff", a commit, a pull
request, a range or paths. Nothing named means your own working diff. The wording decides the
procedure.
| The operator wrote | Do this |
|---|
| "remove X and /simplify", "sloppy, remove and find another way" | The deletion is decided. Follow the whole strand (below), and do not come back with a smaller X |
| "rewrite from scratch", "completely remove and rewrite", "overkill" | commit-r |
| "why do we even need X?" | Answer with the owner that already covers it, or the root cause X patches. Code comes after that answer |
| "one layer higher" | Revert the leaf edit. Find the one owner every path runs through and fix it there |
| "find a reference in Bun or another well-known repository" | Read how that project settles it, cite the file, then fit that form into the existing owner |
| "is it simple enough?" | The verdict first. "Yes" is a whole answer; otherwise show the folded form |
| "what looks most bloated?" | Rank by concepts a reader must learn, from measure.py and the census |
a bare /simplify after your own change | Your diff is the scope, read as if a colleague wrote it |
Work inline, and use subagents only when the operator asks. A named boundary ("keep these
owners") is a boundary, and a renamed shim is the forbidden thing itself. When the boundary blocks
the only real cut, say so once and stop.
Following a removal. Each caller goes too or gets simpler now that the value cannot occur. A
field only X set leaves its type. A guard that existed because X could yield nothing goes. A test
of X alone goes, and a test that pinned other behaviour through X is given another fixture. A
reader whose only producer went is the next finding. Where X patched a failure, the removal
reopens it: fix it at its owner, or say plainly that it is open. Moving a helper's body into three
callers is no removal.
What the repository already finds
Read these before reading by hand. Each was a shape this skill kept finding by eye.
bun run lint -- --strict: the ana and anti-slop plugins are the catchers that decide from
the syntax alone (tools/oxlint/HABITS.md, 12.4 and 12.5). Fix the site or tune the rule; never
turn one off (AGENTS.md, section 12).
bun run simplify: the whole-tree scans, which relate two files and which no per-file rule can
express (HABITS 12.9): orphan and test-only modules and exports, single-reader exports, copied
blocks, unread fields, compatibility paths. bun run commit:check prints the ones on your
changed lines. Each row is a question.
bun run not-slop -- answer <id> "<a reason a reviewer can check>" answers a site that is
right as it stands, and nothing reports it again until its code changes. -- prune drops the
answers whose site went.
bun tools/loc/source-policy.ts: 800 nonblank lines a file and 115 a function over src,
tools and vendor, and every export with a reader in another file. No CI job runs it, so run
it yourself and add no finding; it does not read .claude, where the census does.
Measure before and after
Run reduce-complexity's scope.sh and measure.py (its section "One diff") once before editing
and once after, and put the budget lines in the report: production and test lines net, new
functions net of moved ones, the names each import source gained. Its
repeats section is added code that already exists in tracked source: call that owner, and
delete the other copies of the same reader too. For a pull request or a stack, give the structural
rows of stack-complexity.py beside the lines (decision points, import edges, exported symbols,
optional fields): a pass that moves lines and none of those has moved mass.
The ladder
Climb it for each mechanism, and stop at the first rung that holds.
- Does it need to exist? Map it as rule 13 asks,
owner → live reader → decision it changes → evidence → hostile test, and drop it when a link is
missing. The Diagnoser named the first wrong step of each failed practice solve; 20 of 20 of its
readings named the solver, and it went with 1,063 lines (docs/simplify.md, E1).
- Does a higher layer already do it? The launcher's refusals were asked again by each move's
own command (
src/builder/admission.ts), and the campaign command already waited for the host's
load, so bun run runs launch went: 1,363 lines out, 610 in (docs/simplify.md, D2). The verb
that came back on 15 October starts the command and asks none of them.
- Is it already in this repository? Add no second reader of one fact: find the fold that reads
it and call that (
AGENTS.md, section 5). measure.py's repeats name candidates.
- The platform, Bun or an installed dependency? Every runtime surface has one owner in
src/meta (HABITS 12.8). Never add a dependency for a few lines.
- Can it be one expression? Two arms that differ in one term are one arm
(
ana/no-arms-differing-in-one-term); an object literal of three or more properties spelled
three times is one function (HABITS 12.3).
- Only then the least that works, inside the owner.
At each rung, ask whether the root cause was fixed or the result patched, and how much of the old
behaviour the minimum must keep: prefer the form with about half the machinery that keeps nearly
all of the result, measured on both sides, with measure.py for the mechanism and a replay or the
decision the code feeds for what it still does.
v2's own review named four forms its growth took (docs/simplify.md, section 5), and each is a
rung-1 or rung-2 question: two generations of one mechanism standing side by side; a weak seat
compensated by more rows in the controller instead of being mended at the seat; an incident
written into the code as a date and a branch; and proof machinery carried from v1 for a threat v2
does not have.
Budgets
- A repair adds at most five production lines, one new function and no new module, and a pure
simplification nets zero production lines or fewer, unless the operator states another.
- A file keeps its count of names imported from
src/meta/filesystem.ts, and a change adds at most
one (filesystem-import-budget, HABITS 12.9).
- A ceiling is met with fewer branches in the owner, then a closure, then a function. Helpers with
one use spread the same count over more places. A sibling module that lets new code sit outside
the 800-line count is refused, and
tools/loc/source-policy.json stays empty. v2 has no
complexity gate: measure.py's branch count over 21 is a lead, never a refusal.
When the change is a producer, replay it
Code that writes events, rows, reports or prompts is measured on recorded input. A reader of the
log is put through every recorded log with bun run runs replay, and its output is counted before
and after. Label a count taken any other way an estimate, and say which numbers were replayed.
What a pass must not cut
The rules of AGENTS.md section 8 are what no pass trades. A measure that improved because one of
them left is no simplification.
- The wall: a change to a hidden value, a check or which held-out task was won leaves every message
a Builder or a solver reads byte-identical (rule 3,
test/builder.test.ts).
- Terms logged before the first solve, both sides solved afresh, a non-result typed and never a
pass or a fail, a rejection counted only once ruled (rules 4, 5, 7 and 8).
- The log: no second store, no second reader of one fact, no event written by hand, and an old
event kind still readable (section 5).
null for unknown is a state, never a zero.
- Code owns the conditions and models own the content (section 2): never put a classifier in code
where a model decides because the reasoning is hard.
- A head comment that names the defect, the record and the date stays, and changes with its
mechanism.
vendor/ is a copy of upstream and not reshaped. .ana/, a pack, a check program and a kept
harness are never edited (section 11).
Keep, and not a finding
- A
null that means unknown, and a re-read that measures another moment.
- Two readers of one file with different contracts, where one wants nulls and the other refusals.
- A helper with one caller, where the caller would otherwise carry a branch the name explains.
- Working code already close to its smallest honest form. Say "already the smallest honest form"
when it is, and name the one place that came closest to changing.
Finish
- While editing, run the owning tests only:
bun run test -- <paths>. Before the commit,
bun run commit:check. Before delivery, the gate CI runs (AGENTS.md, section 11), once, with
every exit code read; a run that printed nothing is not a pass.
- Report with the outcome first, in one sentence: what got simpler, and the commit. Then the budget
lines, what the simpler form gave up and how that was measured (or that it gave up nothing),
what stayed and why, and the test counts.
When the operator corrects a pass, replace the rule that misled you here. Do not append a
checklist item.