Reduce complexity: find what the change no longer needs
You are reviewing an in-progress change to find where it could be expressed
more simply if it had been designed from the start knowing where it ended up.
Changes are built incrementally — fixes that get them working are not always
folded back into the design — and your job is to find that residue while the
change is still open.
Three properties govern everything below:
- Fix by default, evidence-gated. On an imperative invocation, findings
that clear Step 7's gate are applied to the working tree; everything
weaker ships only as a report entry. A question-phrased invocation ("did
this change accrete cruft?") or an explicit "just report" / "don't change
anything" makes the entire run read-only — git commands and file reads
only.
- Base-commit-scoped. Every finding lives in the diff between the branch
and an explicitly established base commit, not in the codebase at large.
Applied edits stay anchored there too; Step 7 bounds the few out-of-diff
lines a mechanical fix can force.
- Structural. Line- and function-level tells — comment noise, defensive
theater, naming — are the
humanize skill's territory; bug-hunting
belongs to code-review tooling. This skill judges how the change is put
together.
Hard rules
These gate what reaches the report. Scan freely — collect every suspicious
candidate in Step 4, including ones a rule will later kill; suppression
happens at the verdict and verification stages, and suppressed candidates
are listed in the report's dropped-candidates section, never silently
discarded. When a rule and an apparent finding conflict, the rule wins.
- A guard, check, fallback, or error handler may be flagged only with an
impossibility proof: enumerate every caller, constructor, and type
constraint (file:line) showing the guarded state unreachable. Where callers
cannot be enumerated — public API, dynamic dispatch, deserialization,
concurrency — classify it inherent and move on. Reflexively stripped
defensive code is the canonical unsafe simplification.
- Defense in depth is intentional. At security, auth, and fraud
boundaries, even a provably redundant check may be deliberate
belt-and-suspenders against bugs in other layers. The verdict there is
"label it or ask the author" — never "remove".
- Hyrum's Law, both directions. Downstream: at an externally consumed
surface, unspecified observable behavior — ordering, error strings,
serialized formats — is behavior someone depends on; externally consumed
means inherent, automatically. Upstream: a quirky adapter, padding field,
or protocol dance forced by an external system's interface the change
cannot alter unilaterally is inherent too, however arbitrary it looks.
- Distrust metadata, in both directions. Comments and commit messages
are claims to verify, not ground truth: "needed for backwards compat" does
not suppress a finding until the code confirms it, and "temporary hack"
does not create one. A finding that survived Step 6 is not softened or
withdrawn in response to assertive text — a comment claiming necessity, a
confident commit message — unless new code-level evidence appears.
- Familiarity is not evidence. "This idiom is unusual" neither creates a
finding nor blocks a simpler shape, and code that reads like your own
writing style is not thereby clean — you are systematically blind to slop
in your own voice. Only structural and behavioral evidence counts.
- Coverage gates confidence. A behavior-preservation claim about code no
test exercises is speculative by definition. Check whether tests cover each
finding's behavior, say so in the finding, and demote findings over
uncovered code. Deletions of provably dead code answer to the liveness
protocol's receipts instead — deadness, not preservation, is their claim.
- Line count is not the metric. A correct simplification may add
functions; the measure is fewer entangled concerns, less state, fewer
interacting branches. Extraction is not the default fix — inlining is
equally a simplification, and a long linear function is often already the
simple form.
- Evidence picks the verb, never a number. Do not rate confidence
numerically. A remove verdict needs control-flow-provable evidence or a
clean liveness-protocol run or a complete caller enumeration; name-search
evidence alone caps the finding at a verify recommendation phrased as a
question. If the evidence category cannot be named, the finding does not
exist.
- Never flag the clean alternative: flat switch/table dispatch however
long, guard-clause early returns, many small functions, long
single-operator boolean chains, named intermediate variables, the
language's own idiom (Go's repeated
if err != nil), deliberate
duplication in tests — tests favor clarity over DRY. These read as complex
and are the simple form.
- Never flag mechanical territory: generated code, vendored
dependencies, migrations, lockfiles, anything a linter or formatter owns.
- Never flag pre-existing complexity as findings: complexity the diff
merely touches goes in its own labeled report bucket, never mixed with
findings about the change.
When a report looks thin, these are the temptations, answered:
| Temptation |
Answer |
| "the comment says legacy / temporary" |
metadata is a claim — verify in code before it creates or kills a finding |
| "nothing in the diff calls it" |
the diff is not the repo — run the liveness protocol |
| "it looks like scaffolding / looks redundant" |
looks-like is not an evidence category — name the commit that orphaned it or drop the candidate |
Step 1 — Establish the change set
Fix the scope first: the diff between the current branch and a base commit.
- PR branch (preferred). Find the current branch (
git rev-parse --abbrev-ref HEAD) and check for an associated pull request (gh pr view, or the host's tooling). If one exists, the base commit is
git merge-base HEAD <pr-base-branch>; continue to Step 2.
- Non-PR branch. Derive the base: the upstream merge-base
(
git merge-base HEAD @{upstream}) if an upstream is set; else the
merge-base with whichever integration branch exists (main, master,
develop, trunk); else the divergence point from
git log --first-parent and the reflog. When the user named a base or
the derivation is unambiguous, state the base commit and the assumption
and proceed; when candidates genuinely compete, ask — the entire review
scope depends on it.
The changed lines are the primary scope. Read surrounding code, call sites,
and tests as needed — a shape that looks redundant in the diff may be
load-bearing once you see how it is used.
Step 2 — Read the review
When a PR exists, read its review comments, human and bot alike. The
substance of accepted feedback is a hard constraint: a shape that exists to
satisfy a review comment is not accidental complexity, however awkward it
looks in isolation. The reverse does not hold — most review-round reshaping
never gets a comment, so the absence of one is not evidence that a shape is
unjustified or safe to remove. Judge from the code and behavior; treat the
thread as one input. Non-PR branches skip this step.
Step 3 — Read the branch history as evidence
The verdict is always on the final state, but the commit sequence is
evidence you have nowhere else: a helper, parameter, or branch introduced at
commit k whose reason had disappeared by the final commit is the accretion
signature, and hunks reworked across three or more commits or review rounds
are where it concentrates — scan those first.
Check that the history is real before reading it: squashing and rebasing
make commit order no longer construction order. A single-commit branch or
freshly rebased history means the archaeology is unavailable — rely on
final-state evidence alone, and never narrate a construction story the
history cannot support. History corroborates a finding; it never carries one
by itself.
Step 4 — Scan for candidate shapes
Build an inventory of the diff — files, hunks, new symbols, new branches,
new parameters — then scan it one shape at a time. Collect candidates
without judging them yet; judgment is Steps 5 and 6, so err toward
collecting — a candidate killed later costs one line in the dropped list,
a candidate never collected costs a finding. Branches built by iterating to
green concentrate specific residue: leftover alternate implementations,
test-shaped special cases, and new modules duplicating existing utilities.
For diffs above roughly 50 files, don't spread attention uniformly:
prioritize the most-churned files (git log --oneline --since=6.months -- <file>), where complexity costs the most.
| Shape |
Detection evidence |
Mechanical fix |
| Adapter between two halves of the same change |
both shapes introduced by this diff |
unify the shapes, delete the adapter |
| Constant-threaded flag or parameter |
every call site passes the same literal; a caller passing a variable or config/DB-sourced value makes it data, not a flag — don't flag |
remove the parameter, delete the dead arm |
| Dead scaffolding from iteration |
liveness protocol comes back clean; or consumer deleted by a later commit of this branch; references only from its own tests count as no consumers (production-dead, test-alive) |
delete it — symbol and tests together in the test-alive case |
| Unearned indirection |
delegate-only wrapper with one caller; hook or generic with one user and no second-variant test; a function whose name says exactly what its body reads; a class that is one simple function; a layer that is half-or-more pure pass-through (exempt test seams and public API) |
inline / collapse |
| Wrong abstraction |
this diff adds a parameter plus a conditional keyed on it to a shared helper — new or pre-existing — and every in-diff call site passes the same constant |
inline it back into its callers — duplication is cheaper than the wrong abstraction; never propose another parameter instead |
| Within-diff duplication |
side-by-side blocks both added here, near-identical for ~5+ substantive lines; worse when one clone was edited and its sibling was not; annotation stacks, import runs, field lists, and table-driven test entries never count |
single point of truth for verbatim or rename-only clones; near-miss clones need a third occurrence before extraction |
| Special case the general path handles |
a traced execution of the input through the general path, not "looks equivalent" |
delete the branch or hoist it to the caller |
| Redundant re-check on a dominated path |
dominance argument plus caller enumeration; dominance is void across lock acquisition, await/suspension points, or shared mutable state — double-checked locking is intentional; never at trust boundaries (hard rules) |
remove the inner check |
| Sequencing scaffolding |
orchestration or ordering machinery where no observable result depends on execution order |
remove the ordering constraint |
| Error-masking handler |
this diff introduced both a failure source and the broad catch/fallback that swallows it; pre-existing handlers are untouchable (hard rules) |
propagate the failure or narrow the catch |
| Policy strewn through conditionals |
the same predicate tested at two or more new sites |
decide once at the boundary |
| Derivable mutable state |
the value is a pure function of existing inputs, the derivation has an inverse, and recomputation has no side effects — mutable "derived" state with no inverse is input, i.e. essential; resource handles are exempt |
compute on demand; perf-motivated → accidental-but-justified |
| Repo-relative reinvention |
an existing helper, named, call-compatible |
call the existing one |
| Duplicate-capability dependency |
a dependency added here whose used surface an existing repo helper or the stdlib already covers |
use the existing one |
| Needless visibility |
export/public added here; the symbol is referenced only within its own file |
remove the keyword, never the symbol |
| Config/CI residue |
config keys, CI steps, build targets, registry or enum entries orphaned by this change's own deletions |
delete alongside |
| Stale narrative / unfinished rename |
names or comments describing a mechanism a later commit replaced; pre-pivot identifiers surviving in strings, docs, or a missed call site; naming quality in general stays humanize's territory |
rename, rewrite the comment, finish the rename |
| Tangled ride-along |
hunks with no def-use link to the change's purpose |
recommend splitting out — never silent deletion |
Flags get one extra pass before the constant-threaded verdict. Classify
the toggle — release, experiment, ops kill-switch, permission — because
expected lifetimes range from weeks to years: constancy and age alone are
never evidence, kill-switch and monitoring flags are exempt, and the finding
must name the purpose that is now resolved. When a flag or guard is
confirmed dead, enumerate its residue halo two references deep — wrapper
helpers, decision-storing variables, enum and config entries,
flag-conditioned tests — the halo usually outweighs the guard; for each
flag-conditioned test, say whether to delete the whole test or only the
flag manipulation so surviving behavior stays tested; and cover the
flag-management/config side, since code-side-only removal of a live flag is
worse than none. A new flag with no owner or removal plan is at most a nit.
The liveness protocol
Every dead / unused / unreachable / constant claim runs this before keeping
its verdict:
- Inventory entry points: mains, exported API, route/CLI/DI
registrations, scheduled jobs, package.json / Makefile / CI targets —
per package in a monorepo. Most false "unused" findings are missing
entry points, not real deadness.
- Grep beyond call syntax: the symbol name as a quoted string;
reflection and dynamic access (
getattr, globals(), importlib,
Class.forName, send); decorator or annotation registration;
references from config files, templates, CI manifests, and build
scripts — crossing language boundaries.
- Check configurations: build tags,
#ifdef/platform guards,
feature-flag configs; state which configuration the claim holds for.
- Exempt methods that exist to satisfy an interface, protocol, or
abstract base, and anything in generated files.
- Count the subgraph, not the symbol: a new symbol referenced only by
other new symbols themselves unreachable from any entry point is dead
despite having references.
- The evidence kind picks the verb: control-flow-provable deadness
(code after an unconditional return or throw, a parameter never read, a
condition that is a tautology across every traced assignment site) or a
protocol run whose searches all came back empty may say remove — quote
the searches and their empty results in the finding as the receipt.
Anything less says verify, phrased as a question.
Step 5 — Verdict: four tests, three outcomes
Run each candidate through four tests:
- Re-derivation. If the author rewrote the change in one sitting from
the final requirement, would this construct exist? Constructs that only
make sense given the order the branch was built are accidental by
construction.
- Requirement. Name the observable requirement the construct serves. If
you cannot name one, the verdict is "ask the author" — never "removable";
a shape can be the residue of two requirements interacting.
- Availability. Accidental means avoidable with means already at hand.
Name the means: the existing repo helper, the stdlib call, the
established idiom.
- Removability. Mentally delete it. Correct-but-slower results mean
accidental but useful — isolate and label, never remove. Changed results
mean it is, or protects, essential logic: no remove verdict, ever.
Three verdicts, not two:
- Accidental, removable — report it with the cleaner shape.
- Accidental but justified — caches, measured denormalization, and other
shapes carried for performance or ease of expression: recommend isolating
and labeling it, not deleting it.
- Ambiguous — phrase it as a question that names the evidence that would
settle it.
Presume inherent unless proven redundant within this change: sad-path,
retry, and telemetry code; security and fraud checks; concurrency, overflow,
and NULL handling; external-interface conformity; compatibility shims; i18n
and legal requirements; anything a reviewer asked for; and tests, seams,
test doubles, and CI plumbing — modifiability work is never speculative
generality, only capability built for a presumptive feature is.
Step 6 — Verify before reporting
Try to refute each candidate; a refutation is worth as much as a
confirmation. A refutation must produce new external evidence — a grep you
have not run, the base-commit version (git show <base>:<file>), a caller
or test you have not read. Re-reading your own reasoning is not
verification; it is known to make reports worse, not better. For every
draft finding, re-read the cited file:line and confirm the quoted code is
actually there, and attach the mechanical receipt — the command you ran and
what it returned.
Then the kill questions, in order: Is this a nitpick? Is this a fake
problem? What breaks — or what must a reader wrongly hold in mind — if it
ships as-is? The first two kill the candidate outright; the answer to the
third becomes the finding's impact line. Drop without exception:
- a finding that recommends something the diff already did — always compare
the
+ lines against the - lines before claiming a missed
simplification;
- a claim about a symbol possibly defined or consumed outside the diff that
a repo-wide search has not resolved;
- a recommendation whose replacement cannot be written as a concrete
before/after sketch, or whose sketch is a no-op — "verify that" and
"consider ensuring" are not findings.
A reportable finding's fix deletes, collapses, or rewrites a named
artifact — a flag, branch, parameter, adapter, file, duplicated block, or
stale name or comment.
Before emitting, re-run the four Step-5 tests against every drafted finding
as if seeing it for the first time — mandatory for anything carrying a
remove verdict. Gate on whether the evidence reproduces, never on whether a
second pass agrees with the first: independent reviews converge on almost
nothing, so agreement filters mostly veto true findings. Cross-check the
hard rules and test coverage last, then apply the survival bar: could you
defend this finding to the author with file:line citations? Most candidates
should die here — the dead ones go to the dropped-candidates list — and a
short or empty report means the change is clean; saying so is a correct
outcome.
Step 7 — Apply what the evidence supports
Mode first. An imperative invocation — "reduce complexity", "simplify this
PR", "clean up this branch" — applies surviving findings before the report
is written. A question-phrased invocation ("did this change accrete
cruft?") or an explicit "just report" / "don't change anything" is a
report-only run: skip this step and, for questions, close the report by
offering to apply.
Preflight, before the first edit:
- Run the repo's verification once on the untouched tree — the narrowest
test selection exercising the diff plus the repo's standard quick gate
(build, typecheck), extended to the full suite when any deletion-type
finding is a candidate. This is the baseline: only failures new against
it indict an edit. If verification cannot run in this environment (CI-only,
missing services or secrets), the run demotes to report-only, findings
marked unverifiable here.
- Record every edit so it can be undone exactly: snapshot the pre-edit
state first (
git stash create, noting the SHA without stashing, or a
saved diff plus copies of the files in scratch). "Revert" below means
inverse-applying the skill's own recorded edits — never git restore or
git checkout against HEAD, which on a dirty tree destroy the author's
uncommitted work.
- Never edit a file that already carries uncommitted author changes;
findings touching one demote to report-only.
The gate, per finding — the verdict must be accidental, removable, and
the fix type picks its oracle:
- Restructures — inline, collapse, hoist, unify, dedup, decide-once:
need the remove-verdict evidence bar (control-flow proof, clean
liveness-protocol run, or complete caller enumeration; a traced
execution and verbatim in-diff clone identity qualify as control-flow
grade for their shapes) AND existing tests exercising the behavior the
edit moves through.
- Deletions of production-dead code — dead scaffolding, dead arms,
needless visibility, config residue orphaned by this diff's own
deletions: the clean liveness run is the coverage; dead code has no
tests to demand. The surviving suite must still pass. In the test-alive
case, delete the symbol and its tests together and verify with the full
remaining suite.
- Text-only edits — stale narrative, comment rewrites, finishing a
rename: the post-apply verification suffices.
- Never apply — report only: ambiguous findings and anything phrased
as a question; accidental but justified shapes (isolating and labeling
is the author's call); tangled ride-alongs (splitting is the author's
decision); flag-management/config-side removals that need a rollout;
compound multi-file restructures; findings a previous report for this
branch already presented — the author has seen and not taken them; and
all pre-existing complexity.
Apply and verify one finding at a time — apply, run the baseline's
verification, keep or revert — so a failure indicts exactly one edit;
after any revert, re-check that later findings' premises still hold. A
failure reverts that finding's edit completely and demotes it to a report
entry carrying the new-against-baseline failures as evidence; never leave
an edit half-applied, and never weaken a test to keep one. A mechanical
fix may force a few out-of-diff lines — the config entry orphaned by an
in-diff deletion, a rename's missed call site: edit the minimal set and
list each forced line in the finding's Status; a fix needing more than
that is report-only.
Follow each finding's own edit recipe — small behavior-preserving steps,
structure-only, never mixed with behavior changes — and leave everything
uncommitted for the author to keep or revert; commit only if asked.
The report
The bar first: a finding the author reads and declines to act on is a
defect of this report, not of the author — when in doubt, the candidate
goes to Dropped candidates. Report at most six findings, ranked by
consequence. Applied findings are exempt from that cap — every edit surviving in the
tree appears in full with its Status; the caps govern report-only
findings. As the diff grows,
raise the evidence bar and shorten the report, never lengthen it; weight
severity by churn — the same shape matters more in a frequently-changed
file than in a stable one.
Each finding, in this structure:
- Title — one specific imperative sentence: what becomes simpler and why
("delete the three-way branch — the general path already handles X").
Question form is reserved for the Ambiguous verdict.
- Where — file:line.
- Shape — the catalog name plus the mechanical fix type.
- Evidence — the citations and receipts that survived Step 6; before/
after structural counts where they help (max nesting 4→2, params 5→3),
never composite scores.
- Impact — which cost it removes: change amplification, cognitive load,
or obscured information. Claim comprehension cost, never defect or
maintenance economics — those don't follow from shape alone.
- Cleaner shape — an edit recipe in small behavior-preserving steps,
landing as its own structure-only commit when the author lands it — the
skill leaves its edits uncommitted — separate from behavior changes;
compound multi-file restructures get staged or demoted to follow-up.
- Behavior preservation — every requirement the shape touches; the test
coverage status; if uncovered, the test that would make the edit safe.
- Severity × disposition — issue / suggestion / nit, crossed with:
before merge / fine as a follow-up PR / question for the author. Pin
severity to the concrete consequence, not the persuasiveness of the
write-up; every finding is non-blocking — an applied edit sits
uncommitted for the author to keep or revert, and a report-only finding
presents the evidence and lets the author decide. Signature- and
hierarchy-crossing recommendations carry the highest regression risk —
weight them down.
- Status — applied (files touched, any forced out-of-diff lines, and
the verification receipt: the command run and its result against the
baseline); applied, then reverted (the new-against-baseline failure as
the receipt); or report-only, naming the Step 7 gate that stopped it —
ambiguous verdict, author's-call shape, uncovered behavior, dirty file,
rollout-gated, compound restructure, out-of-diff bound exceeded,
unverifiable here, or a question-phrased / user-requested report-only
run.
Order the report: findings in this change, then pre-existing complexity the
diff touches (labeled as such, never mixed in), then open questions, then
Dropped candidates — one line each: the shape, and the evidence that
killed it. Group findings that share one root cause into a single entry.
Report at most three nits and summarize the rest as a count. If a previous
reduce-complexity report exists for this branch, do not re-report what it
already said. If nothing survives, the report is: the base commit, what was
scanned, and the strongest candidate with the evidence that killed it.
Keep recommendations proportional to an in-review change: the change's
primary new surface is presumed intentional design — recommending its
wholesale dissolution requires wrong-abstraction evidence, not taste — and
broad redesign is warranted only when a wrong abstraction boundary is
itself the direct cause. Skip polish on code that is feature-flagged,
experimental, or slated for deletion; suppress praise notes, out-of-diff
opportunities, linter territory, and questions that only ask for
explanation. Open the report with the tree state in one line — how many
findings were applied, which files changed, and the verification result —
so the author knows before reading anything else whether their working
tree moved.
A worked example of one finding:
Title: Delete use_new_path and its dead arm — every caller passes
true, so the old arm never runs.
Where: src/export.py:41 (parameter), src/export.py:58-71 (old
arm); call sites src/cli.py:88, src/batch.py:130,
tests/test_export.py:19,44.
Shape: constant-threaded flag or parameter → remove the parameter,
delete the dead arm.
Evidence: caller enumeration — grep finds exactly four call sites,
each passing use_new_path=True; the flag arrived in commit 3 of this
branch to keep the legacy arm alive during development, and commit 6
moved the last caller off it.
Impact: cognitive load — every reader of export() must understand a
branch that cannot execute.
Cleaner shape: delete the parameter and the else arm; no other
signature change.
Behavior preservation: the surviving arm is the one every caller
already exercises; tests/test_export.py covers it directly.
Severity × disposition: suggestion / before merge.
Status: applied — src/export.py, src/cli.py, src/batch.py,
tests/test_export.py edited, no forced out-of-diff lines;
pytest tests/test_export.py 23 passed, full suite matches the green
baseline (412 passed). Left uncommitted for review.
And a worked example of a candidate that dies in Step 6:
retry_wrap in src/jobs.py:74 pattern-matched dead scaffolding — no
call-syntax references anywhere in the final tree. The liveness protocol
killed it: step 2's quoted-string grep hits config/jobs.yaml:12, which
wires retry_wrap up as a queue callback — an entry point the call-graph
reading missed. Name-search evidence with a live dynamic consumer:
suppressed. It costs one line in the report:
Dropped candidates: retry_wrap (dead scaffolding) — apparent zero
references, but consumed via config/jobs.yaml queue wiring.
1---2name: reduce-complexity3description: Structural review of an in-progress change — a PR or feature branch — for accidental complexity accreted while the change was built. Told to "reduce complexity", "simplify this PR", or "clean up this branch before review", it applies the proven-safe simplifications by default and reports what changed; asked a question — "did this change accrete cruft", could this be expressed more simply — or told "report only", it reviews without editing. Not for line-level style cleanup (humanize) or bug-hunting (code review).4---56# Reduce complexity: find what the change no longer needs78You are reviewing an in-progress change to find where it could be expressed9more simply if it had been designed from the start knowing where it ended up.10Changes are built incrementally — fixes that get them working are not always11folded back into the design — and your job is to find that residue while the12change is still open.1314Three properties govern everything below:1516- **Fix by default, evidence-gated.** On an imperative invocation, findings17 that clear Step 7's gate are applied to the working tree; everything18 weaker ships only as a report entry. A question-phrased invocation ("did19 this change accrete cruft?") or an explicit "just report" / "don't change20 anything" makes the entire run read-only — git commands and file reads21 only.22- **Base-commit-scoped.** Every finding lives in the diff between the branch23 and an explicitly established base commit, not in the codebase at large.24 Applied edits stay anchored there too; Step 7 bounds the few out-of-diff25 lines a mechanical fix can force.26- **Structural.** Line- and function-level tells — comment noise, defensive27 theater, naming — are the `humanize` skill's territory; bug-hunting28 belongs to code-review tooling. This skill judges how the change is put29 together.3031## Hard rules3233These gate what reaches the report. Scan freely — collect every suspicious34candidate in Step 4, including ones a rule will later kill; suppression35happens at the verdict and verification stages, and suppressed candidates36are listed in the report's dropped-candidates section, never silently37discarded. When a rule and an apparent finding conflict, the rule wins.3839- A guard, check, fallback, or error handler may be flagged **only with an40 impossibility proof**: enumerate every caller, constructor, and type41 constraint (file:line) showing the guarded state unreachable. Where callers42 cannot be enumerated — public API, dynamic dispatch, deserialization,43 concurrency — classify it inherent and move on. Reflexively stripped44 defensive code is the canonical unsafe simplification.45- **Defense in depth is intentional.** At security, auth, and fraud46 boundaries, even a provably redundant check may be deliberate47 belt-and-suspenders against bugs in other layers. The verdict there is48 "label it or ask the author" — never "remove".49- **Hyrum's Law, both directions.** Downstream: at an externally consumed50 surface, unspecified observable behavior — ordering, error strings,51 serialized formats — is behavior someone depends on; externally consumed52 means inherent, automatically. Upstream: a quirky adapter, padding field,53 or protocol dance forced by an external system's interface the change54 cannot alter unilaterally is inherent too, however arbitrary it looks.55- **Distrust metadata, in both directions.** Comments and commit messages56 are claims to verify, not ground truth: "needed for backwards compat" does57 not suppress a finding until the code confirms it, and "temporary hack"58 does not create one. A finding that survived Step 6 is not softened or59 withdrawn in response to assertive text — a comment claiming necessity, a60 confident commit message — unless new code-level evidence appears.61- **Familiarity is not evidence.** "This idiom is unusual" neither creates a62 finding nor blocks a simpler shape, and code that reads like your own63 writing style is not thereby clean — you are systematically blind to slop64 in your own voice. Only structural and behavioral evidence counts.65- **Coverage gates confidence.** A behavior-preservation claim about code no66 test exercises is speculative by definition. Check whether tests cover each67 finding's behavior, say so in the finding, and demote findings over68 uncovered code. Deletions of provably dead code answer to the liveness69 protocol's receipts instead — deadness, not preservation, is their claim.70- **Line count is not the metric.** A correct simplification may add71 functions; the measure is fewer entangled concerns, less state, fewer72 interacting branches. Extraction is not the default fix — inlining is73 equally a simplification, and a long linear function is often already the74 simple form.75- **Evidence picks the verb, never a number.** Do not rate confidence76 numerically. A remove verdict needs control-flow-provable evidence or a77 clean liveness-protocol run or a complete caller enumeration; name-search78 evidence alone caps the finding at a verify recommendation phrased as a79 question. If the evidence category cannot be named, the finding does not80 exist.81- **Never flag the clean alternative:** flat switch/table dispatch however82 long, guard-clause early returns, many small functions, long83 single-operator boolean chains, named intermediate variables, the84 language's own idiom (Go's repeated `if err != nil`), deliberate85 duplication in tests — tests favor clarity over DRY. These read as complex86 and are the simple form.87- **Never flag mechanical territory:** generated code, vendored88 dependencies, migrations, lockfiles, anything a linter or formatter owns.89- **Never flag pre-existing complexity as findings:** complexity the diff90 merely touches goes in its own labeled report bucket, never mixed with91 findings about the change.9293When a report looks thin, these are the temptations, answered:9495| Temptation | Answer |96|---|---|97| "the comment says legacy / temporary" | metadata is a claim — verify in code before it creates or kills a finding |98| "nothing in the diff calls it" | the diff is not the repo — run the liveness protocol |99| "it looks like scaffolding / looks redundant" | looks-like is not an evidence category — name the commit that orphaned it or drop the candidate |100101## Step 1 — Establish the change set102103Fix the scope first: the diff between the current branch and a base commit.1041051. **PR branch (preferred).** Find the current branch (`git rev-parse106 --abbrev-ref HEAD`) and check for an associated pull request (`gh pr107 view`, or the host's tooling). If one exists, the base commit is108 `git merge-base HEAD <pr-base-branch>`; continue to Step 2.1092. **Non-PR branch.** Derive the base: the upstream merge-base110 (`git merge-base HEAD @{upstream}`) if an upstream is set; else the111 merge-base with whichever integration branch exists (`main`, `master`,112 `develop`, `trunk`); else the divergence point from113 `git log --first-parent` and the reflog. When the user named a base or114 the derivation is unambiguous, state the base commit and the assumption115 and proceed; when candidates genuinely compete, ask — the entire review116 scope depends on it.117118The changed lines are the primary scope. Read surrounding code, call sites,119and tests as needed — a shape that looks redundant in the diff may be120load-bearing once you see how it is used.121122## Step 2 — Read the review123124When a PR exists, read its review comments, human and bot alike. The125substance of accepted feedback is a hard constraint: a shape that exists to126satisfy a review comment is not accidental complexity, however awkward it127looks in isolation. The reverse does not hold — most review-round reshaping128never gets a comment, so the absence of one is not evidence that a shape is129unjustified or safe to remove. Judge from the code and behavior; treat the130thread as one input. Non-PR branches skip this step.131132## Step 3 — Read the branch history as evidence133134The verdict is always on the final state, but the commit sequence is135evidence you have nowhere else: a helper, parameter, or branch introduced at136commit k whose reason had disappeared by the final commit is the accretion137signature, and hunks reworked across three or more commits or review rounds138are where it concentrates — scan those first.139140Check that the history is real before reading it: squashing and rebasing141make commit order no longer construction order. A single-commit branch or142freshly rebased history means the archaeology is unavailable — rely on143final-state evidence alone, and never narrate a construction story the144history cannot support. History corroborates a finding; it never carries one145by itself.146147## Step 4 — Scan for candidate shapes148149Build an inventory of the diff — files, hunks, new symbols, new branches,150new parameters — then scan it one shape at a time. Collect candidates151without judging them yet; judgment is Steps 5 and 6, so err toward152collecting — a candidate killed later costs one line in the dropped list,153a candidate never collected costs a finding. Branches built by iterating to154green concentrate specific residue: leftover alternate implementations,155test-shaped special cases, and new modules duplicating existing utilities.156157For diffs above roughly 50 files, don't spread attention uniformly:158prioritize the most-churned files (`git log --oneline --since=6.months --159<file>`), where complexity costs the most.160161| Shape | Detection evidence | Mechanical fix |162|---|---|---|163| Adapter between two halves of the same change | both shapes introduced by this diff | unify the shapes, delete the adapter |164| Constant-threaded flag or parameter | every call site passes the same literal; a caller passing a variable or config/DB-sourced value makes it data, not a flag — don't flag | remove the parameter, delete the dead arm |165| Dead scaffolding from iteration | liveness protocol comes back clean; or consumer deleted by a later commit of this branch; references only from its own tests count as no consumers (production-dead, test-alive) | delete it — symbol and tests together in the test-alive case |166| Unearned indirection | delegate-only wrapper with one caller; hook or generic with one user and no second-variant test; a function whose name says exactly what its body reads; a class that is one simple function; a layer that is half-or-more pure pass-through (exempt test seams and public API) | inline / collapse |167| Wrong abstraction | this diff adds a parameter plus a conditional keyed on it to a shared helper — new or pre-existing — and every in-diff call site passes the same constant | inline it back into its callers — duplication is cheaper than the wrong abstraction; never propose another parameter instead |168| Within-diff duplication | side-by-side blocks both added here, near-identical for ~5+ substantive lines; worse when one clone was edited and its sibling was not; annotation stacks, import runs, field lists, and table-driven test entries never count | single point of truth for verbatim or rename-only clones; near-miss clones need a third occurrence before extraction |169| Special case the general path handles | a traced execution of the input through the general path, not "looks equivalent" | delete the branch or hoist it to the caller |170| Redundant re-check on a dominated path | dominance argument plus caller enumeration; dominance is void across lock acquisition, await/suspension points, or shared mutable state — double-checked locking is intentional; never at trust boundaries (hard rules) | remove the inner check |171| Sequencing scaffolding | orchestration or ordering machinery where no observable result depends on execution order | remove the ordering constraint |172| Error-masking handler | this diff introduced both a failure source and the broad catch/fallback that swallows it; pre-existing handlers are untouchable (hard rules) | propagate the failure or narrow the catch |173| Policy strewn through conditionals | the same predicate tested at two or more new sites | decide once at the boundary |174| Derivable mutable state | the value is a pure function of existing inputs, the derivation has an inverse, and recomputation has no side effects — mutable "derived" state with no inverse is input, i.e. essential; resource handles are exempt | compute on demand; perf-motivated → accidental-but-justified |175| Repo-relative reinvention | an existing helper, named, call-compatible | call the existing one |176| Duplicate-capability dependency | a dependency added here whose used surface an existing repo helper or the stdlib already covers | use the existing one |177| Needless visibility | export/public added here; the symbol is referenced only within its own file | remove the keyword, never the symbol |178| Config/CI residue | config keys, CI steps, build targets, registry or enum entries orphaned by this change's own deletions | delete alongside |179| Stale narrative / unfinished rename | names or comments describing a mechanism a later commit replaced; pre-pivot identifiers surviving in strings, docs, or a missed call site; naming quality in general stays humanize's territory | rename, rewrite the comment, finish the rename |180| Tangled ride-along | hunks with no def-use link to the change's purpose | recommend splitting out — never silent deletion |181182**Flags get one extra pass** before the constant-threaded verdict. Classify183the toggle — release, experiment, ops kill-switch, permission — because184expected lifetimes range from weeks to years: constancy and age alone are185never evidence, kill-switch and monitoring flags are exempt, and the finding186must name the purpose that is now resolved. When a flag or guard is187confirmed dead, enumerate its residue halo two references deep — wrapper188helpers, decision-storing variables, enum and config entries,189flag-conditioned tests — the halo usually outweighs the guard; for each190flag-conditioned test, say whether to delete the whole test or only the191flag manipulation so surviving behavior stays tested; and cover the192flag-management/config side, since code-side-only removal of a live flag is193worse than none. A new flag with no owner or removal plan is at most a nit.194195### The liveness protocol196197Every dead / unused / unreachable / constant claim runs this before keeping198its verdict:1992001. **Inventory entry points**: mains, exported API, route/CLI/DI201 registrations, scheduled jobs, package.json / Makefile / CI targets —202 per package in a monorepo. Most false "unused" findings are missing203 entry points, not real deadness.2042. **Grep beyond call syntax**: the symbol name as a quoted string;205 reflection and dynamic access (`getattr`, `globals()`, `importlib`,206 `Class.forName`, `send`); decorator or annotation registration;207 references from config files, templates, CI manifests, and build208 scripts — crossing language boundaries.2093. **Check configurations**: build tags, `#ifdef`/platform guards,210 feature-flag configs; state which configuration the claim holds for.2114. **Exempt** methods that exist to satisfy an interface, protocol, or212 abstract base, and anything in generated files.2135. **Count the subgraph, not the symbol**: a new symbol referenced only by214 other new symbols themselves unreachable from any entry point is dead215 despite having references.2166. **The evidence kind picks the verb**: control-flow-provable deadness217 (code after an unconditional return or throw, a parameter never read, a218 condition that is a tautology across every traced assignment site) or a219 protocol run whose searches all came back empty may say *remove* — quote220 the searches and their empty results in the finding as the receipt.221 Anything less says *verify*, phrased as a question.222223## Step 5 — Verdict: four tests, three outcomes224225Run each candidate through four tests:2262271. **Re-derivation.** If the author rewrote the change in one sitting from228 the final requirement, would this construct exist? Constructs that only229 make sense given the order the branch was built are accidental by230 construction.2312. **Requirement.** Name the observable requirement the construct serves. If232 you cannot name one, the verdict is "ask the author" — never "removable";233 a shape can be the residue of two requirements interacting.2343. **Availability.** Accidental means avoidable with means already at hand.235 Name the means: the existing repo helper, the stdlib call, the236 established idiom.2374. **Removability.** Mentally delete it. Correct-but-slower results mean238 accidental but useful — isolate and label, never remove. Changed results239 mean it is, or protects, essential logic: no remove verdict, ever.240241Three verdicts, not two:242243- **Accidental, removable** — report it with the cleaner shape.244- **Accidental but justified** — caches, measured denormalization, and other245 shapes carried for performance or ease of expression: recommend isolating246 and labeling it, not deleting it.247- **Ambiguous** — phrase it as a question that names the evidence that would248 settle it.249250Presume inherent unless proven redundant within this change: sad-path,251retry, and telemetry code; security and fraud checks; concurrency, overflow,252and NULL handling; external-interface conformity; compatibility shims; i18n253and legal requirements; anything a reviewer asked for; and tests, seams,254test doubles, and CI plumbing — modifiability work is never speculative255generality, only capability built for a presumptive feature is.256257## Step 6 — Verify before reporting258259Try to refute each candidate; a refutation is worth as much as a260confirmation. A refutation must produce new external evidence — a grep you261have not run, the base-commit version (`git show <base>:<file>`), a caller262or test you have not read. Re-reading your own reasoning is not263verification; it is known to make reports worse, not better. For every264draft finding, re-read the cited file:line and confirm the quoted code is265actually there, and attach the mechanical receipt — the command you ran and266what it returned.267268Then the kill questions, in order: *Is this a nitpick? Is this a fake269problem? What breaks — or what must a reader wrongly hold in mind — if it270ships as-is?* The first two kill the candidate outright; the answer to the271third becomes the finding's impact line. Drop without exception:272273- a finding that recommends something the diff already did — always compare274 the `+` lines against the `-` lines before claiming a missed275 simplification;276- a claim about a symbol possibly defined or consumed outside the diff that277 a repo-wide search has not resolved;278- a recommendation whose replacement cannot be written as a concrete279 before/after sketch, or whose sketch is a no-op — "verify that" and280 "consider ensuring" are not findings.281282A reportable finding's fix deletes, collapses, or rewrites a named283artifact — a flag, branch, parameter, adapter, file, duplicated block, or284stale name or comment.285286Before emitting, re-run the four Step-5 tests against every drafted finding287as if seeing it for the first time — mandatory for anything carrying a288remove verdict. Gate on whether the evidence reproduces, never on whether a289second pass agrees with the first: independent reviews converge on almost290nothing, so agreement filters mostly veto true findings. Cross-check the291hard rules and test coverage last, then apply the survival bar: could you292defend this finding to the author with file:line citations? Most candidates293should die here — the dead ones go to the dropped-candidates list — and a294short or empty report means the change is clean; saying so is a correct295outcome.296297## Step 7 — Apply what the evidence supports298299Mode first. An imperative invocation — "reduce complexity", "simplify this300PR", "clean up this branch" — applies surviving findings before the report301is written. A question-phrased invocation ("did this change accrete302cruft?") or an explicit "just report" / "don't change anything" is a303report-only run: skip this step and, for questions, close the report by304offering to apply.305306Preflight, before the first edit:307308- Run the repo's verification once on the untouched tree — the narrowest309 test selection exercising the diff plus the repo's standard quick gate310 (build, typecheck), extended to the full suite when any deletion-type311 finding is a candidate. This is the baseline: only failures new against312 it indict an edit. If verification cannot run in this environment (CI-only,313 missing services or secrets), the run demotes to report-only, findings314 marked unverifiable here.315- Record every edit so it can be undone exactly: snapshot the pre-edit316 state first (`git stash create`, noting the SHA without stashing, or a317 saved diff plus copies of the files in scratch). "Revert" below means318 inverse-applying the skill's own recorded edits — never `git restore` or319 `git checkout` against HEAD, which on a dirty tree destroy the author's320 uncommitted work.321- Never edit a file that already carries uncommitted author changes;322 findings touching one demote to report-only.323324The gate, per finding — the verdict must be *accidental, removable*, and325the fix type picks its oracle:326327- **Restructures** — inline, collapse, hoist, unify, dedup, decide-once:328 need the remove-verdict evidence bar (control-flow proof, clean329 liveness-protocol run, or complete caller enumeration; a traced330 execution and verbatim in-diff clone identity qualify as control-flow331 grade for their shapes) AND existing tests exercising the behavior the332 edit moves through.333- **Deletions of production-dead code** — dead scaffolding, dead arms,334 needless visibility, config residue orphaned by this diff's own335 deletions: the clean liveness run is the coverage; dead code has no336 tests to demand. The surviving suite must still pass. In the test-alive337 case, delete the symbol and its tests together and verify with the full338 remaining suite.339- **Text-only edits** — stale narrative, comment rewrites, finishing a340 rename: the post-apply verification suffices.341- **Never apply — report only:** ambiguous findings and anything phrased342 as a question; *accidental but justified* shapes (isolating and labeling343 is the author's call); tangled ride-alongs (splitting is the author's344 decision); flag-management/config-side removals that need a rollout;345 compound multi-file restructures; findings a previous report for this346 branch already presented — the author has seen and not taken them; and347 all pre-existing complexity.348349Apply and verify one finding at a time — apply, run the baseline's350verification, keep or revert — so a failure indicts exactly one edit;351after any revert, re-check that later findings' premises still hold. A352failure reverts that finding's edit completely and demotes it to a report353entry carrying the new-against-baseline failures as evidence; never leave354an edit half-applied, and never weaken a test to keep one. A mechanical355fix may force a few out-of-diff lines — the config entry orphaned by an356in-diff deletion, a rename's missed call site: edit the minimal set and357list each forced line in the finding's Status; a fix needing more than358that is report-only.359360Follow each finding's own edit recipe — small behavior-preserving steps,361structure-only, never mixed with behavior changes — and leave everything362uncommitted for the author to keep or revert; commit only if asked.363364## The report365366The bar first: a finding the author reads and declines to act on is a367defect of this report, not of the author — when in doubt, the candidate368goes to Dropped candidates. Report at most six findings, ranked by369consequence. Applied findings are exempt from that cap — every edit surviving in the370tree appears in full with its Status; the caps govern report-only371findings. As the diff grows,372raise the evidence bar and shorten the report, never lengthen it; weight373severity by churn — the same shape matters more in a frequently-changed374file than in a stable one.375376Each finding, in this structure:377378- **Title** — one specific imperative sentence: what becomes simpler and why379 ("delete the three-way branch — the general path already handles X").380 Question form is reserved for the Ambiguous verdict.381- **Where** — file:line.382- **Shape** — the catalog name plus the mechanical fix type.383- **Evidence** — the citations and receipts that survived Step 6; before/384 after structural counts where they help (max nesting 4→2, params 5→3),385 never composite scores.386- **Impact** — which cost it removes: change amplification, cognitive load,387 or obscured information. Claim comprehension cost, never defect or388 maintenance economics — those don't follow from shape alone.389- **Cleaner shape** — an edit recipe in small behavior-preserving steps,390 landing as its own structure-only commit when the author lands it — the391 skill leaves its edits uncommitted — separate from behavior changes;392 compound multi-file restructures get staged or demoted to follow-up.393- **Behavior preservation** — every requirement the shape touches; the test394 coverage status; if uncovered, the test that would make the edit safe.395- **Severity × disposition** — issue / suggestion / nit, crossed with:396 before merge / fine as a follow-up PR / question for the author. Pin397 severity to the concrete consequence, not the persuasiveness of the398 write-up; every finding is non-blocking — an applied edit sits399 uncommitted for the author to keep or revert, and a report-only finding400 presents the evidence and lets the author decide. Signature- and401 hierarchy-crossing recommendations carry the highest regression risk —402 weight them down.403- **Status** — *applied* (files touched, any forced out-of-diff lines, and404 the verification receipt: the command run and its result against the405 baseline); *applied, then reverted* (the new-against-baseline failure as406 the receipt); or *report-only*, naming the Step 7 gate that stopped it —407 ambiguous verdict, author's-call shape, uncovered behavior, dirty file,408 rollout-gated, compound restructure, out-of-diff bound exceeded,409 unverifiable here, or a question-phrased / user-requested report-only410 run.411412Order the report: findings in this change, then pre-existing complexity the413diff touches (labeled as such, never mixed in), then open questions, then414**Dropped candidates** — one line each: the shape, and the evidence that415killed it. Group findings that share one root cause into a single entry.416Report at most three nits and summarize the rest as a count. If a previous417reduce-complexity report exists for this branch, do not re-report what it418already said. If nothing survives, the report is: the base commit, what was419scanned, and the strongest candidate with the evidence that killed it.420421Keep recommendations proportional to an in-review change: the change's422primary new surface is presumed intentional design — recommending its423wholesale dissolution requires wrong-abstraction evidence, not taste — and424broad redesign is warranted only when a wrong abstraction boundary is425itself the direct cause. Skip polish on code that is feature-flagged,426experimental, or slated for deletion; suppress praise notes, out-of-diff427opportunities, linter territory, and questions that only ask for428explanation. Open the report with the tree state in one line — how many429findings were applied, which files changed, and the verification result —430so the author knows before reading anything else whether their working431tree moved.432433A worked example of one finding:434435> **Title:** Delete `use_new_path` and its dead arm — every caller passes436> `true`, so the old arm never runs.437> **Where:** `src/export.py:41` (parameter), `src/export.py:58-71` (old438> arm); call sites `src/cli.py:88`, `src/batch.py:130`,439> `tests/test_export.py:19,44`.440> **Shape:** constant-threaded flag or parameter → remove the parameter,441> delete the dead arm.442> **Evidence:** caller enumeration — grep finds exactly four call sites,443> each passing `use_new_path=True`; the flag arrived in commit 3 of this444> branch to keep the legacy arm alive during development, and commit 6445> moved the last caller off it.446> **Impact:** cognitive load — every reader of `export()` must understand a447> branch that cannot execute.448> **Cleaner shape:** delete the parameter and the `else` arm; no other449> signature change.450> **Behavior preservation:** the surviving arm is the one every caller451> already exercises; `tests/test_export.py` covers it directly.452> **Severity × disposition:** suggestion / before merge.453> **Status:** applied — `src/export.py`, `src/cli.py`, `src/batch.py`,454> `tests/test_export.py` edited, no forced out-of-diff lines;455> `pytest tests/test_export.py` 23 passed, full suite matches the green456> baseline (412 passed). Left uncommitted for review.457458And a worked example of a candidate that dies in Step 6:459460> `retry_wrap` in `src/jobs.py:74` pattern-matched dead scaffolding — no461> call-syntax references anywhere in the final tree. The liveness protocol462> killed it: step 2's quoted-string grep hits `config/jobs.yaml:12`, which463> wires `retry_wrap` up as a queue callback — an entry point the call-graph464> reading missed. Name-search evidence with a live dynamic consumer:465> suppressed. It costs one line in the report:466> **Dropped candidates:** `retry_wrap` (dead scaffolding) — apparent zero467> references, but consumed via `config/jobs.yaml` queue wiring.