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