Captain Obvious
Deletes tests that assert what is already guaranteed — by the compiler, by the
mock framework, or by the laws of logic. These tests burn CI time, inflate
coverage confidence, and can never catch a regression. They are the signature
of AI-generated test suites (empirical studies find test smells in 38–100% of
LLM-generated tests).
The heavy lifting is done by two deterministic scripts in
. Your job
is orchestration: run them, interpret the report, clean up the residue, and
verify nothing broke.
Do not hand-scan test files or spawn subagents per
file — one script invocation scans the whole project.
Workflow
1. Detect the stack(s)
- TypeScript: a and / / files.
- Python: / files (pytest).
- A repo can have both; run both detectors.
2. Safety first
The fix step edits test files in place. Both scripts enforce this themselves:
exits 2 unless the target is a git repository with a clean working
tree (untracked files are fine). If it refuses, stash or commit rather than
reaching for
—
removes the only undo path there is
(
), so use it only when the user has explicitly
accepted that.
Trust boundary: scanning executes the project's own toolchain. mypy loads
from the repo's config as in-process Python;
/
resolve (and can run) the
repo's dependencies; the TS side loads the repo's own
package. Run the scan only on repositories you would be willing to run
/
in yourself.
Note: when installed as a plugin, a write-time PreToolUse hook may also be
active — if a test-file Write/Edit is denied with a "captain-obvious:"
reason during cleanup rewrites, fix the flagged assertions instead of
re-trying the same content (see
). Both scanners
also support
for a syntactic-only single-file
scan (JSON to stdout; no mypy/tsc, no side effects).
3. Scan (report-only)
bash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --json /tmp/co-ts.json
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --json /tmp/co-py.json
- The Python detector shells out to mypy for the type-guaranteed category. Use
the project's own environment: pass for uv projects,
for poetry, etc. If mypy isn't available it
degrades gracefully to the syntactic categories.
- Note: the mypy pass briefly writes copies next to test
files (removed when the run ends) — so a "report-only" scan does touch the
working tree. Pass for a strictly read-only scan; if the tree
is not writable the scan degrades to syntactic categories and says so.
- The TS detector resolves the project's own package; without a
tsconfig it degrades to syntactic categories.
- If the project already produces coverage (or you can cheaply run it),
pass (lcov / istanbul / coverage.py
). This is the dynamic half of the ICSE'19 rotten-green
analysis: a whose line never ran is promoted to proven
rotten, and one that did run is dropped as a confirmed false positive. It
turns the noisiest advisory category into a trustworthy one — use it whenever
coverage is available.
Show the user the summary table and the findings before deleting anything.
4. Understand the two levels
- proven — cannot fail, by construction. The scripts guard the known
escape hatches (/, casts, , index signatures, unchecked
index access, structural , custom assertion helpers). Safe to
auto-delete.
- advisory — almost certainly useless but not provable (assertion-free
tests, structural instanceof, mock-echo variants, index-signature-backed
checks, rotten-green conditional asserts, unawaited async assertions). The
script never auto-deletes these, but it records exactly why each is
uncertain, plus a hint ( = usually a deletion,
= usually needs a rewrite). That reason is a question you
are equipped to answer against the surrounding code — so advisories are
adjudicated by you (step 6), not dumped on the user.
See
for the full category catalog and the reasoning
behind each guard.
5. Fix the proven tier (deterministic)
bash
node <skill-dir>/scripts/captain_obvious_ts.mjs --project <repo> --fix
python3 <skill-dir>/scripts/captain_obvious_py.py --path <repo> --fix
Plain
removes only the
proven findings — no judgment required, no
LLM. This is the safe deterministic core; run it first.
6. Adjudicate the advisory tier (you decide, then confirm)
Advisories are the cases determinism can't settle — and that's your job, not
a report line for the user. Do not just forward the list. For each advisory
finding:
- Read the test and the code it exercises. The finding's field is a
pointed question — e.g. "structural instanceof — a shaped non-instance
could sneak in" → check whether anything actually constructs a non-instance
of that type; "mock-echo, indirect" → check whether a real code path runs
between stub and assert.
- Decide one of: delete (the doubt doesn't hold — it really is useless),
keep (the doubt holds — it's a real check), or rewrite (the intent
is valid but the assertion is broken). Rewrite is the advisory tier's real
value: fix the unawaited ( it), narrow a
to the specific type, repair a rotten-green
so it actually runs. Note findings are
smoke tests — legitimate by design (ICSE'19); default to keep unless
the test clearly meant to assert something and forgot.
- Propose before acting. Present a compact per-item table — finding,
verdict, one-line rationale, and the exact edit for rewrites — and apply
only what the user approves. Never auto-delete or auto-rewrite an advisory.
For a large advisory set, delegate the per-item code reads to a Sonnet
subagent (batch the findings; have it return verdict + rationale + proposed
edit per item) and keep the final proposal/synthesis here — don't burn the main
loop reading files one by one. The proven tier is never handed to a subagent;
it's already decided.
7. Clean the residue
The scripts delete whole test blocks or individual assertion lines. That can
leave behind: unused imports/variables (
will flag them),
empty
blocks, empty test classes, orphaned fixtures/mocks. Fix
those by hand — the typechecker output is your worklist.
8. Verify
Run the project's typecheck AND full test suite (
+ the test
command from package.json /
). Everything must pass with the same
result as before (minus the deleted tests). If anything regresses,
and report what happened instead of pushing through.
9. Report
Tell the user: proven tests/assertions removed (per-category counts, lines
saved), the advisory verdicts you applied (deleted / rewritten, with the fix),
and anything you chose to keep with the reason the doubt held — that last
group is the tool earning trust, not failing.
What NOT to flag (the scripts already know, but so should you)
- on / results — the type is , the check is real.
- Enum/constant contract locks (
expect(ExitCode.OK).toBe(0)
) — they catch renumbering.
- Assertions on values read from files/APIs at test time — real regression tests.
- Tests asserting via custom helpers (, ).
- "Must not raise" contract tests for fail-open code paths.
When NOT to run this at all
- Mid red-green. During TDD a test is supposed to be failing, and a
freshly-written test may not have its assertion yet. This is post-hoc
cleanup — run it once the suite is green, never between red and green.
- On a branch under review. Scan () is fine; is not.
Rewriting test files while a reviewer or a merge gate is reading the diff
invalidates what they reviewed.
- As a coverage or CI-time optimizer. It deletes tests that cannot fail,
which is a correctness argument, not a speed one. "CI is slow" is not a
reason to reach for it — a slow suite full of real tests stays slow.