Skip to content

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

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.

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

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

  1. Identify the diff, scope, and relevant specification or issue. Read references/fan-out.md for the context checklist and deterministic planning contract. Collect instruction sources, build context, and run age.pyz age-route through 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 run python3 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.
  2. 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 with python3 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-ups section and every unresolved item. Copy each unresolved item into a ## Press findings sub-section. Keep the artifact and baseline values from that preamble for step 5. /cure reads 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).

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

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.

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: sprawling or fix-cost-later: structural. Also ask when two findings conflict. Also ask when the user passes --safe. Read references/handoff-detail.md first. Render the gate per ../cheese/references/handoff-gate.md. Pre-select the composite. Mark each heavy row.
  • Otherwise — Act. Announce the selection. Dispatch /cure per references/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).

  • Read the generated command inventory in references/commands.md.
  • Read references/dimensions.md before grading a finding.
  • Read references/fan-out.md before every review.
  • Read references/packet.md before assembling a fan-out context packet.
  • Read references/sub-agent-gate.md before a sub-agent dispatch.
  • Read references/handoff-detail.md before the selection gate or a /cure dispatch.
  • 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 --auto run.
  • Read references/voice.md before writing the report.
  • Read the applicable Rust, TypeScript, Python, Shell, or Go catalog before grading deslop.

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.

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

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.

Go error strings use lowercase text and no trailing punctuation. Go code wraps returned errors with %w.

// SLOP
return fmt.Errorf("Failed to open file: %s", err)
return errors.New("User not found.")
// CLEAN
return 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.

A bare return in a long function is the defect. A named result is not. Grade the bare return, not the declaration.

// SLOP
func 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
}
// CLEAN
func 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.

AI often generates context.TODO() and leaves it in place.

// SLOP
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := context.TODO()
result, err := db.Query(ctx, query)
}
// CLEAN — use the context you already have
func 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.

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.

// SLOP
func NewService(repo *Repository) *Service { ... }
// where Repository is an interface
// CLEAN
func NewService(repo Repository) *Service { ... }

AI spawns goroutines without cancellation paths.

// SLOP — runs forever, no way to stop it
go func() {
for {
doWork()
time.Sleep(time.Second)
}
}()
// CLEAN — respects context cancellation
go 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.

// SLOP
var result string
for _, s := range items {
result = fmt.Sprintf("%s%s", result, s)
}
// CLEAN
var b strings.Builder
for _, s := range items {
b.WriteString(s)
}
result := b.String()

perfsprint flags this pattern.

// SLOP — user.UserService, user.UserModel
package user
type UserService struct{}
type UserModel struct{}
// CLEAN — user.Service, user.Model
package user
type Service struct{}
type Model struct{}

Revive exported reports this issue (“type name will be used as user.UserService by other packages”).

AI puts complex initialization in init(). init() cannot return errors. init() runs at import time, so callers cannot control it.

// SLOP
func 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 controls
func NewDB(dsn string) (*sql.DB, error) {
return sql.Open("postgres", dsn)
}

gochecknoinits flags every init() function.

  • 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/goleak detects goroutine leaks during tests.

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.

AI defaults to C-style index loops.

# SLOP
for i in range(len(items)):
print(i, items[i])
# CLEAN
for i, item in enumerate(items):
print(i, item)

Iterate directly when you do not need the index:

for item in items:
process(item)

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 one
if 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 branch
if user and user.name:
greet(user.name)
# CLEAN — when the branches differ, keep the explicit check
if 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.

AI mixes %, .format(), and f-strings inconsistently.

# SLOP
message = "Hello, %s! You have %d messages." % (name, count)
message = "Hello, {}!".format(name)
# CLEAN — f-strings for a plain string
message = f"Hello, {name}! You have {count} messages."
# CLEAN — %-style for a logging call, which formats only when the record emits
logger.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.

An unhandled exception that the code swallows leaves no evidence. The defect then costs the most time to find.

# SLOP
try:
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(...)

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.

# SLOP
def get_user():
return {"id": 1, "name": "Alice", "email": "alice@example.com"}
# CLEAN
@dataclass
class User:
id: int
name: str
email: str
# SLOP
f = open("file.txt")
data = f.read()
f.close() # Never reached if f.read() throws
# CLEAN
with open("file.txt") as f:
data = f.read()

7. Overzealous type hints on obvious locals

Section titled “7. Overzealous type hints on obvious locals”
# SLOP
name: str = "Alice"
count: int = 0
items: list[str] = []
active: bool = True
# CLEAN — type hints on function signatures, not obvious assignments
name = "Alice"
count = 0
items: list[str] = [] # Empty collection annotation is fine (inference can't know the element type)
active = True

8. List comprehension where a generator suffices

Section titled “8. List comprehension where a generator suffices”
# SLOP — builds entire list in memory just to iterate
total = sum([x * x for x in range(1_000_000)])
# CLEAN — generator expression, lazy evaluation
total = sum(x * x for x in range(1_000_000))

def f(x=[]) shares one list across every call.

# SLOP
def append_item(item, items=[]):
items.append(item)
return items
# CLEAN
def append_item(item, items=None):
if items is None:
items = []
items.append(item)
return items

Ruff identifies this pattern with B006.

requests and httpx calls without timeout= can hang forever when the server hangs.

# SLOP
response = requests.get(url)
# CLEAN
response = requests.get(url, timeout=10)

Ruff identifies this pattern with S113.

The tryceratops family covers oversized try blocks with logging noise.

# SLOP — log-and-raise duplicates the traceback up the stack
try:
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 path
value = compute()
if value < 0:
raise ValueError("negative")
try:
data = load(value)
except OSError:
logger.exception("load failed")
raise
else:
return transform(data)

Ruff identifies this shape with TRY300, TRY301, TRY400, and TRY401.

Models that learn from pre-3.9 code emit typing.List/Optional/Union.

# SLOP
from typing import Dict, List, Optional, Union
def 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.

A module interleaves os.path.join, os.path.exists, and Path.

# SLOP
path = os.path.join(base, "config.yaml")
if os.path.exists(path): ...
# CLEAN
path = 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.

# SLOP
print(f"processing {item}")
# CLEAN — logging, or delete if the code is self-evident
logger.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.

# SLOP
match status:
case Status.ACTIVE:
return activate()
case Status.INACTIVE:
return deactivate()
case _:
pass
# CLEAN
from 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 form
if 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.

  • Ruff rule docs (docs.astral.sh/ruff/rules) verify every rule code above.
  • typing.assert_never (Python 3.11+, typing_extensions before) 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 pathlib rule follows the PTH123 dispute thread (discuss.python.org/t/106904).

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 &str instead of String in function parameters
  • Use .as_ref() on Option/Result instead of cloning to unwrap
  • Ban .clone() unless you can explain why you need owned data
// SLOP
fn greet(name: String) { println!("Hello, {name}"); }
let msg = my_string.clone();
greet(msg);
// CLEAN
fn greet(name: &str) { println!("Hello, {name}"); }
greet(&my_string);

Excessive .unwrap() calls create runtime panics throughout the codebase.

Fix:

  • Use the ? operator to propagate errors
  • Use anyhow or thiserror for structured errors
  • Use if let Some(x) or match for Option types
  • Use ? for every error you can propagate
  • Use .unwrap() only when the type system proves the value exists, such as a const or a checked index
  • A hardcoded regex is not a compile-time guarantee. Regex::new parses at run time. Use LazyLock plus expect("static regex"), or a compile-time macro crate
// SLOP
let file = File::open("config.toml").unwrap();
let config: Config = toml::from_str(&contents).unwrap();
// CLEAN
let file = File::open("config.toml")?;
let config: Config = toml::from_str(&contents)?;

Using String for every value loses type safety and adds unnecessary allocations.

Fix:

  • Accept &str or impl 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)
// SLOP
fn find_user(id: String, name: String) -> String { ... }
// CLEAN
fn find_user(id: &UserId, name: &str) -> Result<User> { ... }

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, ..] => ... }
// SLOP
for i in 0..items.len() {
process(i, &items[i]);
}
// CLEAN
for (i, item) in items.iter().enumerate() {
process(i, item);
}

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

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 return Result
  • .expect("context") panics. Use it only when the test cannot return Result
  • 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
// SLOP
assert!(result.is_ok());
assert!(result.is_err());
assert_eq!(count, 3); // no context on failure
// CLEAN — propagate the real error
let value = result.expect("scan_worktree should succeed");
assert_eq!(value.label, "Ready");
// CLEAN — check specific error variant
assert!(matches!(result, Err(MyError::NotFound { .. })));
// or check the message
let err = result.unwrap_err();
assert!(err.to_string().contains("not found"), "expected NotFound, got: {err}");
// CLEAN — failure message for non-obvious operands
assert_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 implements Debug and PartialEq
  • Use assert!(matches!(x, None), "got {x:?}") when it implements only Debug
  • Keep assert!(x.is_none()) when the inner type implements neither trait
  • For is_some(), extract the inner value and check it
// SLOP
assert!(x.is_none());
assert!(ping["result"]["host_type"].as_str().is_some());
// CLEAN
assert_eq!(x, None);
assert_eq!(ping["result"]["host_type"].as_str(), Some("daemon"));

A raw tokio::time::sleep call before assertions is fragile: it passes on fast machines and flakes in CI.

Fix:

  • Use a wait_until_async polling pattern with a timeout
  • Use sleep-then-assert only to test actual timing behavior
// SLOP
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(state.status(), "ready");
// CLEAN — poll with timeout
wait_until_async(Duration::from_secs(2), || async {
state.status() == "ready"
}).await.expect("status should reach ready");

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("");
}

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.

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.

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 error
fn 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 type
fn 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.

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 constants

Exception: FFI modules that wrap C libraries may need non_snake_case or non_camel_case_types to match the C API.

These three patterns almost certainly indicate hastily generated code:

// SLOP — debug macro left in source
fn process(data: &[u8]) {
#[allow(clippy::dbg_macro)]
dbg!(data); // this went to production
// ...
}
// CLEAN — remove the debug macro entirely
fn 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 framework
tracing::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
}

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 prefix
fn process(_name: &str, _config: &Config) { ... }

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 exactly
fn ffi_create_window(x: i32, y: i32, w: i32, h: i32, flags: u32) -> *mut Window { ... }

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.

Test code:

  • #[allow(dead_code)] on test utility functions
  • #[allow(unused)] in mod tests blocks
  • #[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_types matching C signatures
  • #[allow(unsafe_code)] when wrapping C libraries

Generated code:

  • build.rs output
  • 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 check immediately after generating code
  • Pin specific crate versions
  • Use Clippy: cargo clippy -- -W clippy::all
  • When in doubt, check docs against the current crate version

Implementing Deref on a wrapper so it “inherits” the inner type’s methods simulates OO inheritance, which Rust deliberately lacks.

// SLOP
struct AppConfig { base: Config }
impl Deref for AppConfig {
type Target = Config;
fn deref(&self) -> &Config { &self.base }
}
// CLEAN — delegate explicitly, or implement the shared trait
impl 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).

Using Box or Arc when plain ownership or a borrow works adds unnecessary indirection to avoid borrow-checker errors.

// SLOP
fn process(data: &Box<MyStruct>) { ... } // borrowed_box
struct Registry { items: Vec<Box<String>> } // vec_box
let cfg: Box<Config> = Box::new(Default::default()); // box_default
// CLEAN
fn 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.

Functions marked async by habit add unnecessary async behavior. An async fn that never awaits forces every caller into async machinery for no reason.

// SLOP
async fn config_path() -> PathBuf {
dirs::config_dir().expect("config dir").join("app")
}
// CLEAN
fn config_path() -> PathBuf { ... }

unused_async has false-negative gaps, so check async functions by hand.

#![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 deny

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

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 reasoning
let val = unsafe { *ptr.add(i) };
// CLEAN — safe alternative existed all along
let 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.
  • 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

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.

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.

This pattern causes the number-one shell bug. Unquoted variables break on spaces, globs, and empty values.

Terminal window
# SLOP
for file in $files; do
rm $file
done
# CLEAN
for file in "${files[@]}"; do
rm -- "$file"
done

Quote 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/bash
cd /some/directory # Might fail silently
rm -rf build/ # Now you're deleting in the wrong place
# SLOP — partial strict mode (common AI output)
#!/bin/bash
set -e
yq '.items[]' file.yaml | while read -r item; do # yq failure silently ignored
process "$item"
done
# CLEAN
#!/bin/bash
set -euo pipefail
cd /some/directory
rm -rf build/
  • The -e option makes the shell exit after an error.
  • The -u option reports undefined variables and catches typos such as $UESR instead of $USER.
  • The -o pipefail option 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.

set -e has documented gaps (BashFAQ/105). Do not assume that it catches every failure:

Terminal window
# MASKED — `local`'s own success hides the command's failure
local output=$(failing_cmd) # -e does NOT fire
# CLEAN — declare and assign in two steps
local output
output=$(failing_cmd) # -e fires here
# MASKED — -e is disabled inside a function used as a conditional
if my_func; then ... # failures inside my_func won't exit

The ls output is not machine-readable. Filenames with spaces, newlines, or special characters break parsers.

Terminal window
# SLOP
for file in $(ls *.txt); do
process "$file"
done
# CLEAN — glob directly
for file in *.txt; do
[[ -f "$file" ]] && process "$file"
done
# CLEAN — find for a recursive search
find . -name '*.txt' -print0 | while IFS= read -r -d '' file; do
process "$file"
done
# CLEAN — fd only when the project already declares it as a dependency
fd -e txt -x process {}

Prefer the standard command. Use fd only when the project declares it.

Terminal window
# SLOP
cat file.txt | grep "pattern"
cat file.txt | wc -l
# CLEAN
grep "pattern" file.txt
wc -l < file.txt

Backticks do not nest and are harder to read.

Terminal window
# SLOP
result=`command`
nested=`echo \`date\``
# CLEAN
result=$(command)
nested=$(echo "$(date)")

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.

Terminal window
# SLOP
if [ $var = "value" ]; then
if [ -z $maybe_empty ]; then
# CLEAN
if [[ "$var" == "value" ]]; then
if [[ -z "${maybe_empty:-}" ]]; then

AI writes absolute paths or assumes CWD.

Terminal window
# SLOP
source /home/user/project/lib/utils.sh
config_file=./config.yaml
# CLEAN
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/utils.sh"
config_file="${SCRIPT_DIR}/config.yaml"
Terminal window
# SLOP
MAX_RETRIES=3
BASE_URL="https://api.example.com"
# CLEAN
readonly MAX_RETRIES=3
readonly BASE_URL="https://api.example.com"

Write errors to stderr, not stdout.

Terminal window
# SLOP
echo "Error: file not found"
exit 1
# CLEAN
echo >&2 "Error: file not found"
exit 1
# Or with a helper
die() { echo >&2 "$@"; exit 1; }
die "file not found"
Terminal window
# SLOP
some_command
if [ $? -eq 0 ]; then
echo "ok"
fi
# CLEAN
if some_command; then
echo "ok"
fi
# CLEAN — error path
if ! some_command; then
die "some_command failed"
fi

ShellCheck rule SC2181 flags this pattern.

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.

Terminal window
# SLOP
cd "$build_dir"
rm -rf ./*
# CLEAN — in an executable script
cd "$build_dir" || exit 1
rm -rf ./*
# CLEAN — in a function of a sourced file
cd "$build_dir" || return 1
rm -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.

for x in $(cmd) splits output on whitespace instead of lines. This behavior breaks on spaces and globs.

Terminal window
# SLOP
for f in $(find . -name '*.log'); do
process "$f"
done
# CLEAN — NUL-delimited for filenames
find . -name '*.log' -print0 | while IFS= read -r -d '' f; do
process "$f"
done
# CLEAN — line-oriented command output
readarray -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.

Terminal window
# SLOP — prints 0
count=0
cat file | while read -r line; do
count=$((count + 1))
done
echo "$count"
# CLEAN — redirect (or process-substitute); no subshell
count=0
while read -r line; do
count=$((count + 1))
done < file

echo flags behave differently in the Bash builtin and /bin/echo. These flags are not POSIX-portable.

Terminal window
# SLOP
echo -e "line1\nline2"
echo -n "no newline"
# CLEAN
printf '%s\n' "line1" "line2"
printf '%s' "no newline"

These forms start external processes or use deprecated syntax for operations the shell performs natively.

Terminal window
# SLOP
i=$(expr $i + 1)
let i=i+1
result=$[ a + b ]
# CLEAN
(( i += 1 ))
result=$(( a + b ))

The Google Shell Style Guide says to always use (( )) or $(( )) for arithmetic.

Unquoted $@ and $* split on internal spaces and drop empty arguments.

Terminal window
# SLOP
my_func $@
# CLEAN
my_func "$@"
  • 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.

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.

When types become complex, AI gives up and uses any, which discards TypeScript’s type safety.

// SLOP
function processData(data: any): any {
return data.value;
}
// CLEAN
function 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");
}

AI mixes paradigms or uses promise chains when async/await provides clearer code.

// SLOP
function fetchUser(id: string) {
return fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => data.user)
.catch(err => console.error(err));
}
// CLEAN
async 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
}

AI adds debug logging and leaves it in the code.

// SLOP
console.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.

forEach does not await asynchronous callbacks. Those callbacks run, but their promises become unobserved.

// SLOP — these await calls do nothing useful
items.forEach(async (item) => {
await processItem(item); // Runs concurrently, forEach doesn't wait
});
// CLEAN — sequential
for (const item of items) {
await processItem(item);
}
// CLEAN — concurrent with control
await 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 | null
function 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 type
function 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”
// SLOP
const cloned = JSON.parse(JSON.stringify(user));
// CLEAN — when the value holds only structured-cloneable data
const 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”
// SLOP
const count: number = 0;
const name: string = user.name;
const isActive: boolean = true;
const users: User[] = getUsers();
// CLEAN — inference handles these
const count = 0;
const name = user.name;
const isActive = true;
const users = getUsers(); // Return type already typed
// Keep annotations on empty collections or ambiguous initializers
const 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 one
import { UserService, UserModel, UserDTO, UserMapper, UserValidator } from "./users";
// CLEAN — import only what you use
import { 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).

// SLOP
const user = users.find(u => u.id === id)!;
processUser(user);
// CLEAN
const 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.

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.

// SLOP
const config = JSON.parse(raw) as unknown as Config;
// CLEAN — validate at the boundary
const config = configSchema.parse(JSON.parse(raw)); // zod or similar

11. @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-ignore
legacyCall(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.

Fire-and-forget asynchronous calls can lose rejections and create accidental ordering.

// SLOP
saveUser(user); // not awaited — errors disappear
items.map(async i => await process(i)); // array of dropped promises
// CLEAN
await 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.

Enums often reflect habits from other languages. Literal unions erase during compilation, serialize directly, and need no runtime object.

// SLOP
enum Status { Active = "active", Inactive = "inactive" }
// CLEAN
type Status = "active" | "inactive";
// When you need the values at runtime:
const STATUSES = ["active", "inactive"] as const;
type Status = (typeof STATUSES)[number];

A catch (e: any) block can log and rethrow the error at every level. No level handles the error.

// SLOP
try {
await handler(req);
} catch (e: any) {
console.error(e);
throw e;
}
// CLEAN — catch only where you add value; the error is unknown, not any
try {
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.

AI often uses effects to compute values or chain fetches.

// SLOP — derived state via effect
const [fullName, setFullName] = useState("");
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
// CLEAN — derive during render
const 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.

// SLOP
switch (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 PII
function 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.

  • The typescript-eslint strict-type-checked configuration and rule documentation provide the source of truth for every rule named above, except switch-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 !/as overuse.

Each dimension has its own rubric.

Each dimension answers what kind of problem. Severity answers how bad this one is. The two remain orthogonal.

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

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.

  1. Base — Use the dimension’s per-tier rubric (see § Per-dimension rubrics below).
  2. Location bump — Add one tier when location = contract and the dimension is location-sensitive (see § Location sensitivity).
  3. Compounding bump — Add one tier when fix-cost-later = structural.

Do not compute the formula mentally. Invoke src/easy_cheese/shared/severity.py compute:

Terminal window
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 | low

A 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).

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

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

“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:

Terminal window
python3 skills/age/scripts/age.pyz severity bucket --files <N> [--modules <M>]
# -> contained | moderate | sprawling

Source priority for the raw count:

  1. tilth_deps — primary. It returns the file set that needs changes.
  2. 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.

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

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.

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.

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.

Each dimension uses a base-severity table for each violation shape before modifiers. Apply location and compounding modifiers after the base tier.

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

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

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>/index instead 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”.

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”

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 data to <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”

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

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 with Promise.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”

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_id through 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”

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

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.

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

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.

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.

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.

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.

  1. Render the numbered selection table:

    python3 skills/age/scripts/age.pyz findings render-table --report .cheese/age/<slug>.md

    Mark any sprawling/structural-fix row as heavy.

  2. 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 in SKILL.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 all for 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. Add cheap to 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 /plate resolves topology and publishes. Carry --hard.

  • Checkpoint & stop — run /wheypoint to 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 /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.

/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>.md with 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. Set next: cure when at least one finding meets the medium+ floor. Set next: done when no finding meets that floor.
  • The fixed chain length of the fan pathway enforces the two-cure-pass cap. Age counts no passes. ## Auto mode below states the same rule. Publish the terminal age report only with next: done. Treat next: cure or a missing next as 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 observed dispatched: <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 records verifier: skipped (sub-agent) separately.

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-pr and --hard when 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.

Read ## Within cook's own fan pathway above for the no-chain isolation directive. Follow that directive before you write the report.

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.

  1. 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.
  2. Component roles — Include relevant dependency manifests and the component/path map. Distinguish library exports, application entry points, tests, build systems, and documentation.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. Output and ownership — Include the exact per-finding format, also-relevant-to, applicable invariants from report-example.md, and the reconciliation contract below.

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.

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.

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.

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.

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.

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.

Read this with SKILL.md § Output for a concrete report skeleton.

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`).

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 | done
artifact: <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.

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.

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.

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.

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.

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.

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

  • 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.
  • 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 /age records 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”.

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”. Give file:line evidence. 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.

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