/age
When to invoke: Review a diff, PR, branch, or path across twelve dimensions. Emit a severity-grouped findings report. Use when the user wants a code review. Trigger on “review this”, “/age”, “is this safe to merge”, or “find bugs”. Also trigger on “spot security issues”, “check for slop”, “review my PR”, or “what’s wrong with this code”. Review every requested dimension. Review all twelve dimensions by default. Do not apply fixes. Route them to /cure. Do not harden tests; route that work to /press.
Review a diff or scoped path before merging or after /press. Use this skill whenever the user wants evidence-backed observations rather than an approval verdict. Do not apply fixes in this skill. Let /cure apply them.
Phase entry
Section titled “Phase entry”Run python3 skills/age/scripts/age.pyz wheypoint-resolve --ref <slug>.
authoritative uses the record; its working_context is the first batched tilth_read.
not-found proceeds cold; legacy shows its source and slug, then proceeds.
gated, ambiguous, and error stop and show the payload.
Show advisory stale-commit and grounded-path-missing findings.
Inputs
Section titled “Inputs”/age [<ref-or-range>] [--scope <path>]... [--slug <slug>] [--effort quick|normal|deep] [--overall] [--full] [--safe] [--open-pr] [--auto] [--hard] [--html]/age <slug> [--effort quick|normal|deep] [--overall] [--full] [--safe] [--open-pr] [--auto] [--hard] [--html]Repeat --scope <path> for each reviewed path.
Every report needs a slug.
Take the slug from the <slug> form.
Take it from --slug <slug> on a scoped or range review.
Derive it with slugify only when the caller supplies neither.
A caller in a pipeline always passes the pipeline slug.
--full expands the ## Low section when ten or more low-severity findings exist.
The default report collapses that section to a one-line summary.
--safe re-introduces cure selection.
--open-pr propagates through /cure to terminal /plate.
A new PR follows /plate’s explicit-choice and review-shape policy.
After phase entry, use .cheese/press/<slug>.md (if present) as Press context.
Review the current working diff.
For a <ref-or-range>, review that range.
Review the current working diff when the user supplies neither input.
If the base branch is unclear, ask or use the repository’s documented default.
--auto is the propagated autonomous-mode flag from /cook --auto.
See ## Handoff and references/handoff-detail.md § Auto mode for the cap rule and the full chain.
handoff_context.wiki_hits is optional routed input from /cheese.
Each hit carries page, line, and why.
Reuse each valid hit before you ground the review again.
Render the reused hits in ## Wiki context.
--hard propagates through /cure to /plate.
Age never fires the gate.
/plate gives /hard-cheese the final verified artifact state before publication.
--html emits a static HTML copy alongside .cheese/age/<slug>.md.
Write the markdown first.
Then run python3 skills/age/scripts/age.pyz html-report --report .cheese/age/<slug>.md --slug <slug>.
Print the returned path.
The HTML groups findings by severity into the shared HTML shell.
The output is offline and uses no CDN or JS.
Read ../cheese/references/harness-portability.md for helper resolution, sub-agent dispatch, GitHub operations, and handoff transitions.
Prefer the bundled or repo-local helper.
Treat ${CLAUDE_SKILL_DIR} as an optional host-provided fallback.
The handoff blocks below define the portable contract.
Remember: slash commands are host renderings, not the control model.
Review effort and scope
Section titled “Review effort and scope”Use --effort quick|normal|deep; the default is normal.
Use --overall for full subject fan-out across the review target.
--full still controls finding visibility, not review scope.
Review dimensions
Section titled “Review dimensions”Review correctness, security, encapsulation, spec, complexity, deslop, assertions, NIH, efficiency, telemetry, conventions, and altitude.
Assign one blocker, high, medium, or low severity to each finding.
Use references/dimensions.md for severity rules and recommendation shapes.
This workflow omits the git-history/precedent dimension.
- Identify the diff, scope, and relevant specification or issue.
Read
references/fan-out.mdfor the context checklist and deterministic planning contract. Collect instruction sources, build context, and runage.pyz age-routethrough its bundle path. Use the returned assignments, effort, dispatch batches, and explicit capability restrictions. Assemble shared evidence and the plan before the lock; do not launch a separate classifier agent. The lock covers the packet, because the packet is review evidence. Then runpython3 skills/age/scripts/age.pyz review-lock --slug <slug>to lock the production tree. Use the resolved slug from## Inputs. Step 5 rejects the report when a production file changes. - Gather evidence from the diff, touched files, tests, and callers/imports.
Resolve the upstream report with
python3 skills/age/scripts/age.pyz artifact-path --phase press --slug <slug>. Validate its preamble withpython3 skills/age/scripts/age.pyz read-handoff-slug --phase press --slug <slug>. That command returns preamble fields only. Read the resolved file itself for the## Review follow-upssection and every unresolved item. Copy each unresolved item into a## Press findingssub-section. Keep theartifactandbaselinevalues from that preamble for step 5./curereads only.cheese/age/<slug>.md.
If no press report exists but a cook handoff exists, record press: skipped (see ## Output).
Print the warning at handoff.
If no cook artifact exists either, omit the marker and continue.
If .cheese/glossary/<slug>.md exists, read it to flag naming drift as a deslop finding.
3. Review every dimension.
Omit dimensions with no findings.
Report every defect, however minor.
Do not filter findings by perceived significance.
Verification filters findings after reconciliation.
Do not report a gate failure that matches the diff’s recorded baseline: block.
Read ../cook/references/quality-gates.md for the baseline rules.
Report only new or changed failures.
4. Compute severity per finding (base + location bump + compounding bump, capped at blocker).
Group findings by severity (## Blocker → ## High → ## Medium → ## Low).
Order findings by file within each severity group.
Follow references/fan-out.md for verification and the deep-only gap sweep before writing.
5. Write the report body to .cheese/age/<slug>-body.md.
Write the body only. Do not write the handoff preamble into that file.
Do not write .cheese/age/<slug>.md yourself. The gated writer creates it.
Compute the recommended set first (see ## Handoff).
Set <next> to cure when that set is not empty.
Set <next> to done when that set is empty.
Set <artifact> to the upstream report path from step 2. Use "" only when no upstream report exists.
Set <baseline> to the baseline block from that upstream handoff. Omit --baseline only when the upstream handoff has none.
Run python3 skills/age/scripts/age.pyz write-handoff-artifact --phase age --slug <slug> --status ok --next <next> --artifact "<artifact>" --orientation "<one-line orientation>" --durable-flags "<none | one line per flag>" --baseline "<baseline>" --grounded <path[#start-end]> --body-file ".cheese/age/<slug>-body.md".
Print the path.
Only a new packet for this slug permits recovery; follow references/packet.md § Late evidence.
Other changes require a fresh review.
6. Hand off (see ## Handoff below).
Sub-agent fan-out
Section titled “Sub-agent fan-out”/age plans independent review subjects with the contextual age router (src/easy_cheese/shared/fanout/age_route.py).
See references/fan-out.md, references/packet.md, and references/sub-agent-gate.md for mechanics.
Call each source-code backend through the shared code-intelligence-routing.md contract.
Output
Section titled “Output”Use ../cheese/references/formatting.md for cross-cutting house style and citation form.
This section defines the findings-report shape.
formatting.md defines the voice rules and footnote primitive.
The gated writer writes .cheese/age/<slug>.md.
It puts the minimum handoff slug at the top.
The slug carries status, next, artifact, durable_flags, baseline, and one-line orientation.
Supply each value through the step 5 command.
Write the body below into .cheese/age/<slug>-body.md, with no preamble.
references/report-example.md § Body order defines the section order, the exact
finding format, and the full skeleton. Read it before you write the body.
Use the voice-kernel scale for per-finding confidence: (see references/voice.md § Reasoning posture).
Use certain for direct evidence from a diff/code read or command output.
Use speculating for an inference from an indirect signal.
Do not ship a don't know grading as a finding row.
Gather the missing evidence or drop the claim.
Reserve don't know for the report-level ## Confidence line.
Add suppressed lows to the cure-selection table only when the user passes --full.
Set status: ok when the review completes.
Set status: halt: <reason> when the review cannot reach evidence.
Follow the handback contract.
Derive next from the recommended set that ## Handoff computes.
Set next: cure when that set is not empty.
Set next: done when that set is empty.
Keep every finding in the report, whatever next says.
Set durable_flags: to none by default, as in cook’s gate.
When the plan or host restricts coverage, record the actual restriction in durable_flags and ## Confidence, not a size-only warning.
Record the resolved worker types under ## Agent resolution in the body.
Record dispatch metadata for every topology. Use the fields in report-example.md.
Check supplied dispatch observations with age.pyz review-plan-check before writing; absent observations remain explicitly unobserved.
Print Age report: .cheese/age/<slug>.md.
When press: skipped is set, print the following warning:
Warning: no /press report for <slug> — hardening was skipped. Run /press <slug> first, or continue with /cure.
When the user passes --html, print the HTML path that html-report returns.
The render command appears under --html in ## Inputs.
Handoff
Section titled “Handoff”Pipeline: culture → mold → cook → press → [age] → cure → plate
Compute the recommended set. Use the all-medium, cheap composite.
Include the medium floor (blocker+high+medium).
Also include every Low with fix-cost-now: contained.
Decide whether to act or ask.
This selection is the only one. ## Output derives next from it.
A review with contained lows and no medium finding has a non-empty set.
That review sets next: cure and keeps every low finding in the report.
- Empty set — Set
next: done. Print the report path. Stop. - Reason to ask — Ask when a set member has
fix-cost-now: sprawlingorfix-cost-later: structural. Also ask when two findings conflict. Also ask when the user passes--safe. Readreferences/handoff-detail.mdfirst. Render the gate per../cheese/references/handoff-gate.md. Pre-select the composite. Mark each heavy row. - Otherwise — Act. Announce the selection. Dispatch
/cureperreferences/handoff-detail.md§ Dispatch. Do not render a gate.
--auto substitutes a severity-floor selection and its own chain.
Read references/handoff-detail.md § Auto mode before an --auto run.
That file also defines the no-chain override under /cook’s fan pathway.
Review is not a verdict.
Explain where to look and why.
Do not edit production files.
The step 1 review lock enforces this rule.
/cure applies each fix.
Do not raise a finding for a gate failure identical to the diff’s recorded baseline: block.
Flag only new/changed failures per ../cook/references/quality-gates.md.
Default to acting.
Auto-select the recommended set.
Dispatch /cure without a gate.
Ask first only for a genuine reason or when --safe is active.
Treat an empty recommended set as a clean stop, not a question.
Do not invent evidence.
Cite files, diffs, commands, or unavailable-source notes.
Agree when the diff is fine.
Treat an empty dimension as a valid outcome, not a gap to fill.
Keep confidence qualitative (certain | speculating | don't know) in the report and each finding.
Never use a numeric score.
Give each finding a location and recommendation.
Write recommendation: and optional invariants: per references/report-example.md.
Do not add JSON sidecars or tag-anchored fix payloads.
/cure reads the markdown directly.
Apply references/voice.md (output discipline, reasoning posture, confidence vocabulary).
References
Section titled “References”- Read the generated command inventory in
references/commands.md. - Read
references/dimensions.mdbefore grading a finding. - Read
references/fan-out.mdbefore every review. - Read
references/packet.mdbefore assembling a fan-out context packet. - Read
references/sub-agent-gate.mdbefore a sub-agent dispatch. - Read
references/handoff-detail.mdbefore the selection gate or a/curedispatch. - Read
references/report-example.md§ Body order before writing the report body. - Read
references/packet.md§ Evidence tools and fallbacks before gathering evidence. - Read
references/handoff-detail.md§ Auto mode before an--autorun. - Read
references/voice.mdbefore writing the report. - Read the applicable Rust, TypeScript, Python, Shell, or Go catalog before grading
deslop.
Agent resolution
Section titled “Agent resolution”Resolve every subject worker and fresh-context review through ../cheese/references/agent-resolution.md.
| Work | Preferred types | Permissions/isolation | Minimum power | Effort | Fallback |
|---|---|---|---|---|---|
| Review a diff or assigned subjects | reviewer | read-only, fresh-context | powerful | high | compatible reviewer, then general |
The router sets each assignment’s effort to low, medium, or high.
Pass that assignment value to its worker, not the plan’s quick, normal, or deep review mode.
The report body carries the shared agent_resolution block under ## Agent resolution.
/age bundle commands
Section titled “/age bundle commands”scripts/render_generated_regions.py generates this file from the static COMMANDS manifest in src/easy_cheese/skills/age/commands.py. Do not edit this file. Run each command as python3 skills/age/scripts/age.pyz <command> [args...]. Each command returns an integer exit status. Pass --help to a command for its arguments and output format. Keep worked examples in the skill instructions.
| Command | Purpose | Subcommands |
|---|---|---|
age-route |
Plan scoped review subjects from evidence-bearing context (JSON in, JSON out) | |
artifact-path |
Resolve the durable or transient artifact path for a phase and slug | |
findings |
Render an /age report’s selection table or coder brief and resolve selection verbs | render-table, parse-selection, render-brief |
gates |
Map each quality-gate result to a readiness verdict | classify |
handoff |
Render, parse, and dispatch-split handoff preambles | render, parse, dispatch |
html-report |
Render an /age markdown report into one offline HTML file | |
paths |
Slugify, validate, resolve, and list .cheese artifact paths | slugify, validate, existing, resolve, list, domain-model-target |
read-handoff-slug |
Read the handoff preamble back from a phase artifact | |
render-html |
Render a markdown report into one self-contained offline HTML file | |
review-instructions |
Collect scoped repository and explicit external instruction sources | |
review-lock |
Record the production tree digest that keeps /age review-only | |
review-plan-check |
Check a review plan against supplied dispatch observations (JSON in, JSON out) | |
review-surface |
Score the git surface that the age router sizes (JSON out) | |
severity |
Compute per-finding severity and fix-cost-now buckets | compute, bucket |
slugify |
Derive a kebab-case slug and durable spec path from task text | from-task |
wheypoint-resolve |
Resolve a phase slug through the shared Wheypoint kernel (JSON out) | |
write-handoff-artifact |
Write an age handoff atomically after the review lock verifies the tree; pass –grounded paths |
Go De-slop Catalog
Section titled “Go De-slop Catalog”This section provides Go evidence for the age deslop dimension.
Each pattern names a Go-specific AI signature for review.
Most patterns map to a staticcheck or golangci-lint rule.
These rules give reviewers citable names for findings.
Use this section with the deslop rubric in dimensions.md.
This section provides review details, not a separate severity scale.
1. Error string conventions
Section titled “1. Error string conventions”Go error strings use lowercase text and no trailing punctuation.
Go code wraps returned errors with %w.
// SLOPreturn fmt.Errorf("Failed to open file: %s", err)return errors.New("User not found.")
// CLEANreturn fmt.Errorf("open file: %w", err)return errors.New("user not found")The %w verb wraps the error so callers can use errors.Is/errors.As.
Use %v only when you intentionally want to break the error chain.
Staticcheck ST1005 checks error-string capitalization and punctuation.
errorlint checks %w and %v wrapping.
2. Named returns with bare return
Section titled “2. Named returns with bare return”A bare return in a long function is the defect. A named result is not.
Grade the bare return, not the declaration.
// SLOPfunc getUser(id int) (user *User, err error) { user = db.Find(id) if user == nil { err = errors.New("not found") return // Which values? Have to read the whole function } return}
// CLEANfunc getUser(id int) (*User, error) { user := db.Find(id) if user == nil { return nil, errors.New("user not found") } return user, nil}A named result is correct in several cases that the Go guide permits.
Use one to recover a panic in a defer block.
Use one to document two results of the same type.
Use one to modify a result in a defer block.
Keep the explicit return user, nil form in every case.
The nakedret and revive bare-return linters flag the bare return.
3. context.TODO() permanently
Section titled “3. context.TODO() permanently”AI often generates context.TODO() and leaves it in place.
// SLOPfunc handleRequest(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() result, err := db.Query(ctx, query)}
// CLEAN — use the context you already havefunc handleRequest(w http.ResponseWriter, r *http.Request) { result, err := db.Query(r.Context(), query)}context.TODO() records that the caller has not chosen a context yet.
Replace it with the caller’s context before you ship production code.
context.Background() stays correct at the top of main, in a test, and in an initializer.
4. Pointer to interface
Section titled “4. Pointer to interface”Avoid a pointer to an interface in almost every case. An interface value is a two-word pair: a type and a value pointer. Passing it by value copies that pair, not the concrete data behind it. A pointer to an interface adds one indirection and no benefit.
// SLOPfunc NewService(repo *Repository) *Service { ... }// where Repository is an interface
// CLEANfunc NewService(repo Repository) *Service { ... }5. Goroutine leaks
Section titled “5. Goroutine leaks”AI spawns goroutines without cancellation paths.
// SLOP — runs forever, no way to stop itgo func() { for { doWork() time.Sleep(time.Second) }}()
// CLEAN — respects context cancellationgo func(ctx context.Context) { ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: doWork() } }}(ctx)go.uber.org/goleak catches leaked goroutines during tests.
6. fmt.Sprintf for string concatenation in loops
Section titled “6. fmt.Sprintf for string concatenation in loops”Repeated string building has O(n²) cost.
// SLOPvar result stringfor _, s := range items { result = fmt.Sprintf("%s%s", result, s)}
// CLEANvar b strings.Builderfor _, s := range items { b.WriteString(s)}result := b.String()perfsprint flags this pattern.
7. Stuttering package names
Section titled “7. Stuttering package names”// SLOP — user.UserService, user.UserModelpackage usertype UserService struct{}type UserModel struct{}
// CLEAN — user.Service, user.Modelpackage usertype Service struct{}type Model struct{}Revive exported reports this issue (“type name will be used as user.UserService by other packages”).
8. init() for non-trivial setup
Section titled “8. init() for non-trivial setup”AI puts complex initialization in init().
init() cannot return errors.
init() runs at import time, so callers cannot control it.
// SLOPfunc init() { db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) if err != nil { log.Fatal(err) // Kills the process at import time } globalDB = db}
// CLEAN — explicit initialization the caller controlsfunc NewDB(dsn string) (*sql.DB, error) { return sql.Open("postgres", dsn)}gochecknoinits flags every init() function.
Sources
Section titled “Sources”- The Go wiki covers error strings, naked returns, package-name stutter, and contexts in Code Review Comments (go.dev/wiki/CodeReviewComments).
- The Uber Go guide (github.com/uber-go/guide) covers goroutine lifetimes and
init()avoidance. - The golangci-lint linters index (golangci-lint.run/usage/linters) lists
nakedret,perfsprint,gochecknoinits, and revive rules. go.uber.org/goleakdetects goroutine leaks during tests.
Python De-slop Catalog
Section titled “Python De-slop Catalog”This section provides Python evidence for the age deslop dimension.
Each pattern identifies a Python-specific AI tell for review.
Most patterns map to a Ruff rule code.
The rule code gives reviewers a citable name for a finding.
Use this section with the deslop rubric in dimensions.md.
This section provides review details, not a separate severity scale.
1. range(len()) instead of enumerate
Section titled “1. range(len()) instead of enumerate”AI defaults to C-style index loops.
# SLOPfor i in range(len(items)): print(i, items[i])
# CLEANfor i, item in enumerate(items): print(i, item)Iterate directly when you do not need the index:
for item in items: process(item)2. Redundant None and length checks
Section titled “2. Redundant None and length checks”A truth test is not a None check. It also rejects "", 0, 0.0, [], {}, and False.
Use it only when the empty value and the missing value need the same branch.
# SLOP — three checks where the type allows oneif user is not None and user.name is not None and len(user.name) > 0: greet(user.name)
# CLEAN — when an empty name and a missing name take the same branchif user and user.name: greet(user.name)
# CLEAN — when the branches differ, keep the explicit checkif user is not None and user.name is not None: greet(user.name)Keep is not None on any value that can hold 0, False, or an empty container.
3. Old-style string formatting
Section titled “3. Old-style string formatting”AI mixes %, .format(), and f-strings inconsistently.
# SLOPmessage = "Hello, %s! You have %d messages." % (name, count)message = "Hello, {}!".format(name)
# CLEAN — f-strings for a plain stringmessage = f"Hello, {name}! You have {count} messages."
# CLEAN — %-style for a logging call, which formats only when the record emitslogger.info("Hello, %s! You have %d messages.", name, count)Do not use an f-string in a logging call.
The f-string formats on every call, even when the level filters the record.
Ruff rule G004 flags an f-string in a logging call.
4. Silent except: pass
Section titled “4. Silent except: pass”An unhandled exception that the code swallows leaves no evidence. The defect then costs the most time to find.
# SLOPtry: risky_operation()except Exception: pass # Silent failure: no log, no trace, no evidence
# CLEAN — either handle it meaningfully or don't catch it# If you truly need to ignore: except SpecificError as e: logger.debug(...)5. Raw dicts for structured data
Section titled “5. Raw dicts for structured data”AI returns {"id": 1, "name": "Alice"} instead of a declared type.
A dataclass gives static types, editor support, and a named shape.
A dataclass does not check a type at run time. It assigns whatever the caller passes.
Use pydantic or attrs with validators when the data crosses a trust boundary.
# SLOPdef get_user(): return {"id": 1, "name": "Alice", "email": "alice@example.com"}
# CLEAN@dataclassclass User: id: int name: str email: str6. open() without context manager
Section titled “6. open() without context manager”# SLOPf = open("file.txt")data = f.read()f.close() # Never reached if f.read() throws
# CLEANwith open("file.txt") as f: data = f.read()7. Overzealous type hints on obvious locals
Section titled “7. Overzealous type hints on obvious locals”# SLOPname: str = "Alice"count: int = 0items: list[str] = []active: bool = True
# CLEAN — type hints on function signatures, not obvious assignmentsname = "Alice"count = 0items: list[str] = [] # Empty collection annotation is fine (inference can't know the element type)active = True8. List comprehension where a generator suffices
Section titled “8. List comprehension where a generator suffices”# SLOP — builds entire list in memory just to iteratetotal = sum([x * x for x in range(1_000_000)])
# CLEAN — generator expression, lazy evaluationtotal = sum(x * x for x in range(1_000_000))9. Mutable default arguments
Section titled “9. Mutable default arguments”def f(x=[]) shares one list across every call.
# SLOPdef append_item(item, items=[]): items.append(item) return items
# CLEANdef append_item(item, items=None): if items is None: items = [] items.append(item) return itemsRuff identifies this pattern with B006.
10. HTTP calls without a timeout
Section titled “10. HTTP calls without a timeout”requests and httpx calls without timeout= can hang forever when the server hangs.
# SLOPresponse = requests.get(url)
# CLEANresponse = requests.get(url, timeout=10)Ruff identifies this pattern with S113.
11. try/except shape slop
Section titled “11. try/except shape slop”The tryceratops family covers oversized try blocks with logging noise.
# SLOP — log-and-raise duplicates the traceback up the stacktry: process(item)except ValueError as e: logger.error(f"failed: {e}") # TRY400: use logger.exception raise
# SLOP — raise inside try, caught by its own except (TRY301);# success path buried inside try (TRY300)try: value = compute() if value < 0: raise ValueError("negative") return transform(value)except ValueError: ...
# CLEAN — narrow try, raise outside it, else for the success pathvalue = compute()if value < 0: raise ValueError("negative")try: data = load(value)except OSError: logger.exception("load failed") raiseelse: return transform(data)Ruff identifies this shape with TRY300, TRY301, TRY400, and TRY401.
12. Deprecated typing forms
Section titled “12. Deprecated typing forms”Models that learn from pre-3.9 code emit typing.List/Optional/Union.
# SLOPfrom typing import Dict, List, Optional, Uniondef find(ids: List[int]) -> Optional[Dict[str, Union[int, str]]]: ...
# CLEAN — builtin generics (3.9+) and | unions (3.10+)def find(ids: list[int]) -> dict[str, int | str] | None: ...Ruff identifies this pattern with UP006, UP007, and UP045.
13. os.path / pathlib mixing
Section titled “13. os.path / pathlib mixing”A module interleaves os.path.join, os.path.exists, and Path.
# SLOPpath = os.path.join(base, "config.yaml")if os.path.exists(path): ...
# CLEANpath = Path(base) / "config.yaml"if path.exists(): ...Ruff uses the PTH family for this pattern.
open(path) on a Path is valid.
Core developers contest the PTH123 rule, which forces Path.open().
Do not fix open(path) on a Path solely to satisfy PTH123.
14. print() debugging in library code
Section titled “14. print() debugging in library code”# SLOPprint(f"processing {item}")
# CLEAN — logging, or delete if the code is self-evidentlogger.debug("processing %s", item)15. Non-exhaustive match over a closed union
Section titled “15. Non-exhaustive match over a closed union”AI writes a match or if/elif chain over an enum or Literal union and leaves a silent case _:, a bare else, or no final branch.
A new member then falls through without a type error.
assert_never turns the missing case into a mypy or pyright error, so the checker carries that review.
# SLOPmatch status: case Status.ACTIVE: return activate() case Status.INACTIVE: return deactivate() case _: pass
# CLEANfrom typing import assert_never # typing_extensions before 3.11
match status: case Status.ACTIVE: return activate() case Status.INACTIVE: return deactivate() case _: assert_never(status)
# CLEAN — if/elif formif status is Status.ACTIVE: return activate()elif status is Status.INACTIVE: return deactivate()else: assert_never(status)No Ruff rule covers this. Review it by hand.
Sources
Section titled “Sources”- Ruff rule docs (docs.astral.sh/ruff/rules) verify every rule code above.
typing.assert_never(Python 3.11+,typing_extensionsbefore) documents the exhaustiveness idiom; mypy and pyright both report the unreachable-argument error.- charlax/professional-programming documents error-handling anti-patterns with before-and-after exception examples.
- The
pathlibrule follows the PTH123 dispute thread (discuss.python.org/t/106904).
Rust De-slop Catalog
Section titled “Rust De-slop Catalog”This catalog provides Rust-specific evidence for the age deslop dimension.
Each pattern identifies a Rust-specific AI tell for review.
Most patterns map to a clippy lint.
The lint provides a citable rule name for each finding.
Use this catalog with the deslop rubric in dimensions.md.
This catalog provides the “Look for” details, not a separate severity scale.
1. Excessive .clone() to silence the borrow checker
Section titled “1. Excessive .clone() to silence the borrow checker”LLMs reach for .clone() as a universal fix for ownership errors.
Fix:
- Use borrowing (
&and&mut) instead - Take
&strinstead ofStringin function parameters - Use
.as_ref()onOption/Resultinstead of cloning to unwrap - Ban
.clone()unless you can explain why you need owned data
// SLOPfn greet(name: String) { println!("Hello, {name}"); }let msg = my_string.clone();greet(msg);
// CLEANfn greet(name: &str) { println!("Hello, {name}"); }greet(&my_string);2. .unwrap() everywhere
Section titled “2. .unwrap() everywhere”Excessive .unwrap() calls create runtime panics throughout the codebase.
Fix:
- Use the
?operator to propagate errors - Use
anyhoworthiserrorfor structured errors - Use
if let Some(x)ormatchforOptiontypes - Use
?for every error you can propagate - Use
.unwrap()only when the type system proves the value exists, such as aconstor a checked index - A hardcoded regex is not a compile-time guarantee.
Regex::newparses at run time. UseLazyLockplusexpect("static regex"), or a compile-time macro crate
// SLOPlet file = File::open("config.toml").unwrap();let config: Config = toml::from_str(&contents).unwrap();
// CLEANlet file = File::open("config.toml")?;let config: Config = toml::from_str(&contents)?;3. Treating everything as String
Section titled “3. Treating everything as String”Using String for every value loses type safety and adds unnecessary allocations.
Fix:
- Accept
&strorimpl AsRef<str>as function parameters - Use
Cow<'_, str>when a value may be owned or borrowed - Create newtypes for domain concepts, such as
struct UserId(String)
// SLOPfn find_user(id: String, name: String) -> String { ... }
// CLEANfn find_user(id: &UserId, name: &str) -> Result<User> { ... }4. Index-based loops instead of iterators
Section titled “4. Index-based loops instead of iterators”C-style for i in 0..vec.len() misses safety and optimization.
Fix:
- Use
.iter(),.map(),.filter(),.enumerate(),.collect() - Use slice patterns:
match vec.as_slice() { [first, ..] => ... }
// SLOPfor i in 0..items.len() { process(i, &items[i]);}
// CLEANfor (i, item) in items.iter().enumerate() { process(i, item);}5. Fighting lifetimes with Rc<RefCell<T>>
Section titled “5. Fighting lifetimes with Rc<RefCell<T>>”When ownership gets complex, AI reaches for interior mutability or unsafe.
Fix:
- Reduce borrow lifetimes so they don’t overlap
- Design structs to own their data
- Pass short-lived borrows as method parameters
- Restructure to avoid holding long-lived references
6. Weak assertions
Section titled “6. Weak assertions”The assertions assert!(result.is_ok()) and assert!(result.is_err()) hide the actual error or value when they fail and print only false.
Fix:
- Propagate the error with
?and let the test signature returnResult .expect("context")panics. Use it only when the test cannot returnResult- Check actual values, not just existence
- For errors, verify the specific variant with
matches!or check the message - Add a failure message to every
assert_eq!/assert!with non-obvious operands
// SLOPassert!(result.is_ok());assert!(result.is_err());assert_eq!(count, 3); // no context on failure// CLEAN — propagate the real errorlet value = result.expect("scan_worktree should succeed");assert_eq!(value.label, "Ready");// CLEAN — check specific error variantassert!(matches!(result, Err(MyError::NotFound { .. })));// or check the messagelet err = result.unwrap_err();assert!(err.to_string().contains("not found"), "expected NotFound, got: {err}");// CLEAN — failure message for non-obvious operandsassert_eq!(count, 3, "expected 3 active workers after spawn");7. is_none() / is_some() without value context
Section titled “7. is_none() / is_some() without value context”The assertion assert!(x.is_none()) prints assertion failed: false, while assert_eq! shows the actual value.
Fix:
- Use
assert_eq!(x, None)when the inner type implementsDebugandPartialEq - Use
assert!(matches!(x, None), "got {x:?}")when it implements onlyDebug - Keep
assert!(x.is_none())when the inner type implements neither trait - For
is_some(), extract the inner value and check it
// SLOPassert!(x.is_none());assert!(ping["result"]["host_type"].as_str().is_some());
// CLEANassert_eq!(x, None);assert_eq!(ping["result"]["host_type"].as_str(), Some("daemon"));8. Async timing slop
Section titled “8. Async timing slop”A raw tokio::time::sleep call before assertions is fragile: it passes on fast machines and flakes in CI.
Fix:
- Use a
wait_until_asyncpolling pattern with a timeout - Use sleep-then-assert only to test actual timing behavior
// SLOPtokio::time::sleep(Duration::from_millis(500)).await;assert_eq!(state.status(), "ready");
// CLEAN — poll with timeoutwait_until_async(Duration::from_secs(2), || async { state.status() == "ready"}).await.expect("status should reach ready");9. #[should_panic] without expected
Section titled “9. #[should_panic] without expected”A bare #[should_panic] accepts any panic, including unrelated panics caused by refactoring. Always pin the expected message.
Fix:
- Add
expected = "substring"to match the intended panic message
// SLOP#[test]#[should_panic]fn rejects_empty_input() { parse("");}
// CLEAN#[test]#[should_panic(expected = "input must not be empty")]fn rejects_empty_input() { parse("");}10. No-crash-is-success tests
Section titled “10. No-crash-is-success tests”Tests with zero assertions prove only that the code does not panic, not that it works.
Fix:
- Add assertions on return values or side effects
- If you intentionally test “no panic”, add a comment that explains why
// SLOP#[test]fn stamp_activity_nonexistent_is_noop() { tracker.stamp_activity("ghost-id");}
// CLEAN — document the intent#[test]fn stamp_activity_nonexistent_is_noop() { // No assertion needed: verifying no panic on missing ID tracker.stamp_activity("ghost-id");}11. Lint suppression instead of a fix (#[allow(...)])
Section titled “11. Lint suppression instead of a fix (#[allow(...)])”AI adds #[allow(...)] attributes to silence warnings instead of fixing their causes.
Treat each compiler warning as a problem to fix, not a message to suppress.
Crate-level suppression (always a finding)
Section titled “Crate-level suppression (always a finding)”These attributes suppress warnings globally and do not belong in production code:
// SLOP — suppresses every warning in the crate#![allow(warnings)]#![allow(clippy::all)]
// SLOP — three or more together are an AI signature#![allow(dead_code)]#![allow(unused_imports)]#![allow(unused_variables)]Fix: Delete the allows. Fix each warning individually. Numerous warnings indicate broader code problems, not mere lint noise.
The AI scaffold cluster
Section titled “The AI scaffold cluster”These five attributes together provide the strongest AI signal:
| Attribute | AI excuse | Real fix |
|---|---|---|
allow(dead_code) |
“I’ll wire it up later” | Delete unconnected code |
allow(unused_imports) |
Copied from examples | Remove unused use statements |
allow(unused_variables) |
Bound “just in case” | Prefix with _ or remove |
allow(unused_mut) |
Added mut preemptively |
Remove unnecessary mut |
allow(unused_assignments) |
Assign then overwrite | Remove dead assignment |
Fix: Each attribute has a specific fix, but the allow hides which fix you need. Remove the allow. Read the warning. Apply the appropriate fix.
Clippy suppression smells
Section titled “Clippy suppression smells”Red Flag: These attributes almost always indicate slop because they suppress restrictions.
// SLOP — hiding panic risks#[allow(clippy::unwrap_used)]#[allow(clippy::expect_used)]#[allow(clippy::indexing_slicing)]#[allow(clippy::panic)]
// CLEAN — handle the errorfn get_item(items: &[Item], idx: usize) -> Option<&Item> { items.get(idx)}// SLOP — incomplete code in CI#[allow(clippy::todo)]#[allow(clippy::unimplemented)]#[allow(clippy::dbg_macro)] // debug macros left in source
// CLEAN — ship nothing with these lints suppressed// SLOP — logging-aware code ignored#[allow(clippy::print_stdout)]#[allow(clippy::print_stderr)]
// CLEAN — use a logging framework (tracing, log, slog)tracing::info!("event happened");// SLOP — weak error handling#[allow(clippy::result_unit_err)] // Result<T, ()> is useless for error context
// CLEAN — use a real error typefn parse_config(s: &str) -> Result<Config, ConfigError> { ... }Yellow Flag: These attributes often indicate slop; check the context before removing them.
// SLOP (often) — hiding complexity debt#[allow(clippy::too_many_arguments)]#[allow(clippy::too_many_lines)]
// CLEAN — decompose the function// SLOP (often) — legitimate in some contexts (async move blocks, trait impls)#[allow(clippy::needless_pass_by_value)]#[allow(clippy::cognitive_complexity)]
// Check: does the suppression hide a real refactoring opportunity?Blue Flag: These attributes express style preferences and do not necessarily indicate slop.
// Acceptable — pedantic lints are opt-in for a reason#[allow(clippy::cast_possible_truncation)]#[allow(clippy::cast_sign_loss)]#[allow(clippy::module_name_repetitions)]#[allow(clippy::wildcard_imports)]
// These are in "pedantic" (not "restriction"), so suppressing them// is more defensible. Still check the reason.Naming convention suppressions
Section titled “Naming convention suppressions”Three together suggest that the author came from Python/Java, not Rust:
// SLOP#![allow(non_snake_case)]#![allow(non_camel_case_types)]#![allow(non_upper_case_globals)]
// CLEAN — use Rust conventions// snake_case for functions, CamelCase for types, SCREAMING for constantsException: FFI modules that wrap C libraries may need non_snake_case or non_camel_case_types to match the C API.
The “debug and print” tells
Section titled “The “debug and print” tells”These three patterns almost certainly indicate hastily generated code:
// SLOP — debug macro left in sourcefn process(data: &[u8]) { #[allow(clippy::dbg_macro)] dbg!(data); // this went to production // ...}
// CLEAN — remove the debug macro entirelyfn process(data: &[u8]) { tracing::debug!(?data); // use structured logging // ...}// SLOP — println instead of logging#[allow(clippy::print_stdout)]println!("Processing file: {}", path);
// CLEAN — use a logging frameworktracing::info!(file = %path, "Processing file");// SLOP — placeholder error type#[allow(clippy::result_unit_err)]fn load_config(path: &str) -> Result<Config, ()> { // caller has no idea what went wrong}
// CLEAN — define a real error type#[derive(Debug)]pub enum ConfigError { NotFound(String), InvalidFormat { line: usize, reason: String },}
fn load_config(path: &str) -> Result<Config, ConfigError> { // caller can now handle specific errors}Redundant allows
Section titled “Redundant allows”This allow duplicates behavior that the language already provides:
// SLOP — `_name` already suppresses unused_variables#[allow(unused_variables)]fn process(_name: &str, _config: &Config) { ... }
// SLOP — pub items can't be dead code (compiler perspective)#[allow(dead_code)]pub fn my_function() { ... }
// CLEAN — just use the underscore prefixfn process(_name: &str, _config: &Config) { ... }Scope matters
Section titled “Scope matters”A wider allow scope increases the risk of hidden problems:
| Scope | Severity | Example |
|---|---|---|
Crate-level #![allow(...)] |
High | Suppresses across entire crate |
Module-level #[allow(...)] on mod |
Medium | Blanket suppression for module |
| Function-level | Low | Targeted, possibly legitimate |
| Statement-level | Lowest | Precise suppression with clear reason |
Rule: Scope each allow to the narrowest possible target. Add a comment that explains why.
// Acceptable — narrow scope, clear reason#[allow(clippy::too_many_arguments)] // mirrors the C FFI signature exactlyfn ffi_create_window(x: i32, y: i32, w: i32, h: i32, flags: u32) -> *mut Window { ... }Tier system for evaluation
Section titled “Tier system for evaluation”Clippy groups lints into categories. Use these categories as a heuristic when you judge whether a suppression is legitimate:
| Category | Philosophy | Example | Suppression OK? |
|---|---|---|---|
| restriction | “Don’t do this” | unwrap_used, panic, todo, print_stdout |
🔴 Almost never |
| correctness | “This is likely wrong” | Most logic bugs | 🔴 Almost never |
| complexity | “This is confusing” | too_many_arguments, type_complexity |
🟡 With justification |
| perf | “This is slow” | clone_on_copy, inefficient_to_string |
🟡 Document why |
| style | “Use X instead” | let_and_return, wildcard_imports |
🟡 Preference |
| pedantic | “Extra strict” | cast_possible_truncation, module_name_repetitions |
🟢 Usually OK |
Rule: Do not suppress restriction lints casually.
Treat Pedantic lints as more defensible because they are opt-in.
Justify each Complexity lint suppression.
Legitimate uses (don’t flag these)
Section titled “Legitimate uses (don’t flag these)”Test code:
#[allow(dead_code)]on test utility functions#[allow(unused)]inmod testsblocks#[allow(clippy::unwrap_used)]on a test function, scoped to that item
Framework integration:
#[allow(unused)]on trait impls required by framework (async frameworks often have dead-looking methods)#[allow(clippy::must_use_candidate)]when the framework signature doesn’t support#[must_use]
Intentional design:
#[allow(clippy::pedantic)]at crate level (pedantic lints are opt-in)#[allow(clippy::cognitive_complexity)]on state machines or DSLs (legitimately complex, not a bug)
FFI/interop:
#[allow(non_snake_case)]/non_camel_case_typesmatching C signatures#[allow(unsafe_code)]when wrapping C libraries
Generated code:
build.rsoutput- Protobuf/gRPC generated files
- Macro-generated code inside the macro itself
#[cfg_attr(feature = "generated", allow(...))]for optional generated modules
One suppression rule. Judge each #[allow(...)] by its scope and its context.
| Scope | Verdict |
|---|---|
Crate level (#![allow(...)]) |
Slop, except for an opt-in group such as clippy::pedantic |
| Item level in production code | Slop for a restriction lint such as unwrap_used, panic, todo, or print |
| Item level in a test, a generated file, or an FFI binding | Acceptable, including a restriction lint |
Require a comment that names the reason on every allow that this table accepts.
12. Hallucinated APIs and deprecated syntax
Section titled “12. Hallucinated APIs and deprecated syntax”AI generates nonexistent functions or uses outdated API patterns, such as clap App::new instead of derive macros.
Fix:
- Run
cargo checkimmediately after generating code - Pin specific crate versions
- Use Clippy:
cargo clippy -- -W clippy::all - When in doubt, check docs against the current crate version
13. Deref polymorphism (fake inheritance)
Section titled “13. Deref polymorphism (fake inheritance)”Implementing Deref on a wrapper so it “inherits” the inner type’s methods simulates OO inheritance, which Rust deliberately lacks.
// SLOPstruct AppConfig { base: Config }impl Deref for AppConfig { type Target = Config; fn deref(&self) -> &Config { &self.base }}
// CLEAN — delegate explicitly, or implement the shared traitimpl AppConfig { fn timeout(&self) -> Duration { self.base.timeout() }}Use Deref for smart pointers.
No clippy lint catches this pattern, so review it by hand (rust-unofficial/patterns, anti-patterns chapter).
14. Boxing reflex
Section titled “14. Boxing reflex”Using Box or Arc when plain ownership or a borrow works adds unnecessary indirection to avoid borrow-checker errors.
// SLOPfn process(data: &Box<MyStruct>) { ... } // borrowed_boxstruct Registry { items: Vec<Box<String>> } // vec_boxlet cfg: Box<Config> = Box::new(Default::default()); // box_default
// CLEANfn process(data: &MyStruct) { ... }struct Registry { items: Vec<String> }let cfg = Config::default();clippy reports borrowed_box, vec_box, box_collection, and box_default.
In the inverse case, you SHOULD box a very large enum variant (large_enum_variant).
Boxing is not inherently wrong, but unmotivated boxing is wrong.
15. async fn with no .await
Section titled “15. async fn with no .await”Functions marked async by habit add unnecessary async behavior.
An async fn that never awaits forces every caller into async machinery for no reason.
// SLOPasync fn config_path() -> PathBuf { dirs::config_dir().expect("config dir").join("app")}
// CLEANfn config_path() -> PathBuf { ... }unused_async has false-negative gaps, so check async functions by hand.
16. #![deny(warnings)]
Section titled “16. #![deny(warnings)]”#![deny(warnings)] turns every future compiler warning into a build break.
The crate then stops compiling when a new toolchain adds a lint.
// SLOP#![deny(warnings)]
// CLEAN — leave the crate free of a global denyEnforce the warning budget in CI, on a pinned toolchain, with an explicit lint list.
Do not replace the attribute with RUSTFLAGS="-D warnings" on a floating toolchain.
That replacement breaks the same build for the same reason.
The rust-unofficial/patterns anti-patterns chapter documents this pattern.
17. anyhow::Error in a library’s public API
Section titled “17. anyhow::Error in a library’s public API”Use anyhow in applications, not in public library APIs.
Libraries that return anyhow::Error give callers no concrete error type to match.
// SLOP (in a lib crate)pub fn parse(s: &str) -> anyhow::Result<Config> { ... }
// CLEAN — concrete error type; thiserror for the boilerplate#[derive(Debug, thiserror::Error)]pub enum ParseError { #[error("invalid syntax at line {0}")] Syntax(usize),}pub fn parse(s: &str) -> Result<Config, ParseError> { ... }Convention, not a lint — check whether the crate is a lib or a bin before
flagging. anyhow in binaries and tests is fine.
18. unsafe to make it compile
Section titled “18. unsafe to make it compile”Pressure to move quickly causes agents to use unsafe to escape the borrow checker.
Bun’s audit of its AI-assisted Zig→Rust port found 13,365 unsafe call sites that required review.
For every unsafe block, ask whether a safe alternative exists.
Confirm that the code documents the invariant.
Confirm that tests cover the block.
// SLOP — no SAFETY comment, no bounds reasoninglet val = unsafe { *ptr.add(i) };
// CLEAN — safe alternative existed all alonglet val = slice.get(i).copied().ok_or(Error::OutOfBounds)?;
// If unsafe is genuinely required:// SAFETY: i < self.len is checked by the caller contract above.Sources
Section titled “Sources”- rust-unofficial/patterns (Rust Design Patterns book) — the official anti-pattern chapter
- clippy lint list (rust-lang.github.io/rust-clippy/master) — ground truth for every lint named above
- Bun unsafe audit (bun.com/bun-unsafe-audit) — quantified case study of AI-agent Rust output
Shell / Bash De-slop Catalog
Section titled “Shell / Bash De-slop Catalog”This catalog gives per-language evidence for the age deslop dimension.
Each pattern names a shell-specific AI signature for review.
Most patterns map to a ShellCheck code, which gives a citable rule name for a finding.
Use this catalog with the deslop rubric in dimensions.md.
This catalog supplies the detail. It defines no separate severity scale.
Detect the shell first
Section titled “Detect the shell first”Read the shebang line and the file extension before you grade a pattern. These rules are Bash rules unless a pattern says otherwise.
| Shell | Rules that apply |
|---|---|
#!/bin/bash, #!/usr/bin/env bash, .bash |
Every rule in this catalog |
#!/bin/sh, .sh with no shebang, a POSIX target |
Quoting and cd rules only. [[ ... ]] is a syntax error in POSIX shell. Use [ ... ] there |
| A file that another script sources | See the sourced-file rules under each pattern |
Do not raise a [[ ... ]] finding against a POSIX script.
Do not raise a strict-mode finding against a sourced file.
1. Unquoted variables
Section titled “1. Unquoted variables”This pattern causes the number-one shell bug. Unquoted variables break on spaces, globs, and empty values.
# SLOPfor file in $files; do rm $filedone
# CLEANfor file in "${files[@]}"; do rm -- "$file"doneQuote every variable expansion: "$var", "${array[@]}", "$(command)". The -- stops option parsing and protects against filenames that start with -.
2. Missing or incomplete set -euo pipefail
Section titled “2. Missing or incomplete set -euo pipefail”AI scripts either omit strict mode or use only set -e, without -u and -o pipefail. Both patterns are dangerous.
# SLOP — no strict mode#!/bin/bashcd /some/directory # Might fail silentlyrm -rf build/ # Now you're deleting in the wrong place
# SLOP — partial strict mode (common AI output)#!/bin/bashset -eyq '.items[]' file.yaml | while read -r item; do # yq failure silently ignored process "$item"done
# CLEAN#!/bin/bashset -euo pipefailcd /some/directoryrm -rf build/- The
-eoption makes the shell exit after an error. - The
-uoption reports undefined variables and catches typos such as$UESRinstead of$USER. - The
-o pipefailoption makes a pipeline fail when any command fails, not only the last command.
Use all three flags together in an executable script.
set -e alone is a half-measure. A script can silently swallow a left-side failure when it pipes through jq, yq, or grep.
Do not set strict mode in a file that another script sources. The options stay set in the calling shell after the source returns. An interactive shell can then exit on the next unset variable. Set the options inside each function of a sourced file instead.
Strict mode does not catch every failure
Section titled “Strict mode does not catch every failure”set -e has documented gaps (BashFAQ/105). Do not assume that it catches every failure:
# MASKED — `local`'s own success hides the command's failurelocal output=$(failing_cmd) # -e does NOT fire
# CLEAN — declare and assign in two stepslocal outputoutput=$(failing_cmd) # -e fires here
# MASKED — -e is disabled inside a function used as a conditionalif my_func; then ... # failures inside my_func won't exit3. Parsing ls output
Section titled “3. Parsing ls output”The ls output is not machine-readable. Filenames with spaces, newlines, or special characters break parsers.
# SLOPfor file in $(ls *.txt); do process "$file"done
# CLEAN — glob directlyfor file in *.txt; do [[ -f "$file" ]] && process "$file"done
# CLEAN — find for a recursive searchfind . -name '*.txt' -print0 | while IFS= read -r -d '' file; do process "$file"done
# CLEAN — fd only when the project already declares it as a dependencyfd -e txt -x process {}Prefer the standard command. Use fd only when the project declares it.
4. Useless use of cat
Section titled “4. Useless use of cat”# SLOPcat file.txt | grep "pattern"cat file.txt | wc -l
# CLEANgrep "pattern" file.txtwc -l < file.txt5. Backticks instead of $()
Section titled “5. Backticks instead of $()”Backticks do not nest and are harder to read.
# SLOPresult=`command`nested=`echo \`date\``
# CLEANresult=$(command)nested=$(echo "$(date)")6. [ ] instead of [[ ]]
Section titled “6. [ ] instead of [[ ]]”In Bash, [[ ]] is safer. It prevents word splitting, supports regular expressions, and avoids quoting surprises.
[[ ]] is a Bash keyword. It fails in POSIX shell with [[: not found.
Grade this pattern only when the shebang names Bash.
# SLOPif [ $var = "value" ]; thenif [ -z $maybe_empty ]; then
# CLEANif [[ "$var" == "value" ]]; thenif [[ -z "${maybe_empty:-}" ]]; then7. Hardcoded paths
Section titled “7. Hardcoded paths”AI writes absolute paths or assumes CWD.
# SLOPsource /home/user/project/lib/utils.shconfig_file=./config.yaml
# CLEANSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"source "${SCRIPT_DIR}/lib/utils.sh"config_file="${SCRIPT_DIR}/config.yaml"8. Not using readonly for constants
Section titled “8. Not using readonly for constants”# SLOPMAX_RETRIES=3BASE_URL="https://api.example.com"
# CLEANreadonly MAX_RETRIES=3readonly BASE_URL="https://api.example.com"9. Using echo for error messages
Section titled “9. Using echo for error messages”Write errors to stderr, not stdout.
# SLOPecho "Error: file not found"exit 1
# CLEANecho >&2 "Error: file not found"exit 1
# Or with a helperdie() { echo >&2 "$@"; exit 1; }die "file not found"10. Checking $? instead of the command
Section titled “10. Checking $? instead of the command”# SLOPsome_commandif [ $? -eq 0 ]; then echo "ok"fi
# CLEANif some_command; then echo "ok"fi
# CLEAN — error pathif ! some_command; then die "some_command failed"fiShellCheck rule SC2181 flags this pattern.
11. cd without a fallback
Section titled “11. cd without a fallback”This pattern has the highest consequence. A failed cd caused by a typo or permission issue lets every following command, including rm -rf, run in the wrong directory.
# SLOPcd "$build_dir"rm -rf ./*
# CLEAN — in an executable scriptcd "$build_dir" || exit 1rm -rf ./*
# CLEAN — in a function of a sourced filecd "$build_dir" || return 1rm -rf ./*Use exit only in an executable script.
exit inside a sourced file terminates the calling shell.
Use return in every function of a sourced file.
ShellCheck rule SC2164 flags this pattern.
12. Iterating command output with for
Section titled “12. Iterating command output with for”for x in $(cmd) splits output on whitespace instead of lines. This behavior breaks on spaces and globs.
# SLOPfor f in $(find . -name '*.log'); do process "$f"done
# CLEAN — NUL-delimited for filenamesfind . -name '*.log' -print0 | while IFS= read -r -d '' f; do process "$f"done
# CLEAN — line-oriented command outputreadarray -t lines < <(cmd)ShellCheck rule SC2044 covers a find loop.
ShellCheck rule SC2046 covers an unquoted $(...) expansion.
13. Piping into while read and losing variables
Section titled “13. Piping into while read and losing variables”Each side of a pipe runs in a subshell. Assignments inside the loop disappear when the subshell exits.
# SLOP — prints 0count=0cat file | while read -r line; do count=$((count + 1))doneecho "$count"
# CLEAN — redirect (or process-substitute); no subshellcount=0while read -r line; do count=$((count + 1))done < file14. echo -e / echo -n
Section titled “14. echo -e / echo -n”echo flags behave differently in the Bash builtin and /bin/echo. These flags are not POSIX-portable.
# SLOPecho -e "line1\nline2"echo -n "no newline"
# CLEANprintf '%s\n' "line1" "line2"printf '%s' "no newline"15. expr, let, $[ ] arithmetic
Section titled “15. expr, let, $[ ] arithmetic”These forms start external processes or use deprecated syntax for operations the shell performs natively.
# SLOPi=$(expr $i + 1)let i=i+1result=$[ a + b ]
# CLEAN(( i += 1 ))result=$(( a + b ))The Google Shell Style Guide says to always use (( )) or $(( )) for arithmetic.
16. Bare $@ / $* for argument forwarding
Section titled “16. Bare $@ / $* for argument forwarding”Unquoted $@ and $* split on internal spaces and drop empty arguments.
# SLOPmy_func $@
# CLEANmy_func "$@"Sources
Section titled “Sources”- The ShellCheck wiki (shellcheck.net/wiki/SCxxxx) provides canonical slop-to-fix rationale for each code.
- Greg’s Wiki, including BashPitfalls and BashFAQ/105, provides calibration guidance for
set -e. - The Google Shell Style Guide covers arithmetic, quoting, loop idioms, and when not to use Bash.
TypeScript / JavaScript De-slop Catalog
Section titled “TypeScript / JavaScript De-slop Catalog”This catalog gives per-language evidence for the age deslop dimension.
Each pattern names a TypeScript or JavaScript AI signature for review.
Most patterns map to a typescript-eslint rule, which gives a citable rule name for a finding.
Use this catalog with the deslop rubric in dimensions.md.
This catalog supplies the detail. It defines no separate severity scale.
1. any instead of a real type
Section titled “1. any instead of a real type”When types become complex, AI gives up and uses any, which discards TypeScript’s type safety.
// SLOPfunction processData(data: any): any { return data.value;}
// CLEANfunction processData<T extends { value: unknown }>(data: T): T['value'] { return data.value;}
// Or if you genuinely don't know the shape:function processData(data: unknown): unknown { if (hasValue(data)) return data.value; throw new Error("missing value field");}2. .then() chains instead of async/await
Section titled “2. .then() chains instead of async/await”AI mixes paradigms or uses promise chains when async/await provides clearer code.
// SLOPfunction fetchUser(id: string) { return fetch(`/api/users/${id}`) .then(res => res.json()) .then(data => data.user) .catch(err => console.error(err));}
// CLEANasync function fetchUser(id: string): Promise<User> { const res = await fetch(`/api/users/${id}`); return (await res.json()).user; // Let errors propagate — the caller should decide what to do}3. console.log debugging left in
Section titled “3. console.log debugging left in”AI adds debug logging and leaves it in the code.
// SLOPconsole.log("Fetching user...");const user = await fetchUser(id);console.log("User fetched:", user);console.log("Processing...");Fix: Delete all console.log debug statements. Use a proper logger when you need observability. Remove the statements when the code is self-evident.
4. Array.forEach with async callbacks
Section titled “4. Array.forEach with async callbacks”forEach does not await asynchronous callbacks. Those callbacks run, but their promises become unobserved.
// SLOP — these await calls do nothing usefulitems.forEach(async (item) => { await processItem(item); // Runs concurrently, forEach doesn't wait});
// CLEAN — sequentialfor (const item of items) { await processItem(item);}
// CLEAN — concurrent with controlawait Promise.all(items.map(item => processItem(item)));5. Redundant null checks TypeScript already handles
Section titled “5. Redundant null checks TypeScript already handles”With strictNullChecks, the compiler enforces null safety.
// SLOP — name can't be undefined here, the type says string | nullfunction greet(name: string | null): string { if (name === null || name === undefined) { return "Hello, stranger"; } return `Hello, ${name}`;}
// CLEAN — the `undefined` arm is unreachable under the declared typefunction greet(name: string | null): string { return name === null ? "Hello, stranger" : `Hello, ${name}`;}Keep the null semantics. Do not replace a null check with a truth test.
A truth test also rejects "", 0, NaN, and false.
Use name ?? "stranger" when you want null and undefined only.
Lint rule @typescript-eslint/no-unnecessary-condition catches a condition that always evaluates to true or false.
6. JSON.parse(JSON.stringify()) for deep cloning
Section titled “6. JSON.parse(JSON.stringify()) for deep cloning”// SLOPconst cloned = JSON.parse(JSON.stringify(user));
// CLEAN — when the value holds only structured-cloneable dataconst cloned = structuredClone(user);Check the clone requirements first.
structuredClone throws on a function, a class instance method, a Symbol, and a DOM node.
It keeps a Date, a Map, a Set, and a cyclic reference, which the JSON round trip loses.
Use a library deep clone when the value holds a function or a class instance.
7. Redundant type annotations on initialized variables
Section titled “7. Redundant type annotations on initialized variables”// SLOPconst count: number = 0;const name: string = user.name;const isActive: boolean = true;const users: User[] = getUsers();
// CLEAN — inference handles theseconst count = 0;const name = user.name;const isActive = true;const users = getUsers(); // Return type already typed
// Keep annotations on empty collections or ambiguous initializersconst users: User[] = [];8. Importing more names than the file uses
Section titled “8. Importing more names than the file uses”// SLOP — five names where the file uses oneimport { UserService, UserModel, UserDTO, UserMapper, UserValidator } from "./users";
// CLEAN — import only what you useimport { UserService } from "./users";A named barrel import does not load every export by itself.
A bundler drops the unused names when three conditions hold.
The package sets "sideEffects": false, the modules use ECMAScript syntax, and the build runs tree shaking.
Raise this finding when one of those conditions fails, or when the file imports names it never uses.
9. Non-null assertion as narrowing substitute
Section titled “9. Non-null assertion as narrowing substitute”The ! operator suppresses compiler checks. A guard establishes a true condition for the compiler. AI-authored PRs use ! and as far more often than human PRs (arXiv 2602.17955).
// SLOPconst user = users.find(u => u.id === id)!;processUser(user);
// CLEANconst user = users.find(u => u.id === id);if (!user) throw new Error(`unknown user: ${id}`);processUser(user);Lint rule @typescript-eslint/no-non-null-assertion detects non-null assertions.
10. Double assertion to force a type
Section titled “10. Double assertion to force a type”The assertion as unknown as T lets any value claim type T. It bypasses the type system at the location most likely to contain an error.
// SLOPconst config = JSON.parse(raw) as unknown as Config;
// CLEAN — validate at the boundaryconst config = configSchema.parse(JSON.parse(raw)); // zod or similar11. @ts-ignore instead of @ts-expect-error
Section titled “11. @ts-ignore instead of @ts-expect-error”@ts-ignore permanently suppresses a diagnostic. The stale directive remains after you fix the underlying error. @ts-expect-error fails when no diagnostic remains to suppress.
// SLOP// @ts-ignorelegacyCall(data);
// CLEAN// @ts-expect-error — legacy API typed wrong upstream (issue #123)legacyCall(data);Configure the lint rule @typescript-eslint/ban-ts-comment with minimumDescriptionLength.
12. Floating promises
Section titled “12. Floating promises”Fire-and-forget asynchronous calls can lose rejections and create accidental ordering.
// SLOPsaveUser(user); // not awaited — errors disappearitems.map(async i => await process(i)); // array of dropped promises
// CLEANawait saveUser(user);await Promise.all(items.map(i => process(i)));
// Intentionally fire-and-forget? Handle the rejection as well:void saveUser(user).catch((error: unknown) => logger.error({ error }));void only marks the intent for the linter. It does not handle a rejection.
An unhandled rejection still reaches the process handler.
Attach a .catch to every promise that you do not await.
Relevant lint rules include @typescript-eslint/no-floating-promises and @typescript-eslint/no-misused-promises.
13. enum where a union suffices
Section titled “13. enum where a union suffices”Enums often reflect habits from other languages. Literal unions erase during compilation, serialize directly, and need no runtime object.
// SLOPenum Status { Active = "active", Inactive = "inactive" }
// CLEANtype Status = "active" | "inactive";
// When you need the values at runtime:const STATUSES = ["active", "inactive"] as const;type Status = (typeof STATUSES)[number];14. Catch-block slop
Section titled “14. Catch-block slop”A catch (e: any) block can log and rethrow the error at every level. No level handles the error.
// SLOPtry { await handler(req);} catch (e: any) { console.error(e); throw e;}
// CLEAN — catch only where you add value; the error is unknown, not anytry { await handler(req);} catch (e) { if (e instanceof ValidationError) return res.status(400).json(e.detail); throw e; // the boundary logger handles the rest}Lint: @typescript-eslint/only-throw-error,
@typescript-eslint/use-unknown-in-catch-callback-variable.
That rule covers a .catch(callback) argument only.
Cite @typescript-eslint/no-explicit-any for a catch (e: any) clause.
Set useUnknownInCatchVariables in tsconfig.json to type the clause as unknown.
15. useEffect for derived state (React)
Section titled “15. useEffect for derived state (React)”AI often uses effects to compute values or chain fetches.
// SLOP — derived state via effectconst [fullName, setFullName] = useState("");useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
// CLEAN — derive during renderconst fullName = `${first} ${last}`;No lint rule catches this pattern. react-hooks/exhaustive-deps does not catch it. Review it manually. See the react.dev article “You Might Not Need an Effect”.
16. Non-exhaustive switch over a discriminated union
Section titled “16. Non-exhaustive switch over a discriminated union”AI writes a switch on a discriminant and omits the default arm, or fills it with a silent break.
A new union member then compiles and falls through.
An assertNever default makes the missing case a compile error, so the type checker carries that review.
// SLOPswitch (event.kind) { case "open": return onOpen(event); case "close": return onClose(event); default: break;}
// CLEAN — report only the discriminant; the payload may carry secrets or PIIfunction assertNever(value: never): never { throw new Error(`Unhandled case: ${String((value as { kind: string }).kind)}`);}
switch (event.kind) { case "open": return onOpen(event); case "close": return onClose(event); default: return assertNever(event);}typescript-eslint switch-exhaustiveness-check flags the missing cases. It is opt-in and type-aware; no preset enables it.
Sources
Section titled “Sources”- The typescript-eslint
strict-type-checkedconfiguration and rule documentation provide the source of truth for every rule named above, exceptswitch-exhaustiveness-check, which its own rule page documents as opt-in. - Chapter 5 of Effective TypeScript, 2nd ed. (Vanderkam, 2024), covers narrowing
any’s scope. - The arXiv 2602.17955 study provides an empirical AI-versus-human PR comparison of
!/asoveruse.
Review dimensions
Section titled “Review dimensions”Each dimension has its own rubric.
Each dimension answers what kind of problem. Severity answers how bad this one is. The two remain orthogonal.
Severity vocabulary
Section titled “Severity vocabulary”Four tiers, in order:
blocker > high > medium > low| Tier | Meaning |
|---|---|
blocker |
Do not merge when the contract breaks, exposure stays open, or data remains at risk |
high |
Fix before merge when the code risks an incident or rework |
medium |
Fix the real defect before the next release |
low |
Merge safely and fix the annoyance later |
Severity computation
Section titled “Severity computation”Compute each finding’s severity. Do not declare it.
Start at the base tier. Then apply each bump in the order below.
Both bumps can apply to one finding. Cap the result at blocker.
- Base — Use the dimension’s per-tier rubric (see § Per-dimension rubrics below).
- Location bump — Add one tier when
location = contractand the dimension is location-sensitive (see § Location sensitivity). - Compounding bump — Add one tier when
fix-cost-later = structural.
Do not compute the formula mentally. Invoke src/easy_cheese/shared/severity.py compute:
python3 skills/age/scripts/age.pyz severity compute \ --dimension <dim> --base <low|medium|high|blocker> \ --location <class|module|cross-module|contract> \ --fix-cost-later <contained|spreading|structural># -> blocker | high | medium | lowA class-private encapsulation leak lands at low. The same leak at a slice’s index re-export lands at blocker (base high → contract bump → structural fix-cost bump, capped).
Per-finding fields
Section titled “Per-finding fields”Every finding carries these fields:
| Field | Values | Source |
|---|---|---|
dimension |
correctness, security, encapsulation, spec, complexity, deslop, assertions, nih, efficiency, telemetry, conventions, altitude | reviewer-tagged |
severity |
blocker / high / medium / low |
computed (formula above) |
location |
class / module / cross-module / contract |
reviewer-classified |
fix-cost-now |
contained / moderate / sprawling |
bucketed from blast-radius count |
fix-cost-later |
contained / spreading / structural |
reviewer-classified |
confidence |
certain / speculating |
reviewer-assigned per the voice-kernel scale (voice.md); don't know findings are never emitted |
recommendation |
one-line action; /cure implements it as the locked fix decision |
reviewer |
invariants |
optional must-hold: <X>; must-not: <Y>; add on blocker / high when the fix could break a neighbour |
reviewer |
Location classification
Section titled “Location classification”| Tier | Definition |
|---|---|
class |
The scope stays inside one class / type / file’s private scope. The caller graph stays inside the file. |
module |
The scope stays within one module / slice. Calls cross files but stay inside the slice’s internal namespace. |
cross-module |
The caller reaches another module’s internals and bypasses the public index/crust. |
contract |
The caller crosses an ingress/egress boundary, such as a public slice index, HTTP/RPC handler signature, DB schema, language-FFI boundary, plugin extension point, or published library API. |
In projects without an explicit public-index layer, classify a direct import of another file’s internal function across a package boundary as cross-module. This includes flat scripts and packages without an __init__ re-export surface. Classify CLI argv / stdin ingress as contract.
Location sensitivity
Section titled “Location sensitivity”Apply the contract bump only to dimensions where boundary position changes finding impact:
| Dimension | Contract bump? | Why |
|---|---|---|
| correctness | yes | A contract bug reaches every consumer; an internal bug stays contained |
| security | yes | A tainted input crosses a trust boundary |
| encapsulation | yes | This dimension measures boundary integrity |
| spec | yes | Spec drift at the API surface contradicts the published contract |
| complexity | no | Complexity grades function/file shape, not boundary position |
| deslop | no | Dead code stays dead wherever it lives |
| assertions | no | Test quality does not change with SUT location |
| nih | yes | Reinvented primitives that cross the boundary cause more harm than internal helpers |
| efficiency | yes | A public handler on a hot path shows the typical blocker shape |
| telemetry | yes | A boundary outbound call with silent failure forms the canonical blocker |
| conventions | no | An explicit rule remains independently evidenced wherever it occurs; boundary position does not change the convention claim |
| altitude | no | Placement quality is graded from its symptom and concrete cost, not from API location alone |
Fix-cost-now
Section titled “Fix-cost-now”“How hard would it be to fix this right now?”
Count files in the proposed fix’s blast radius. Do not bucket the count mentally. Pipe raw file/module counts through src/easy_cheese/shared/severity.py bucket:
python3 skills/age/scripts/age.pyz severity bucket --files <N> [--modules <M>]# -> contained | moderate | sprawlingSource priority for the raw count:
tilth_deps— primary. It returns the file set that needs changes.- LSP
find-references/find-callers— fallback when tilth is unavailable.
Worked recipe. Start with a finding at path:line. Run tilth_deps on the containing file. Count distinct files in the imported-by set. Use the <N> dependents header count for --files. The Used by list reports one entry per call site. Several entries can identify one file, so raw entries overcount. Use each logical package root to count distinct slice/module roots for --modules. For example, src/easy_cheese/skills/melt and src/easy_cheese/skills/affinage count as two modules. Do not count the shared src/easy_cheese/skills parent. Then run python3 skills/age/scripts/age.pyz severity bucket --files <N> --modules <M>. If tilth_deps is unavailable, use LSP callers. Count distinct touched files and distinct module directories in the same way. This method keeps the buckets comparable.
Report Fix-cost-now; do not bump severity with it. Severity selects fixes. Fix-cost-now explains effort and supports triage scheduling.
Fix-cost-later (compounding)
Section titled “Fix-cost-later (compounding)”“How much harder does this get if we leave it?”
| Tier | Meaning |
|---|---|
contained |
Cost stays roughly fixed. A typo in a docstring takes the same effort in six months. |
spreading |
Cost grows linearly. New code extends the bad pattern; each new caller adds one unit of fix work. |
structural |
Cost grows non-linearly. Consumers harden against the current shape. They re-export types, calcify mocks, and build downstream APIs on the leak. Public-API leaks, DB-schema mistakes, and ingress-contract violations belong here. |
Decision. Mark structural when a consumer re-exports the changed symbol. Also mark structural when the fix touches a file outside the diff. Mark spreading when the fix stays local but the diff adds callers of the bad shape. Also mark spreading when multiple sites copy the pattern. Otherwise, mark contained. When two tiers apply, choose the higher tier.
Per-dimension structural anchors:
| Dimension | structural looks like |
|---|---|
| correctness | A race or lost write reaches a public API boundary. Consumers harden retry/mock logic around broken atomicity. |
| security | A taint path crosses a published signature. Every consumer must validate again after the contract leaks the unsafe shape. |
| encapsulation | A slice index re-exports a leaked internal type. Downstream slices build on it. |
| spec | A dropped requirement becomes baked into downstream behavior. Later code depends on it. |
| complexity | New code keeps landing in a god module. Each addition compounds untangling cost. |
| deslop | A duplicated block spreads across modules. Each copy diverges and multiplies the eventual merge. |
| assertions | A test mocks the system under test or uses a weak harness. Other tests copy that pattern. |
| nih | Other modules import a reinvented primitive. Replacing it later requires migrating every caller. |
| efficiency | An unbounded structure runs on a long-running path. Retained references accumulate as callers grow. |
| telemetry | New code standardizes on a hand-rolled logging shape. Migrating to the real logger later touches every call site. |
The structural tag adds the compounding +1 bump in the formula.
Use the tag to record that the repair cost grows over time.
Do not restate that cost as severity.
Review procedures
Section titled “Review procedures”Changed behavior
Section titled “Changed behavior”For every changed behavior, inspect the enclosing function or method and one caller and callee before grading it clean. Check the language/runtime pitfalls that apply to the file, including evaluation order, nullability, ownership or borrowing, async cancellation, exception propagation, and coercion. Follow wrapper, proxy, adapter, and generated-code paths to the real boundary; a wrapper that drops a guard, changes a return shape, or swallows an exception is part of the changed behavior. Preserve telemetry review: a caught failure still needs the project’s structured log, metric, or trace unless the path is intentionally interactive.
Removed behavior and dropped invariants
Section titled “Removed behavior and dropped invariants”For every line the diff deletes or replaces, name the behavior or invariant it enforced, then search the replacement code and its callers for where that invariant is re-established. Treat removed guards, validations, error paths, cleanup, tests covering a real case, and telemetry as candidates until the replacement provides equivalent protection. A missing re-establishment is a correctness finding; a named requirement that is no longer met also receives a spec finding.
Caller impact
Section titled “Caller impact”For each changed function, method, exported type, or wrapper, find its callers and callees. Check every affected consumer for new preconditions, changed return shapes, new exceptions, ordering or timing dependencies, and unsafe parallel changes in the same diff. Include non-test callers and indirect callers through proxies or adapters; an empty caller search is evidence to record, not permission to assume the change is unconnected.
Per-dimension rubrics
Section titled “Per-dimension rubrics”Each dimension uses a base-severity table for each violation shape before modifiers. Apply location and compounding modifiers after the base tier.
correctness
Section titled “correctness”Look for off-by-one errors, ordering errors, null/empty edge cases, silent failures, races, contradictory branches, and lost writes.
| Base | Trigger |
|---|---|
blocker |
Assign blocker to data loss, data corruption, races in shared concurrent state, lost writes, or irreversible side effects on wrong input |
high |
Assign high when code returns wrong data, misorders results, or fails silently without recovery |
medium |
Assign medium when a recoverable flow mishandles a rare null/empty input |
low |
Assign low to cosmetic edge cases in well-bounded leaf code |
The diff’s new path can exercise an existing race, lost write, or contradictory branch in the caller graph. Expand callers one level before grading clean.
Name correctness evidence as concrete input/state → wrong output, exception, lost write, or unsafe side effect. Do not promote a style preference or an architecture concern into correctness; use conventions or altitude only when its own evidence contract is met.
Boundaries: § Dimension boundaries owns every ownership rule. Read that table before you assign a dimension.
Recommendation shape: “Add a guard for X” / “Return early when Y” / “Replace catch (_) with explicit handling”.
security
Section titled “security”Look for authN/authZ holes, injection, secrets in source/logs/URLs, tainted inputs reaching dangerous sinks, and crypto missteps.
| Base | Trigger |
|---|---|
blocker |
Assign blocker to injection (SQL/shell/template/deser), authn bypass, secrets in source, RCE, or plaintext secrets on the wire |
high |
Assign high when unvalidated input reaches a dangerous sink, internal-route authz breaks, or weak crypto protects durable data |
medium |
Assign medium when tainted input reaches a limited surface with secondary validation, or an auth-adjacent route lacks a rate limit |
low |
Assign low when already-validated input lacks defense in depth |
The diff can add a caller to an existing tainted-input path or missing authz. Trace the input to its boundary before grading clean.
Define a dangerous sink as any call that executes, queries, renders, deserializes, or persists its argument. Examples include SQL/shell exec, template render, eval / pickle / yaml.load, file-path open, and requests to an internal service. Define secondary validation as an independent check downstream of the sink’s entry that constrains the value. Examples include a schema parse, an allowlist, and a parameterized query. These checks prevent unconstrained tainted values from reaching the sink.
Use telemetry for secrets-in-logs. Use security for access-control findings. Use nih for reinvented crypto or sanitizers. Read the full rules in § Dimension boundaries.
Recommendation shape: “Validate at the boundary” / “Use the project’s existing <helper>” / “Move secret to env or vault”.
encapsulation
Section titled “encapsulation”Look for cross-module access to internals, public APIs that leak implementation types, and parameters that carry excess context. Look for new exports without a use case. Check import direction and crust integrity against ../../cheese/references/sliced-bread.md, and flag a crust delta the spec’s Placement block did not name. Also look for a domain invariant lifted from its producer and enforced above it by every caller. Check whether the producer could absorb error, default, or configuration decisions instead of exporting them.
| Base | Trigger |
|---|---|
blocker |
Assign blocker when a public API leaks an ORM model, infra adapter, framework type, or storage internal across the slice boundary. Also assign it when a slice’s index re-exports an internal type. |
high |
Assign high when code reaches another slice’s internals and bypasses crust/index |
high |
Assign high when callers must invoke or repeat a guard/validation outside the producer. The domain then fails to enforce its invariant, so callers can skip it. Also assign high when a symbol is public solely for calls from above the domain layer. |
high |
Assign high when every caller must handle an error, empty/boundary case, or configuration decision identically. Use this tier when the producer has the information to absorb that decision, such as returning an empty result instead of raising or applying a safe default instead of demanding one. |
medium |
Assign medium to a module-internal leak that exposes private detail across files inside one slice |
low |
Assign low when one class touches another class’s private member within the same file |
Look for a guard inside a slice that only external entry points call. Look for N callers that repeat one check before or after one producer. Look for a public/exported guard whose only consumers sit above the domain layer. Look for callers that apply a check inconsistently. A false-clean result often hides this violation: a private helper becomes public and crust-exported. A diff-scoped pass can grade this clean. Treat the violation as inherited when the diff does not introduce it.
Look for N callers that wrap the same call in the same try/except. Look for the same literal at the same parameter at every call site. Look for callers that re-derive the same default. Test whether the producer has the information to decide. Treat a parameter as valid when callers legitimately differ. Treat a configuration parameter as a finding when no caller can set it correctly.
The module should compute that value itself. Do not flag a knob that expresses genuine caller-specific policy.
Use deslop for duplication caused by a misplaced invariant. Use complexity for a boundary-leaking parameter. Read the full rules in § Dimension boundaries.
Here, the base tier is also the location tier. The contract bump can raise an already-blocker finding, but the cap stops it.
Use one of these recommendation shapes:
- “Import from
<slice>/indexinstead of<slice>/internal/foo” - “Narrow the public surface to
<minimal-type>” - “Move the
<invariant>into<producer>so every caller inherits it; drop the external guard and narrow the public surface” - “Return an empty
<result>instead of raising on the boundary case” - “Absorb
<default>into<producer>and drop the parameter” - “Compute
<constant>inside the module instead of asking every caller for it”
Look for behavior in the spec but not in the diff. Look for behavior in the diff but not in the spec. Look for renamed concepts, relocated boundaries, and missing acceptance criteria.
| Base | Trigger |
|---|---|
blocker |
Assign blocker to silent drift on a security, data, or correctness requirement that the spec explicitly fixes |
high |
Assign high when behavior contradicts the spec |
medium |
Assign medium when the implementation partially meets an acceptance criterion |
low |
Assign low to naming or style drift that the user can realign in 30s |
The diff can inherit a requirement that an earlier commit dropped without restoring or violating it. Compare the diff against the spec.
Locate the spec before grading. Search the durable spec corpus with python3 skills/age/scripts/age.pyz artifact-path specs <slug>. If the resolver is unavailable, use the legacy literal .cheese/specs/<slug>.md. See ../../cheese/references/formatting.md § Corpus location. Never hardcode .cheese/specs/. Then search unresolved items in .cheese/press/<slug>.md. Next, search the PR body or linked issue with gh pr view. Finally, search a commit-message ticket ref. If no source resolves, record “no spec located; searched [list]”. Grade spec findings don't know rather than clean when no source resolves.
Use correctness for contract commitments to spec and runtime risk to correctness. Emit both. Read the full rules in § Dimension boundaries.
Recommendation shape: “Restore the X requirement” / “Confirm with the user that Y is intentional” / “Update the spec to reflect Z”.
complexity
Section titled “complexity”Look for functions over budget: 40 lines, 4 parameters, or 3 nesting levels. Look for files over 300 lines that grew. Look for speculative abstractions, redundant state, parameter sprawl, and stringly-typed code. Look for explanatory-renaming comments. Look for special cases layered on shared infrastructure when generalising the underlying mechanism costs less. Treat this as a bandaid-depth fix. Also look for abstractions whose interfaces cost more than they hide. Examples include pass-through methods, pass-through variables, adjacent layers that restate one abstraction, and wrapper types that forward every call.
| Base | Trigger |
|---|---|
high |
Assign high to a god function at 3× budget, parameter sprawl through 3+ layers when intermediate layers read or transform it, or a new god module in this diff |
high |
Assign high to a shallow layer when a new module, class, or layer exposes an interface nearly as large as the functionality it hides. Callers must still know the internals to use it correctly. |
medium |
Assign medium to 2× budget, a generic helper with one user, or redundant cached state |
medium |
Assign medium to a pass-through method that forwards an unchanged signature without functionality. Also assign it to a pass-through variable threaded through 3+ layers to reach one consumer when intermediate layers do not read it. Assign it to adjacent layers whose abstractions are the same. |
low |
Assign low to a few lines over budget or a mildly speculative abstraction |
Look for a method body that makes one delegating call with an unchanged or nearly unchanged signature. Look for a parameter that exists only to reach the next call. Look for adjacent layers whose method names map 1:1. Look for a class whose public method count approaches its count of non-delegating statements. Treat dispatchers as the deliberate exception. A dispatcher routes to different implementations by type or key, so it does real work.
The diff can extend an inherited god function or parameter sprawl by a few lines. Grade the function as it now stands, not only the added lines.
Route boundary-leaking parameters to encapsulation. Route exported-decision parameters to encapsulation. Route pass-through and same-abstraction layers to complexity. Route fake-modularity file sprawl to deslop. Route cache decisions to complexity. Route runtime cost to efficiency. Read the full rules in § Dimension boundaries.
Complexity has no default blocker row. A base high finding with fix-cost-later: structural still reaches blocker after the +1 compounding bump. The phrase “No blocker row” means no base blocker. It does not mean complexity caps at high. When criticality returns, its floor may raise complexity findings on critical-tier paths.
The budget is a smell trigger, not a target. Do not split a coherent function into shallow pieces just to stay under 40 lines. That split creates a complexity finding. Grade the resulting call depth and interface cost, not only the line count. When a function exceeds the budget but has no clean decomposition, grade it clean and record why. Fire budget rows only when an available decomposition leaves each piece independently understandable.
Use one of these recommendation shapes:
- “Extract
<sub-function>” - “Inline
<one-call helper>” - “Derive
<value>instead of caching” - “Replace
<string>with<enum>” - “Replace
<vague-name>with<concrete-name>” - “Inline
<pass-through>into its caller” - “Collapse
<layer-a>and<layer-b>— same abstraction twice” - “Pass
<context-object>instead of threading<param>through 3 layers” - “Keep
<function>whole — the split to meet budget fragments one abstraction”
deslop
Section titled “deslop”Look for dead code.
Look for an AI signature: a generic catch, an empty docstring, a “// TODO: implement” comment, or a placeholder such as “// in a real implementation”.
Look for duplicated logic where a reusable helper exists.
Look for a vague or container-typed name such as user_data_dictionary.
Look for an abbreviated or single-letter identifier whose scope exceeds about ten lines or crosses a function boundary. A conventional short name used conventionally (i as a loop index, db, ctx) is exempt at any scope. Full-word identifiers speed defect finding by about 19% (Hofmeister et al., 2019). Lint coverage is partial: Ruff E741, pep8-naming N8xx; eslint id-length, @typescript-eslint/naming-convention.
Look for a reimplementation of an existing repository utility.
Look for a false module boundary, such as a one-function utility file or a class with no cohesion.
Look for a lint suppression (# noqa, @ts-ignore, #[allow(...)], //nolint) that hides the real fix.
Look for an edge-case branch for an input that nobody can name.
Look for copied boilerplate and an abstraction with one consumer.
Look for test bloat, which includes a shallow near-duplicate test.
Look for partial shell strict mode: set -e without -uo pipefail.
The per-language pattern catalogs and lint-rule mappings live in deslop-rust.md / deslop-typescript.md / deslop-python.md / deslop-shell.md / deslop-go.md (same directory).
| Base | Trigger |
|---|---|
high |
Assign high to large duplicated logic with diverging behavior or AI residue that actively misshapes flow |
medium |
Assign medium to a dead branch left “for reference”, a duplicated small block, a committed “// TODO: implement”, or an abbreviated or single-letter identifier whose scope exceeds about ten lines or crosses a function boundary |
low |
Assign low to a vague name or a single weak copy-paste |
The diff can inherit duplicated logic or a dead branch that it copies or leaves beside its change. Read the surrounding block, not only the hunk.
Boundaries: § Dimension boundaries owns every ownership rule. Read that table before you assign a dimension.
Deslop has no default blocker row.
Use one of these recommendation shapes:
- “Delete dead branch at
” - “Reuse
<existing-helper>” - “Extract shared
<helper>from the two near-duplicate blocks” - “Rename
datato<noun>” - “Rename
<abbreviation>to<full-word-name>” - “Remove
<allow/noqa/ts-ignore>and fix the underlying<lint-rule>” - “Delete the placeholder comment at
and implement the real branch”
assertions
Section titled “assertions”Look for existence assertions instead of equality, catch-any-error, no-crash-as-success, mocked SUT, and time/random/external coupling.
| Base | Trigger |
|---|---|
blocker |
Assign blocker when the test mocks the SUT or asserts the bug as correct behaviour |
high |
Assign high when the test passes even though the implementation is wrong (no-crash-as-success) |
medium |
Assign medium when the test catches generic Exception or depends on time/random without bounding |
low |
Assign low to toBeDefined when equality is one line away (assert x is not None when assert x == <expected> is one line away) |
A touched-but-unmodified test can inherit a weak assertion that the diff’s behaviour change leaves under-covering. Read the touched test bodies, not only the diff hunks.
Use deslop for generic catches in production when the claim concerns residue. Use correctness when the claim concerns a swallowed failure. Use telemetry when the claim concerns missing observability. Use telemetry for assertions on log strings. Read the full rules in § Dimension boundaries.
Recommendation shape: “Replace toBeTruthy with toEqual(<expected>)” / “Catch <specific-error> not Exception” / “Replace assert result with assert result == <expected>” / “Catch <SpecificError>, not bare except: / except Exception”.
Look for hand-rolled retry/validation/UUID/debounce/date-parse/argparse/deep-equality/sanitizer when an import exists. Look for in-project utility duplication.
| Base | Trigger |
|---|---|
high |
Assign high to reinvented logging, telemetry, or concurrency primitives that the project already wires, or to reinvented crypto |
medium |
Assign medium to reinvented retry, debounce, validation, or UUID |
low |
Assign low to a reinvented small utility that the stdlib already provides |
The diff can inherit an in-project helper or dependency that already performs this task. Check imports and the helper set before grading clean.
Use deslop for duplication inside the diff. Use security for crypto or sanitizer concerns. Use telemetry for custom logger concerns. Use efficiency for algorithm choice. Read the full rules in § Dimension boundaries.
Nih has no default blocker row.
Recommendation shape: “Replace with <existing-dep>.<fn>” / “Use the stdlib <fn> instead of the local helper” / “Call the existing <project-helper> instead of re-implementing”.
efficiency
Section titled “efficiency”Look for unnecessary work, missed concurrency, hot-path bloat, and no-op updates. Look for TOCTOU pre-checks and memory leaks. Look for long-lived objects built from closures that capture the enclosing scope. Such captures keep the whole scope alive. Prefer a type that copies only the fields it needs. Look for overly broad reads.
| Base | Trigger |
|---|---|
blocker |
Assign blocker to an unbounded cache or queue, a listener or timer leak, or retained references after teardown. Use it for anything that grows without bound in a long-running process. |
high |
Assign high to blocking work on a per-request, startup, or per-render path, or N+1 work on a high-traffic endpoint |
medium |
Assign medium to N+1 work on a moderate endpoint or redundant compute in a non-hot loop |
low |
Assign low to redundant compute outside hot paths |
The diff can trigger an inherited N+1, unbounded structure, or hot-path cost. Check whether the changed path runs hot or long-running before grading clean.
Use nih when an import exists for the task. Use correctness for TOCTOU wrong-data claims. Use complexity for cache decisions. Read the full rules in § Dimension boundaries.
Use one of these recommendation shapes:
- “Hoist
<call>out of the loop” - “Run
<a>and<b>in parallel withPromise.all(or equivalent)” - “Guard the store write on a value change”
- “Drop the existence pre-check; handle the error from
<op>instead” - “Bound
<structure>or add cleanup on<teardown>” - “Read only the needed range/columns”
telemetry
Section titled “telemetry”This dimension covers logging, metrics, and tracing hygiene. It checks presence: the path has instrumentation.
It checks shape: the instrumentation uses the correct structure, level, context, and cardinality. Non-interactive paths need real telemetry. Examples include servers, daemons, workers, and outbound calls. Interactive paths where the operator watches stdout do not need backend-shipped telemetry on the happy path. Keep secrets-in-logs under security. Keep hot-path log-volume cost under efficiency. Keep exceptions swallowed without handling under correctness.
| Base | Trigger |
|---|---|
blocker |
Assign blocker to silent failure on critical infrastructure (payments, auth, or irreversible side effects) when the operator has nothing to grep |
high |
Assign high to silent error branches on outbound calls to external services or an un-instrumented new handler on a non-interactive path |
medium |
Assign medium to a silent catch on a non-critical worker or an un-instrumented new background loop |
low |
Assign low to one missing structured field or a wrong level on a development path |
The diff can extend an inherited silent catch or un-instrumented loop in a touched module. Check the surrounding handler, not only the changed branch.
Look for silent error branches on non-interactive paths and outbound calls without observability. Look for silent daemons, workers, or schedulers. Look for missing request/response instrumentation. Look for hand-rolled logging infrastructure. Look for missing operational hygiene, including rotation/retention on new file logging. Look for unstructured or string-concatenated log messages, wrong log levels, double-logging, and errors logged without context. Look for missing correlation IDs or trace IDs.
Look for high-cardinality metric labels or span names.
Look for logs that act as metrics.
Look for print() or console.log in production.
Look for tests that assert log strings.
Look for unbounded list or object dumps in logs.
Use correctness for silent failures with no handling. Use security for secrets-in-logs. Use nih as the primary dimension for custom logger findings. Use assertions for log-string assertions. Read the full rules in § Dimension boundaries.
Use one of these recommendation shapes:
- “Emit a structured error log (and a failure counter) in this catch block before re-raising”
- “Wrap the outbound
<call>in a span and add a failure-counter metric” - “Add startup + per-iteration logs to the
<worker>loop with the failing item id on error” - “Add entry/exit log + latency metric to the new
<handler>” - “Use the project’s existing logger / standard
<stdlib-or-ecosystem-library>instead of the hand-rolled<class>” - “Configure rotation (size + age cap, retention policy) on the new file handler”
- “Read log path / level from project config instead of hardcoding”
- “Replace string-concat log with structured fields”
- “Demote to DEBUG (or drop)”
- “Log once at the boundary, not at every catch”
- “Add
exc_info=True(or equivalent) to capture the stack” - “Thread
trace_idthrough the log context at the request boundary” - “Move
<high-cardinality-attr>from metric label to span attribute” - “Emit a counter instead of grepping logs”
- “Replace
print()with the project logger” - “Assert on behavior, not on log text”
Dimension boundaries
Section titled “Dimension boundaries”conventions
Section titled “conventions”Review explicit repository, language, and wrapper conventions rather than personal taste. A conventions finding is valid only when it names the exact applicable rule, its source, the offending code, and a concrete correction. Use this evidence contract in the finding claim: rule: <exact rule or identifier>; source: <path:line>; code: <path:line>; correction: <action>. If any part is missing, record the uncertainty instead of emitting a finding.
| Base | Trigger |
|---|---|
blocker |
Assign blocker only when an explicit convention violation leaves a security, data, or release-control exposure open |
high |
Assign high when an explicit safety, compatibility, or published wrapper convention is violated at a boundary and the correction prevents incident or rework |
medium |
Assign medium when an explicit project or language convention is violated and the correction is required for maintainability or predictable tooling |
low |
Assign low when a documented non-safety convention is missed and the concrete correction is local |
Do not infer a convention from aesthetics, an unapproved preference, or an architecture concern. Keep architecture placement in altitude and runtime risk in correctness, security, or telemetry. Read the applicable instruction source and the enclosing code before grading clean.
Boundaries: § Dimension boundaries owns every ownership rule. Read that table before you assign a dimension.
Recommendation shape: “Apply <exact-rule> from <source> to <code>” / “Use <project-convention> at <boundary>” / “Confirm the documented exception for <code>”.
altitude
Section titled “altitude”Review whether behavior sits at the right abstraction level and has the right owner. A valid altitude finding must name all four: symptom, owner, better placement, and concrete cost. Examples include a policy-free wrapper that adds a layer, a decision owned by every caller instead of its producer, or a module boundary that leaves callers responsible for internals. Architecture preference alone is not a finding.
| Base | Trigger |
|---|---|
blocker |
Assign blocker only when the misplaced boundary creates an open security, data, or irreversible side-effect exposure; emit the underlying security or correctness finding too |
high |
Assign high when a misplaced owner or abstraction boundary forces multiple consumers to duplicate policy or depend on internals, creating material rework |
medium |
Assign medium when a wrapper, split, or owner mismatch adds a measurable layer or repeats a decision across callers |
low |
Assign low when a local placement mismatch has a named owner, better placement, and one concrete contained cost |
When an approved design intentionally chooses the current placement, treat a contrary observation as an explicit decision for the user to confirm or reject. Do not turn an altitude preference into an automatic /cure rewrite; the recommendation must preserve the approved decision or explicitly request that it be changed.
Boundaries: § Dimension boundaries owns every ownership rule. Read that table before you assign a dimension.
Recommendation shape: “Move <behavior> to <owner> after confirming the approved placement” / “Inline <wrapper> beside <caller> and record the placement decision” / “Keep <boundary> and record why its concrete cost is accepted”.
This table is the single ownership rule for the whole file.
It decides the primary dimension when dimensions identify the same underlying defect.
Each per-dimension Boundaries: line points here and states no rule of its own.
The grader reconciles by the underlying defect or design problem, not by location alone.
Keep distinct problems at the same location separate, and name secondary dimensions for one overlapping problem.
Look for one primary dimension per finding. Use this table to choose the primary when dimensions overlap.
| Pair | Tiebreaker |
|---|---|
| correctness / telemetry | Silent failure with no logging belongs to correctness. Telemetry owns the finding once the failure is caught and only observability remains missing. |
| security / telemetry | Secrets in logs or URLs belong to security regardless of surrounding code. |
| security / correctness | A behavioral bug with an access-control consequence belongs to security. Use correctness only without a security consequence. |
| security / nih | Reinvented crypto or a security sanitizer belongs to security (higher base wins). Leave nih off to avoid downgrading a blocker through nih’s missing blocker row. |
| deslop / correctness | Tag by the claim. Use deslop for AI residue and correctness for silent failure. |
| deslop / nih | Use nih when a pre-existing helper or import already does the task. Use deslop when duplication stays internal to the diff and no existing helper exists. |
| deslop / assertions | Generic catches in test files belong to assertions. In production code, classify them as deslop, correctness, or telemetry according to the claim. |
| nih / telemetry | Tag custom loggers as telemetry primary because it has the richer rubric. Note the nih angle in the recommendation. Do not double-tag. |
| efficiency / nih | Use nih when an import or library exists for the primitive. Use efficiency for an algorithm or concurrency choice without an available import. |
| efficiency / correctness | Tag TOCTOU as efficiency when it wastes work. Tag it as correctness when a race can produce wrong data. Split by failure mode. |
| encapsulation / deslop | Tag duplication from a misplaced invariant as encapsulation. Ownership forms the root cause, not deslop. |
| encapsulation / complexity | Tag a parameter that leaks context or type across a boundary as encapsulation. Tag raw parameter count or threading without a boundary concern as complexity. |
| complexity / encapsulation (exported special case) | Extend the encapsulation / complexity row above. When a threaded parameter carries a decision the producer can make (a voodoo constant), choose encapsulation because misplaced ownership causes the problem. Keep structural cost alone in complexity when no decision is misplaced. |
| complexity / deslop | Tag pass-through methods and same-abstraction layers as complexity. Tag a single-function utils file or one-consumer over-abstraction as deslop. Both show fake modularity; split them by call depth or file sprawl. |
| spec / correctness | Emit both with a cross-reference. Spec records the broken contract commitment. Correctness records the runtime risk. The dimensions remain orthogonal. |
| assertions / telemetry | Tests that assert on log strings belong to telemetry. |
| complexity / efficiency | Complexity owns the structural cache decision. Efficiency owns the runtime cost of redundant work. |
| conventions / spec | Use conventions when an explicit rule/source is violated. Use spec when a requested behavior or acceptance commitment is missing or contradicted. Emit both only when both claims have independent evidence. |
| conventions / deslop | Use conventions for an exact documented rule with source and correction. Use deslop for generic AI residue, duplication, or dead code without a governing rule. |
| conventions / assertions | Use assertions for a weak test contract. Use conventions only when an explicit test convention is the independently evidenced violation. |
| conventions / telemetry | Use telemetry for missing or malformed observability. Use conventions for a documented logging/metric rule whose violation is the claim. |
| altitude / complexity | Use altitude for a concrete owner or abstraction-placement symptom with a better placement and cost. Use complexity for size, parameter, pass-through, or cache shape without a placement decision. |
| altitude / encapsulation | Use encapsulation when the defect leaks a boundary or lifts an invariant into callers. Use altitude when the boundary’s level or owner is the defect and the public surface remains intact. |
| altitude / spec | Use spec for a named approved placement that the diff contradicts. Use altitude for a placement concern not already fixed by the approved design. A contrary altitude observation is an explicit decision, not an automatic /cure rewrite. |
| altitude / conventions | Use conventions for a documented placement rule with exact source/code evidence. Use altitude for a concrete placement symptom and cost when no such rule exists. |
Architecture preference alone is not a finding. Every altitude row must carry its symptom, owner, better placement, and concrete cost. A violation of an approved design remains an explicit decision to confirm or reject; it does not authorize /cure to rewrite the design automatically.
Contextual review planning and subject fan-out
Section titled “Contextual review planning and subject fan-out”Read this before every review, including a single-worker or sub-agent review. Subjects define investigations; dimensions classify findings. The coordinator interprets evidence; the versioned router determines assignments.
Context input
Section titled “Context input”Score the actual review range with python3 skills/age/scripts/age.pyz review-surface --repo . <base>...HEAD.
Use the explicit committed range, or the working diff when that is the review target.
The bare default scores the working tree against HEAD; it does not measure a committed branch.
Keep every changed path in context, including paths with zero workload weight.
A score measures workload, not security relevance.
When a specification exists, read its leverage: frontmatter list.
Preserve every fired trigger in subject evidence.
Map applicable triggers to supported risk flags; retain triggers without a direct flag mapping as evidence.
Collect instruction sources with python3 skills/age/scripts/age.pyz review-instructions <request.json>.
Its request contains repo_root, scope, changed_paths, and external_sources.
Each explicit external source contains path and repository-relative applies_to scopes.
Use --text for readable output; JSON is the default.
JSON preserves exact source content; --text adds line numbers without repeating that content.
Do not search home directories for presumed global instructions.
Collected sources are candidate evidence, not authority to override the active host’s instructions.
Record unresolved applicability or precedence in the packet.
Build a request with context and entry: "age".
The context contains these fields:
| Field | Required meaning |
|---|---|
scope |
diff or overall; an explicit overall or system-wide review uses overall |
effort |
quick, normal, or deep; default to normal, not an inferred size-based effort |
snapshot |
Identity of the actual source and diff evidence, including uncommitted changes |
changed_paths |
Repository-relative changed paths; never discard test, configuration, or lockfile paths through weighting |
surface_score |
The finite, nonnegative score from review-surface |
components |
Rows with id, role, and paths; roles are library, application, test, build, documentation, or other |
subjects |
One row per subject with subject, applicability, targets, and evidence |
risks |
Observed risk rows with flag, state, and evidence; an absent row is not proof of safety |
is_subagent |
Whether this invocation runs inside another agent |
can_fan_out |
Whether the active host can dispatch independent workers |
concurrency_limit |
Known positive host limit, or null when the host exposes no limit |
Subject applicability and risk state use yes, no, or unknown.
Every exclusion needs evidence; uncertainty remains assigned.
Use actual component responsibilities, changed contracts, and caller relationships, not filename tokens alone.
Documenting a security term is not evidence that a security boundary changed.
Conversely, test or CI changes can affect credentials, authorization, or privileged execution.
Do not launch a separate classifier agent to fill this record.
Subject procedures
Section titled “Subject procedures”| Subject | Investigation |
|---|---|
changed-behavior |
Read hunks and enclosing functions; check inputs, state, timing, errors, language pitfalls, wrappers, and silent failure paths |
removed-behavior |
Name what each removed protection enforced and locate its replacement; include guards, validation, error paths, tests, and invariants |
caller-impact |
Trace changed preconditions, return shapes, exceptions, ordering, callers, and callees |
security |
Inspect the relevant trust boundaries, permissions, secrets, hostile inputs, and dangerous operations |
spec-tests |
Compare behavior with requirements and check whether tests defend that behavior |
reuse |
Find existing mechanisms that new code duplicates |
simplification |
Find unnecessary structure, special cases, indirection, and generated-code residue |
efficiency |
Inspect costly paths, avoidable work, copying, allocation, I/O, and scaling |
conventions |
Cite the applicable written rule, its source, violating code, and required correction |
altitude |
Name the local symptom, responsible component, better placement, and concrete cost |
A worker may emit several finding dimensions. Different subjects may expose the same problem. A convention or altitude finding needs concrete evidence, not a style preference. An altitude recommendation that contradicts an approved design identifies that decision; it is not permission for an automatic redesign.
Plan and effort
Section titled “Plan and effort”Run python3 skills/age/scripts/age.pyz age-route <request.json> and save its complete JSON as .cheese/age/<slug>-plan.json.
The same canonical context and policy version produce the same plan.
Semantic context can differ between independent runs; do not call that fully deterministic.
Quick mode combines ordinary work and the two protected subjects. A mandatory risk adds its required specialist without upgrading unrelated assignments. Normal and deep modes reserve separate conventions and altitude reviewers. The remaining ordinary isolation allowances use the current weighted score bands:
| Effort | Below 60 | 60 through 250 | Above 250 |
|---|---|---|---|
| Normal | 1 | Up to 2 | Up to 5 |
| Deep | Up to 3 | Up to 5 | Full relevant subject separation |
These are allowances, not quotas.
Mandatory specialists can exceed them; the plan records the reason.
Overall review separates every subject rather than pruning from a diff.
Sub-agent and unavailable-agent restrictions require explicit degraded output, never a claim of independent protected review.
Use the returned assignments and dispatch batches; do not recreate a dimension ladder or infer effort from n.
If new evidence changes the scope, rebuild context and obtain a new plan before additional dispatch.
Do not refresh a production lock to hide changed source; restart the review when source evidence changes.
Dispatch and shared evidence
Section titled “Dispatch and shared evidence”Assemble packet.md once before the review lock.
Give each worker its assignment and the packet’s relevant evidence sections.
Conventions and altitude retain the whole-change context; other workers receive scoped targets.
Do not give a security specialist every test file merely because the review includes tests.
Reuse the shared caller and dependency evidence instead of discovering it independently for every subject.
Resolve read-only, fresh-context reviewers through ../../cheese/references/agent-resolution.md.
Pass each assignment’s effort, not the requested review-mode name, to the host.
Issue independent calls in the same message where the host permits it.
Use background execution where available and respect the returned batches and actual host limits.
Never serialize independent work merely by waiting for each result before issuing the next call.
Workers do not spawn reviewers, reconcile results, apply fixes, or write the canonical report.
They emit full finding rows and also-relevant-to: [<dimension>, ...] when another rubric may apply.
Pass every candidate with a nameable failure scenario or concrete design cost through; the verifier filters.
Reconciliation and verification
Section titled “Reconciliation and verification”The coordinator reconciles by the underlying defect or design problem, not file-and-line equality alone.
Use dimensions.md’s boundary rules for overlapping dimensions.
A shared location can contain distinct problems; one problem can span several locations.
Keep cross-references when separate findings remain justified.
Once actual candidates exist, run a cheap verifier in batches of up to ten claims.
Do not spawn an empty batch.
The initial plan leaves verification.candidate_batches empty because no findings exist yet.
After reconciliation, form nonempty batches from actual candidate findings, never from first-pass assignment IDs.
Use the plan’s explicit sub-agent or capability skip reason when verification cannot run independently.
For each claim, return one result:
- Confirm: the evidence supports the claim at its current severity.
- Downgrade-or-drop: correct the severity or remove an unsupported claim; retain the reason in the confidence trail.
- Escalate: identify missing evidence under
## Confidence; do not emit an unsettled finding row.
Normal and quick reviews have no gap sweep. Deep review runs a fresh sweep after verification, using the verified list to avoid rediscovery. The sweep looks for omissions, including dropped invariants, language pitfalls, wrapper errors, and setup/teardown asymmetry. Verify its new candidates before adding them to the report.
Output and dispatch observations
Section titled “Output and dispatch observations”Preserve the same finding format and severity grouping at every width.
Record the plan path, policy version, input digest, planned assignments, and observed dispatch in ## Agent resolution.
Keep dispatched: <n> workers, one message: <true|false> for the observed first pass, not the planned count.
Use zero and false when no workers were dispatched.
Record verifier: skipped (sub-agent) separately when applicable.
Run python3 skills/age/scripts/age.pyz review-plan-check <request.json> before writing the report.
The request contains the full plan and observations.
Observations contain assignment_ids, one_message, and source (host or reported).
Use null observations when the host provides none; do not fabricate receipts.
A consistency check cannot authenticate host events.
Reported observations remain unverified, and unavailable observations remain explicitly unobserved.
A mismatched assignment set requires reconciliation or a clearly incomplete review, not a success claim.
Handoff detail: selection gate, dispatch, auto mode
Section titled “Handoff detail: selection gate, dispatch, auto mode”Read this before rendering the selection gate (a reason to ask, or --safe) or dispatching /cure.
Selection gate (--safe, or a reason to ask)
Section titled “Selection gate (--safe, or a reason to ask)”Use the shared handoff gate in ../../cheese/references/handoff-gate.md.
Age’s finding selection is the core decision.
The tail (Plate it, Checkpoint & stop, Stop) follows.
-
Render the numbered selection table:
python3 skills/age/scripts/age.pyz findings render-table --report .cheese/age/<slug>.mdMark any sprawling/structural-fix row as heavy.
-
Ask which findings to cure. Lead each option with the verb that describes what the user wants to do next. Use the underlying selection verb as the backing detail. Lead with the recommended composite. Then present the same four severity-floor options below it. Keep the options in the same most-inclusive-to-least order. This order keeps the gate predictable across every run:
- Fix mediums-and-above plus cheap lows (recommended) — equivalent to
all-medium, cheap. This composite floor appears under Compute the recommended set inSKILL.md § Handoff. Cheap lows are small, valid nits that cost less to fix now than to defer. Leave sprawling/structural lows out. - Fix everything — use
allfor every finding, regardless of severity. - Fix medium-severity and above — use
all-medium. This option uses the medium severity floor from Compute the recommended set. It excludes the cheap-lows union. Addcheapto include contained-fix lows and use the recommended composite. - Fix high-severity findings and blockers — use
all-high. This option floors at high and includes every blocker. - Fix blockers only (strict) — use
all-blocker. Land only the must-fix blockers. Defer the rest to a follow-up.
Offer the non-floor and standard-tail options last.
-
Pick findings to fix — accept a free-text reply using the verbs from
../../cure/references/selection.md. Expand the verb to finding ids.python3 skills/age/scripts/age.pyz findings parse-selection --report .cheese/age/<slug>.md --selection "<verb>" -
Plate it — apply the recommended composite via
/cure <slug> --auto --open-pr --stake medium+. Terminal/plateresolves topology and publishes. Carry--hard. -
Checkpoint & stop — run
/wheypointto write a resumable handoff and pause instead of curing now. -
Stop — leave the report for later — use
none.
Present all four severity options on every run.
Present them even when a severity band is empty, such as no blockers.
Treat a floor that resolves to an empty set as a valid, predictable no-op.
Do not drop or reorder options based on the populated bands.
If the selected floor or recommended composite resolves to an empty set, treat it as none.
Report that no findings match.
Do not dispatch /cure with empty resolved_ids.
The non-empty-selection contract in Dispatch still holds.
Dispatch
Section titled “Dispatch”Dispatch /cure <slug> [--safe] [--open-pr] [--hard] immediately when the selection is non-empty.
Apply this rule to automatic selections and gate selections.
Pass the selection through context, not a CLI flag.
Invoke /cure instead of repairing a file in the review context.
End the Age run after the report.
The step 1 review lock rejects a report written after an inline edit.
handoff_context: source_skill: /age source_report: .cheese/age/<slug>.md selection: "<recognized verb or explicit ids>" resolved_ids: [<expanded ids>]/cure skips its own selection prompt when this context is present.
/cure re-confirms that the cited ids still exist.
/cure owns the apply / validate / push loop.
Always emit resolved_ids alongside selection.
Expand the verb yourself instead of leaving the field empty.
/cure re-confirms the ids against the report regardless.
The report carries each finding’s recommendation: and optional invariants: line.
/cure renders them into the coder brief with findings render-brief and implements the recommendation as the locked decision.
Do not restate them in handoff_context.
Propagate --safe, --open-pr, and --hard to /cure when they are in scope.
On none or Stop (only reachable via the gate), exit cleanly with the report path.
--auto substitutes a severity-floor selection and its own chain. See ## Auto mode below.
Within cook’s own fan pathway
Section titled “Within cook’s own fan pathway”/cook owns the fan pathway. See ../../cook/SKILL.md § Fan pathway.
/ultracook is retired. A host that still routes /ultracook resolves it to /cook.
The no-chain override below applies to both names.
The pathway starts age as a fresh-context sub-agent and owns the chain.
Follow the no-chain isolation directive:
- Write
.cheese/age/<slug>.mdwith the handoff slug at the top. Stop after writing it. Do not invoke/cure <slug> --auto --stake medium+from inside the sub-agent. - Set
next:from what you observe on this run. Do not infer it from the chain position. Setnext: curewhen at least one finding meets the medium+ floor. Setnext: donewhen no finding meets that floor. - The fixed chain length of the fan pathway enforces the two-cure-pass cap.
Age counts no passes.
## Auto modebelow states the same rule. Publish the terminal age report only withnext: done. Treatnext: cureor a missingnextas a halt. Do not publish. Dispatch each parallel work unit as a top-level fresh-context reviewer. Dispatch the post-merge review the same way. Never dispatch either one as a nested inline self-review. Record the plan identity and observeddispatched: <n> workers, one message: <true|false>under## Agent resolution. Use zero and false for inline or sub-agent execution; never substitute the planned worker count. Missing host observations remain unobserved; self-reported observations remain unverified. A sub-agent recordsverifier: skipped (sub-agent)separately.
Auto mode
Section titled “Auto mode”When the caller passes --auto, follow these rules.
- Skip the selection gate above.
- Report the result of this run only.
Age counts no cure passes and holds no pass state.
/cook’s phase table owns the two-pass cap through its fixed chain length. See../../cook/references/auto-mode.md. - If the recommended set is not empty, invoke
/cure <slug> --auto --stake medium+. Forward--open-prand--hardwhen they are in scope. - If the recommended set is empty, stop the chain. Print a one-line “auto chain clean” note and the report path.
Auto mode inside the cook fan pathway
Section titled “Auto mode inside the cook fan pathway”Read ## Within cook's own fan pathway above for the no-chain isolation directive.
Follow that directive before you write the report.
Shared context packet
Section titled “Shared context packet”The coordinator builds one packet at .cheese/age/<slug>-packet.md before the review lock.
Save the contextual request and returned plan beside it.
Do not add a preliminary classifier or mandatory context agent.
Reuse evidence gathered while identifying the review target.
Every review rebuilds its packet; there is no persistent cross-run cache.
Components
Section titled “Components”- Target and requirement — Identify the exact diff or overall scope, source snapshot, and located specification or issue. State when no specification exists. For a specification, include its fired
leverage:triggers. - Component roles — Include relevant dependency manifests and the component/path map. Distinguish library exports, application entry points, tests, build systems, and documentation.
- Shared impact evidence — Include changed symbols, caller and callee relationships, affected contracts, and relevant enclosing functions. Collect these once with the selected code-intelligence backend.
- Project-helper index — Detect source roots from manifests or changed paths. Find task-relevant existing helpers such as sanitization, validation, escaping, retry, and logging mechanisms.
- Instruction sources — Include review-instructions output with source identities, scope, hashes, and line citations. Distinguish collected candidates from host-supplied active instructions. Report unresolved authority or unavailable sources explicitly.
- Context and plan — Include subject applicability, risk evidence, policy version, input digest, assignments, capability restrictions, and dispatch batches. A zero workload weight never removes a path from relevance analysis.
- Rubrics and severity — Give workers their subject procedures and primary dimension rubrics, plus location sensitivity, fix-cost-now, fix-cost-later, and severity computation. Link the other rubrics for findings outside the primary set.
- Output and ownership — Include the exact per-finding format,
also-relevant-to, applicableinvariantsfromreport-example.md, and the reconciliation contract below.
Primary rubric map
Section titled “Primary rubric map”Subjects are investigation procedures, not restrictions on which findings a worker can report. Use this map to prepare the initial rubric material without copying the whole repository into every worker prompt.
| Subject | Primary dimensions |
|---|---|
| changed-behavior | correctness, telemetry, security |
| removed-behavior | correctness, spec, assertions |
| caller-impact | correctness, encapsulation |
| security | security |
| spec-tests | spec, assertions |
| reuse | nih, deslop |
| simplification | complexity, deslop |
| efficiency | efficiency |
| conventions | conventions |
| altitude | altitude, encapsulation, complexity |
Extract rubric sections by their dimension headings from dimensions.md.
Combined assignments receive the union of the relevant rubrics.
A worker that identifies another kind of problem reads that rubric before classifying its finding.
Evidence views
Section titled “Evidence views”Conventions and altitude receive the whole-change orientation and component relationships. Other workers receive their assigned targets and the relevant shared evidence sections. Workers may inspect additional code when evidence requires it; target scopes are starting points, not excuses to miss affected callers. A security assignment names its trust boundary, not every test file in the repository. The altitude view includes the owning component and immediate consumers so it can evaluate solution placement. An optional explorer can resolve a genuinely unmapped area; it is not a fixed prerequisite for every review.
Output and reconciliation ownership
Section titled “Output and reconciliation ownership”Workers emit full finding rows and also-relevant-to: [<dimension>, ...] where another rubric may apply.
Pass every candidate with a nameable failure scenario or concrete design cost through; verification filters it later.
Workers do not deduplicate, apply boundary tiebreakers, reconcile severity, edit source, or write the canonical report.
The coordinator reconciles by the underlying problem and applies the dimension boundary rules.
A shared line does not prove duplicate findings, and one defect can span several locations.
Transient evidence contract
Section titled “Transient evidence contract”Write the request, plan, instruction-source output, and packet before capturing the review lock.
Give workers read-only access.
Do not mutate locked evidence or preserve it as a cross-run cache.
If new observations require replanning, preserve the prior plan and record the changed context.
A production-tree change requires a new review, not a refreshed lock that makes stale findings appear current.
Leave transient artifact removal to normal .cheese/ cleanup.
Late evidence
Section titled “Late evidence”The report write can fail with review evidence changed. The error names each moved .cheese/ file.
This error means that the source tree still matches the lock. Only a new late packet for this slug may be added.
Run python3 skills/age/scripts/age.pyz review-lock --slug <slug> --refresh-evidence. Then write the report again.
The refresh captures one candidate snapshot and compares its source digest before replacing the lock.
It refuses source changes and every change or deletion of previously locked evidence. Require a fresh review.
Record an accepted late packet and its named path under ## Agent resolution in the report.
Every other lock failure stays final. Do not take a new lock to force the write.
Evidence tools and fallbacks
Section titled “Evidence tools and fallbacks”Use ../../cheese/references/code-intelligence-routing.md for backend selection.
Use symbol-aware caller and dependency queries when available; report weaker evidence when only text search is available.
Reuse supplied wiki citations before searching again.
Optional Hallouminate context can explain design intent; cite its pages and mark inference-based claims as speculating.
Use GitHub tools for PR context, or local Git and user-provided data when GitHub is unavailable.
Optional integrations follow ../../cheese/references/optional-plugins.md; state an absence once and use the documented fallback.
No missing optional integration authorizes a fabricated source or a silent coverage claim.
Worked report example
Section titled “Worked report example”Read this with SKILL.md § Output for a concrete report skeleton.
Body order
Section titled “Body order”Write the body in this order.
Start with # Age Report — <slug>.
Add ## Orientation with 1-2 sentences.
Add ## Press findings only when a press report exists.
Add ## Wiki context only when hallouminate grounding returns a hit.
Add ## Agent resolution after the severity sections.
Use the exact three-line form below for each finding.
/cure parses this form with src/easy_cheese/shared/findings.py.
A finding that drops the list marker or the location backticks is invisible to /cure.
- **[<dim>:<sev>]** `path:line` — <claim> - location: <tier> · fix-cost-now: <tier> · fix-cost-later: <tier> · confidence: <tier> - recommendation: <action> - invariants: must-hold: <X>; must-not: <Y>For a conventions finding, state the exact rule, source, offending code, and correction. For an altitude finding, state the symptom, owner, better placement, and concrete cost; architecture preference alone is not a finding.
The invariants: line is optional.
Add it to a blocker or high finding when the fix could break a neighbour.
/cure implements recommendation: as the locked fix decision and keeps every invariants: clause true.
Write both as concrete, checkable statements.
End with ## Confidence and ## Next step.
The worked instantiation and the full skeleton follow below.
Omit empty severity sections.
When ten or more low findings exist, collapse the ## Low section to one line:
## Low*N low-severity findings suppressed.* Re-run with `--full` (or `/age --full`) to see them.## Blocker- **[encapsulation:blocker]** `src/users/index.ts:42` — `index` re-exports `SqlPgUser` (infra ORM type) across slice boundary. 3 consumer slices already import it. - location: contract · fix-cost-now: sprawling · fix-cost-later: structural · confidence: certain - recommendation: define `User` in the slice's public types, map at the boundary, deprecate the leaked export. - invariants: must-hold: `User` stays the only exported user type; must-not: touch the ORM mapping under `infra/`
## High- **[security:high]** `src/api/admin/users.ts:55` — admin route accepts user-supplied filter without validation. - location: contract · fix-cost-now: contained · fix-cost-later: contained · confidence: certain - recommendation: validate against `AdminFilter` schema at boundary.
## Medium- **[complexity:medium]** `src/utils/format.ts:200-240` — 60-line function, 5 params. - location: module · fix-cost-now: contained · fix-cost-later: contained · confidence: speculating - recommendation: extract `formatHeader` / `formatBody`.
- **[conventions:medium]** `src/config.py:12` — `os.environ["PORT"]` bypasses the repository rule: “Parse environment values with `ConfigValue` before use.” - location: module · fix-cost-now: contained · fix-cost-later: spreading · confidence: certain - recommendation: parse the value with `ConfigValue` before use, as `docs/configuration.md:18` requires.
## Low- **[deslop:low]** `src/utils/format.ts:18` — variable `data` shadows outer `data`. - location: class · fix-cost-now: contained · fix-cost-later: contained · confidence: certain - recommendation: rename to `lineItems`.
- **[altitude:low]** `src/utils/format.ts:24` — Symptom: `formatBody` repeats `formatHeader`'s argument list; owner: `formatHeader`; better placement: its sole caller; concrete cost: parameter changes require edits to both signatures. - location: module · fix-cost-now: contained · fix-cost-later: contained · confidence: speculating - recommendation: move the wrapper beside its sole caller after confirming the intended placement.
## Confidence<`certain` | `speculating` | `don't know`> — <one-line justification including which evidence sources were unavailable>
## Next step<when press was skipped, lead with>: Hardening was skipped for this diff — run `/press <slug>` before curing, or continue reviewing as-is.Auto-fixing the recommended set via `/cure` (or the selection prompt on a reason to ask / `--safe`).Full skeleton (with placeholders)
Section titled “Full skeleton (with placeholders)”The worked instantiation above renders only the severity sections. This is the complete report shape, handoff slug through ## Next step, with every placeholder in context. Use the canonical status: grammar from the handback contract:
status: <canonical status field>next: cure | doneartifact: <path-to-press-report-or-prior-cure-if-any>durable_flags: none | <one line per flag: what durable knowledge changed -> target wiki page>baseline: none | <recorded baseline block copied from the upstream handoff — see ../../cook/references/quality-gates.md><one-line orientation: what the diff does>
press: skipped
<!-- `press: skipped` is the first body line after the blank separator. Omit it entirely when a press report exists for this slug or no cook artifact does. -->
# Age Report — <slug>## Orientation<one or two factual sentences about what the diff does>## Press findings<omit this section when `.cheese/press/<slug>.md` does not exist.When that report exists, copy each unresolved press item into one or two bullets.`/cure` never reads the press report, so an item that is absent here never reaches `/cure`.When the press report is absent and `.cheese/cook/<slug>.md` exists, omit this section.Add `press: skipped` on the first body line instead.>
## Wiki context<omit this section when hallouminate is absent.Omit it when grounding returned no hit and `/cheese` routed no `wiki_hits`.List one bullet for each consulted page: `<wiki page path>:<line>` — <one line on why it informed the review>.The user reads this section to challenge what grounded the review.>
## Blocker<one row per blocker, in the finding format above. Omit this section when no blocker exists.>
## High<one row per high finding. Omit this section when no high finding exists.>
## Medium<one row per medium finding. Omit this section when no medium finding exists.>
## Low<one row per low finding. Omit this section when no low finding exists.Collapse this section to one line when ten or more low findings exist.>
## Agent resolution<one bullet per resolved worker: role, selected type, effort, and `degraded: true` when a fallback ran.>Record the plan path, `policy_version`, `input_digest`, planned assignment IDs, and `review-plan-check` status.Include exactly one `dispatched: <n> workers, one message: <true|false>` line from actual execution observations.Never replace observed counts with planned counts.Use zero and false when no workers were dispatched.Mark unavailable observations as `unobserved`, including unknown values on the dispatch line.Self-reported observations remain unverified; a consistency check does not authenticate execution.Record capability limits and skipped verification separately.A sub-agent adds `verifier: skipped (sub-agent)` on its own line.
## Confidence<`certain` | `speculating` | `don't know`> — <one line on the evidence, including each unavailable source><one bullet per escalated claim that the verifier could not settle, with the missing evidence>
## Next step<when press was skipped, lead with>: Hardening was skipped for this diff. Run `/press <slug>` before curing, or continue the review.<when the plan has a non-null `degraded_reason`, or verification is recorded as unavailable or skipped>:State that recorded reason and its effect on coverage.Do not infer degradation from the surface score.<then state the selection>: Fixing the recommended set through `/cure`.<or, on a reason to ask or `--safe`>: Rendering the selection prompt.Read the worked instantiation above for the finding rows that these placeholders stand for.
Sub-agent context gate (shared kernel)
Section titled “Sub-agent context gate (shared kernel)”These cross-skill rules govern work that a skill sends to a sub-agent.
Each skill names its own triggers. This file is the single source of truth for the rules that every skill shares.
Digest contract
Section titled “Digest contract”The sub-agent returns 2 KB or less of UTF-8 text.
It returns a structured summary, citations, and gaps.
It returns no raw bodies, no full file dumps, and no copied source text.
Each skill names the digest contents, such as a claim table, an orientation paragraph, or a root-cause summary.
One exception exists. An Age subject worker returns full per-finding rows without a size ceiling.
fan-out.md § Dispatch and shared evidence defines that worker.
The ceiling applies to every other sub-agent.
Harness-agnostic sub-agent selection
Section titled “Harness-agnostic sub-agent selection”Resolve every worker through ../../cheese/references/agent-resolution.md.
The calling skill supplies the work, permission/isolation floor, minimum power, effort, and fallback.
This context kernel governs only digest boundaries.
What the parent never delegates
Section titled “What the parent never delegates”By default, the parent never delegates severity grading, final verdicts, or approval gates. Age may delegate candidate grading to subject workers across every dimension their assigned investigation exposes. The parent retains cross-subject reconciliation, verification, final verdicts, and canonical report writing. This exception does not loosen the default for other cases.
Do not delegate dialogue, contradictions, handshakes, or user-facing decisions. Do not delegate writing the canonical artifact (report, spec, claim table). The sub-agent supplies the digest; the parent writes the document.
What the sub-agent owns
Section titled “What the sub-agent owns”The sub-agent owns bulk fetches, extracts, crawls, and multi-source research.
It owns many-file reads and dependency / caller graph traversals.
For code navigation, start with kind:symbol to find the definition.
Then use kind:callers for call sites.
Fall back to content/regex only when you do not have a symbol name.
It also owns work that yields mostly raw bodies that the parent will not read line by line.
Send the work to a sub-agent when the raw output is more than 5000 tokens.
Parallelism
Section titled “Parallelism”When two or more heavy units of work are independent, spawn one small sub-agent per unit in parallel. Merge the digests in the parent. Do not send one sub-agent to do five sequential tasks; that shape is wrong.
Age router as fan-out predicate
Section titled “Age router as fan-out predicate”/age sizes its fan-out with the age router, not with a size-only threshold.
skills/age/SKILL.md § Sub-agent fan-out uses route(context=...) in src/easy_cheese/shared/fanout/age_route.py.
The router assigns subjects from evidence, effort, workload, and execution capabilities.
fan-out.md owns the context and assignment contract. This file does not repeat it.
This file’s digest contract, selection rules, and delegation boundaries apply to every worker that the router starts.
Use this file for shared output discipline, reasoning posture, and question scope. Skills cross-reference this file instead of restating those rules. When a skill omits a rule, treat the omission as an opt-out.
Output discipline
Section titled “Output discipline”- Lead with the answer in written reports. Put the result on the first line.
This rule applies to
.cheese/*artifacts, written summaries, and end-of-task reports. Skip preambles, restatements, and trailing sign-offs. - Brief conversational scaffolding earns its place in interactive dialogue when the user explores or aligns. This rule targets reports, not natural turn-taking.
- Match shape to content. Use headers and bullets for genuinely list-shaped content. Keep a two-sentence answer as two sentences.
Reasoning posture
Section titled “Reasoning posture”- Correct false premises before engaging. Name each wrong assumption. Then answer the correct question.
- Name loaded assumptions. When a question presupposes a contested choice, surface it before answering.
- Flag confidence on each critical claim. Use the three-way scale:
certain— Direct evidence appears in file content, command output, a primary doc, or a test result.speculating— Infer from an indirect signal. Name the inference path so the user can audit it.don't know— Say it. Never launder a guess as analysis.
- Steelman the rejected option. When proposing one approach, state the strongest case for the alternative before dismissing it. Apply this to design choices, library picks, and review recommendations.
- Track contradictions across the dialogue. When a later turn contradicts an earlier turn, name the conflict. Resolve it before you continue. Do not make the user the consistency check.
- Agree when evidence supports agreement. Do not manufacture counterpoints to seem balanced. A spec the user already got right needs no re-litigation.
- Prefer satisfying a valid critique to arguing it. This rule applies only in a phase that writes code, such as
/cook,/press, or/cure. A review-only phase such as/agerecords the finding and routes it. It never applies the fix. In a write-enabled phase, apply each correct and inexpensive review comment. Apply each correct and inexpensive self-review finding. A cheap fix touches a few lines or one local refactor. Push back when the critique is wrong, because the code is already correct or the claim has no evidence. Push back when the fix is sprawling or structural and costs more than it returns. A justified push-back usually costs more than a small valid fix. - Name the exact step that breaks when reasoning is invalid. Do not write “this seems off”. Write “the X assumption fails when Y because Z”.
Depth and questions
Section titled “Depth and questions”These rules use different axes: which decisions to ask about, how to phrase a question, and how much to contribute. Do not treat them as one dial to trade off.
- What you ask about — the decisions that the user owns. Ask about each consequential fork.
A fork is consequential when it fires a leverage trigger (
../../cheese/references/routing-policy.md§ Leverage triggers), changes a crust or import direction, or changes user-visible behavior or output. Internal behavior the user cannot observe through a public interface does not qualify. Scope, naming, and trade-offs below that line are[AGENT-DECIDED]: make the call, log a vetoable alternative, and do not ask. Do not decide a consequential fork silently. Do not present a decision that the user owns as settled. Ask first on a decision that the user owns. - How you phrase a question — one clear thing at a time. Preserve working memory. Show the real ambiguity instead of hiding it in a multi-part question. This rule governs phrasing, never whether to ask.
- What you contribute — the most useful depth. Write the full signature of each public interface you propose; bodies and helpers wait for
/cook. Name each edge case. Do not write “consider edge cases”. Givefile:lineevidence. Do not give a vague pointer. Name the rejected option. Do not write “there are trade-offs”. Prefer more detail to less.
The failure mode to watch is treating a low question count as a virtue. Use tight phrasing, not few questions. Do not skip a user decision because you prefer contributing to asking. Do not ask a thin question as a substitute for thinking. If you have nothing substantive to add, add it first.
Out of scope
Section titled “Out of scope”- Punctuation aesthetics, including em dashes and emojis, are out of scope. The repo’s tone allows them in skill prose. Voice rules govern reasoning, not typography.
- Audience-shaping is out of scope. Skills serve the user in front of them, not a generic audience.
- Do not ban Markdown structure in
.cheese/*artifacts. Use headers, bullets, and tables when content is genuinely list-shaped. This rule targets JSON-schema-style layout and AI cadence, not Markdown itself.