/age
When to invoke: Review a diff, PR, branch, or path across ten orthogonal dimensions (correctness, security, encapsulation, spec, complexity, deslop, assertions, NIH, efficiency, telemetry) and emit a severity-grouped findings report. Use when the user wants a code review — phrases like “review this”, “/age”, “is this safe to merge”, “find bugs”, “spot security issues”, “check for slop”, “review my PR”, “what’s wrong with this code”. Use even when the user only asks for one dimension — the report scopes itself. Do NOT use for applying fixes (route to /cure) or test hardening (route to /press).
Review a diff or scoped path before merging, after /press, or whenever the user wants evidence-backed observations rather than an approval verdict. Do not apply fixes here — /cure owns application.
Inputs
Section titled “Inputs”/age [<ref-or-range>] [--scope <path>] [--comprehensive] [--full] [--safe] [--open-pr] [--auto] [--html]/age <slug> [--full] [--safe] [--open-pr] [--auto] [--html]--full un-collapses the ## Low section when 10 or more low-severity findings exist (the default report collapses them 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.
When called with a <slug>, resolve .cheese/press/<slug>.md (if present) for press context and review the current working diff. When called with a <ref-or-range>, review that range. Default to the current working diff when neither is supplied. If the base branch is unclear, ask or use the repository’s documented default.
--auto is the propagated autonomous-mode flag from /cook --auto. It changes the handoff (see ## Handoff and ## Auto mode for the cap rule and full chain).
--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 python3 src/age/age-html-report.py --report .cheese/age/<slug>.md --slug <slug> (bundle fallback: age.pyz html-report with the same flags), and print the returned path. It groups findings by severity into the shared HTML shell (shared/scripts/html_report.render_document) — offline, no CDN, no JS.
Portability reference: ../cheese/references/harness-portability.md covers helper resolution, sub-agent dispatch, GitHub operations, and handoff transitions; prefer the bundled or repo-local helper first, treat ${CLAUDE_SKILL_DIR} as optional host-provided fallback. The handoff blocks below are the portable contract; slash commands are host renderings, not the control model.
Review dimensions
Section titled “Review dimensions”Dimensions answer what kind of problem. Severity (blocker / high / medium / low) is per-finding, computed from base + location + compounding modifiers (see references/dimensions.md § Severity computation).
| Dimension | Base range |
|---|---|
| correctness | low → blocker |
| security | low → blocker |
| encapsulation | low → blocker |
| spec | low → blocker |
| complexity | low → high |
| deslop | low → high |
| assertions | low → blocker |
| nih | low → high |
| efficiency | low → blocker |
| telemetry | low → blocker |
Per-dimension base-severity tables, location-sensitivity, fix-cost-now / fix-cost-later, and recommendation shapes live in references/dimensions.md — read it before computing any finding’s severity. This reduced workflow intentionally omits the git-history/precedent dimension.
-
Identify the diff, scope, and relevant spec or issue. Mode check: compute the review range’s
review_surfacescore and risk flags, then callage_route.route(score=..., risk_flags=..., entry="age")(src/fanout/age_route.py).n=1— steps 2–4 below, unchanged. Anyn>1— readreferences/fan-out.mdfirst; itslenseslist, not a fixed label, sets worker count. Fan-out also requires/agenot itself be a sub-agent — stay single-parent when it is. Thread the router’seffortinto the reviewer dispatch. -
Gather evidence: diff, touched files, tests, callers/imports. If a press report exists for this slug, read it via
python3 shared/scripts/read_handoff_slug.py --phase press --slug <slug>(bundle fallback:common.pyz read_handoff_slug --phase press --slug <slug>) and summarise unresolved items in a## Press findingssub-section —/cureonly reads.cheese/age/<slug>.md.No press report but a cook handoff exists: record
press: skipped(see## Output) and print the warning at handoff. No cook artifact either: skip the marker and continue.If
.cheese/glossary/<slug>.mdexists, read it so naming drift can be flagged as a deslop finding. -
Review every dimension; dimensions with no findings simply omit themselves. Report every defect, however minor — never self-filter on perceived significance; filtering happens downstream in the verifier pass (
n>1) or in severity computation (single-parent). Do not raise a finding for a gate failure identical to the diff’s recordedbaseline:block — see../cook/references/quality-gates.md; flag only new or changed failures. -
Compute severity per finding (base + location bump + compounding bump, capped at
blocker). Group findings by severity (## Blocker → ## High → ## Medium → ## Low); within a severity group, order by file. -
Write the report (see
## Output), thenpython3 shared/scripts/write_handoff_artifact.py --phase age --slug <slug> --status ok --next cure --artifact "" --orientation "<one-line orientation>" --durable-flags "<none | one line per flag>" --body-file "$report_file"(bundle fallback:${CLAUDE_SKILL_DIR}/scripts/common.pyz write_handoff_artifactwith the same flags). Print the path. -
Hand off (see
## Handoffbelow).
Preferred tools and fallbacks
Section titled “Preferred tools and fallbacks”Call source-code backends directly according to the shared code-intelligence-routing.md contract. For caller graphs, use the selected semantic backend’s caller query plus tilth_deps when available.
| Need | Prefer | Fallback |
|---|---|---|
| Diff inspection | delta |
git diff --unified=3 |
| Caller/dependency impact + curated review context | semantic caller search + tilth_deps |
manual scoping; note the precision loss |
| Architecture / hotspot framing for large diffs | changed-file map + caller/dependency evidence | skip and note in confidence |
| Design rationale for encapsulation/spec dimensions (optional) | mcp__hallouminate__list_corpora / mcp__hallouminate__ground on repo:<repo>:wiki, grounding design intent before grading, rendering consulted pages in ## Wiki context |
skip; omit ## Wiki context; proceed with diff + code evidence only; cap at speculating when rationale is the primary evidence |
| GitHub/PR context | gh |
local git commands or user-provided PR data |
| Merge/conflict awareness | mergiraf | manual conflict checks |
Optional MCPs: hallouminate and milknado follow the detect-and-degrade contract in ../cheese/references/optional-plugins.md — state absence once, fall back, reduce confidence only if evidence quality suffers, never block.
Sub-agent fan-out
Section titled “Sub-agent fan-out”/age sizes its own fan-out via the age router (src/fanout/age_route.py), not a size-only threshold, and resolves every dispatched worker through ../cheese/references/agent-resolution.md (read-only, fresh-context; exact specialist, then compatible specialist, then a prompt-constrained general agent with degraded: true). Mechanics: references/fan-out.md, references/packet.md, references/sub-agent-gate.md.
Output
Section titled “Output”Cross-cutting house style and citation form: ../cheese/references/formatting.md. This section owns the findings-report shape; formatting.md owns the voice rules and the footnote primitive.
Write to .cheese/age/<slug>.md with a minimum handoff slug at the top — status, next, artifact, durable_flags, baseline, one-line orientation. press: skipped is the first body line after the blank separator when a cook artifact exists but no press report does; omit it otherwise.
Body, in order: # Age Report — <slug>; ## Orientation (1-2 sentences); ## Press findings (omit unless a press report exists); ## Wiki context (omit unless hallouminate grounding hit); severity sections (**[dim:sev]** path:line — claim, then location: <tier> · fix-cost-now: <tier> · fix-cost-later: <tier> · confidence: <tier>, then recommendation: <action>); ## Confidence; ## Next step. See references/report-example.md for the full skeleton plus a worked instantiation.
Empty severity sections are omitted entirely. When ten or more low findings exist, collapse the ## Low section to a single line:
## Low*N low-severity findings suppressed.* Re-run with `--full` (or `/age --full`) to see them.Per-finding confidence: uses the voice-kernel scale (references/voice.md § Reasoning posture): certain — verified by direct evidence (diff/code read, command output); speculating — inferred from indirect signal. A don't know grading never ships as a finding row — gather the missing evidence or drop the claim. Suppressed lows feed the cure-selection table only when --full is passed.
status: ok when the review completed; status: halt: <reason> when evidence was unreachable. next: cure when any finding meets the medium+ floor; next: done otherwise. durable_flags: mirrors cook’s gate: default none.
Then print Age report: .cheese/age/<slug>.md. When press: skipped is set, also print: Warning: no /press report for <slug> — hardening was skipped. Run /press <slug> first, or continue with /cure. When --html is passed, also print the HTML path returned by html-report (render command under --html in ## Inputs).
Handoff
Section titled “Handoff”Pipeline: culture → mold → cook → press → [age] → cure → plate
Compute the recommended set. Composite all-medium, cheap: the medium floor (blocker+high+medium) unioned with every Low at fix-cost-now: contained.
Decide act vs ask:
- Empty set — write
next: done, print the report path, stop. - Reason to ask — a set member has
fix-cost-now: sprawlingorfix-cost-later: structural, findings conflict, or--safewas passed: readreferences/handoff-detail.mdand render the gate per../cheese/references/handoff-gate.md, pre-selecting the composite and flagging heavy rows. - Otherwise — act: announce the selection and dispatch
/cureperreferences/handoff-detail.md§ Dispatch. No gate.
--auto substitutes a severity-floor selection (## Auto mode below); references/handoff-detail.md also covers the no-chain override under /cook’s fan pathway.
Auto mode
Section titled “Auto mode”When invoked with --auto:
- Skip the handoff gate.
- If two cure passes have already completed (cap reached), stop and surface the final report — do not invoke
/cureagain even if findings remain. - Otherwise, if any finding meets the medium+ floor — invoke
/cure <slug> --auto --stake medium+(forward--open-prwhen it is in scope) and increment the cure-pass count when it returns. - If no finding meets the medium+ floor, stop the chain with a one-line “auto chain clean” note and the report path.
Within cook’s own fan pathway
Section titled “Within cook’s own fan pathway”/cook’s fan pathway spawns age as a fresh-context sub-agent and owns the chain itself. Read references/handoff-detail.md § Within cook’s own fan pathway for the no-chain isolation directive before writing the report.
- Review is not a verdict; explain where to look and why.
- Do not edit production files;
/cureowns application. - Do not raise a finding for a gate failure identical to the diff’s recorded
baseline:block; flag only new/changed failures per../cook/references/quality-gates.md. - Default to acting: auto-select the recommended set, dispatch
/curewithout a gate. Ask first only on a genuine reason or--safe. An empty recommended set is a clean stop, not a question. - Do not invent evidence; cite files, diffs, commands, or unavailable-source notes.
- Agree when the diff is fine — an empty dimension is a valid outcome, not a gap to fill.
- Keep confidence qualitative (
certain | speculating | don't know) at both the report and per-finding level; never a numeric score. - Findings carry location + recommendation, not JSON sidecars or tag-anchored fix payloads —
/curereads the markdown directly. - Apply
references/voice.md(output discipline, reasoning posture, confidence vocabulary).
References
Section titled “References”references/dimensions.md— before grading any finding: rubrics, location sensitivity, fix-cost tables, recommendation shapes.references/fan-out.md— before anyn>1dispatch: router mechanics, lens partitions, six-seam sequence, verifier pass.references/packet.md— when assembling the Seam-2 shared context packet for a fan-out run.references/sub-agent-gate.md— before any sub-agent dispatch: digest contract, harness-agnostic selection, what the parent never delegates.references/handoff-detail.md— before the selection gate or a/curedispatch: gate menu, dispatch payload, cook-fan-pathway no-chain override.references/report-example.md— alongside## Output: worked rendering plus the full placeholder skeleton.references/voice.md— when writing the report: output discipline, reasoning posture, confidence vocabulary.references/deslop-rust.md,references/deslop-typescript.md,references/deslop-python.md,references/deslop-shell.md,references/deslop-go.md— when gradingdeslopin that language: pattern catalogs with lint-rule mappings.
Agent resolution
Section titled “Agent resolution”Resolve every dimension worker and fresh-context review through ../cheese/references/agent-resolution.md.
| Work | Preferred types | Permissions/isolation | Minimum power | Effort | Fallback |
|---|---|---|---|---|---|
| Review a diff or one dimension | reviewer | read-only, fresh-context | powerful | high | compatible reviewer, then general |
The canonical age report/handoff carries the shared agent_resolution block.
Go De-slop Catalog
Section titled “Go De-slop Catalog”Per-language evidence for the age deslop dimension. Each pattern is a Go-specific AI tell to look for during review; most map to a staticcheck or golangci-lint rule, giving a citable rule name to attach to a finding. Use alongside dimensions.md’s deslop rubric — this is the “Look for” detail, not a separate severity scale.
1. Error string conventions
Section titled “1. Error string conventions”Go errors are lowercase, no trailing punctuation, and wrap 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.
Lint: staticcheck ST1005 (error-string capitalization/punctuation); errorlint for %w/%v wrapping.
2. Named returns with bare return
Section titled “2. Named returns with bare return”AI loves named returns. They obscure which values are being returned.
// 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}Named returns are acceptable only in defer recovery patterns.
Lint: nakedret; revive bare-return.
3. context.TODO() permanently
Section titled “3. context.TODO() permanently”AI scaffolds with context.TODO() and never replaces it.
// 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() means “I haven’t decided which context to use yet.”
In production code, you should always have decided.
4. Pointer to interface
Section titled “4. Pointer to interface”Almost never correct. Interfaces are already reference types.
// 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)Lint: go.uber.org/goleak catches leaked goroutines at test time.
6. fmt.Sprintf for string concatenation in loops
Section titled “6. fmt.Sprintf for string concatenation in loops”O(n²) string building.
// 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()Lint: perfsprint.
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{}Lint: revive exported (“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() which can’t return errors
and runs at import time with no control.
// 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)}Lint: gochecknoinits flags any init() function.
Sources
Section titled “Sources”- staticcheck docs (staticcheck.dev/docs/checks) — the
ST1005error-string check - Go wiki, Code Review Comments (go.dev/wiki/CodeReviewComments) — error strings, naked returns, package-name stutter, contexts
- Uber Go Style Guide (github.com/uber-go/guide) — goroutine lifetimes,
init()avoidance - golangci-lint linters index (golangci-lint.run/usage/linters) —
nakedret,perfsprint,gochecknoinits, revive rules - go.uber.org/goleak — test-time goroutine-leak detection
Python De-slop Catalog
Section titled “Python De-slop Catalog”Per-language evidence for the age deslop dimension. Each pattern is a Python-specific AI tell to look for during review; most map to a Ruff rule code, giving a citable rule name to attach to a finding. Use alongside dimensions.md’s deslop rubric — this is the “Look for” detail, 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)When you don’t need the index at all, just iterate directly:
for item in items: process(item)2. Manual None checks instead of truthiness
Section titled “2. Manual None checks instead of truthiness”# SLOPif user is not None and user.name is not None and len(user.name) > 0: greet(user.name)
# CLEANif user and user.name: greet(user.name)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 everywhere (Python 3.6+)message = f"Hello, {name}! You have {count} messages."4. Silent except: pass
Section titled “4. Silent except: pass”Swallowing exceptions without handling is the #1 debugging time-sink.
# SLOPtry: risky_operation()except Exception: pass # Silent failure — good luck debugging
# 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"} where a dataclass gives you
type safety, IDE support, and self-documenting code.
# 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: B006.
10. HTTP calls without a timeout
Section titled “10. HTTP calls without a timeout”requests/httpx calls with no timeout= hang forever when the server does.
# SLOPresponse = requests.get(url)
# CLEANresponse = requests.get(url, timeout=10)Ruff: S113.
11. try/except shape slop
Section titled “11. try/except shape slop”Oversized try blocks with logging noise — the tryceratops family.
# 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: TRY300, TRY301, TRY400, TRY401.
12. Deprecated typing forms
Section titled “12. Deprecated typing forms”Models trained on 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: UP006, UP007, UP045.
13. os.path / pathlib mixing
Section titled “13. os.path / pathlib mixing”os.path.join, os.path.exists, and Path interleaved in the same module.
# SLOPpath = os.path.join(base, "config.yaml")if os.path.exists(path): ...
# CLEANpath = Path(base) / "config.yaml"if path.exists(): ...Ruff: PTH family. Caveat: open(path) on a Path is fine — PTH123
(force Path.open()) is contested among core devs; don’t “fix” it.
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)Sources
Section titled “Sources”- Ruff rule docs (docs.astral.sh/ruff/rules) — every rule code above is verifiable there
- charlax/professional-programming, error-handling anti-patterns — before/after exception examples
- Greg-style caveat: PTH123 dispute thread (discuss.python.org/t/106904) — calibration for the pathlib rule
Rust De-slop Catalog
Section titled “Rust De-slop Catalog”Per-language evidence for the age deslop dimension. Each pattern is a Rust-specific AI tell to look for during review; most map to a clippy lint, giving a citable rule name to attach to a finding. Use alongside dimensions.md’s deslop rubric — this is the “Look for” detail, 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 - Rule:
.clone()is banned 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”Creates runtime panics scattered throughout the codebase.
Fix:
- Use
?operator for error propagation - Use
anyhoworthiserrorfor structured errors - Use
if let Some(x)ormatchforOptiontypes .unwrap()only for compile-time guarantees (hardcoded regex, constants)
// 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”Losing type safety and adding unnecessary allocations.
Fix:
- Accept
&strorimpl AsRef<str>in function parameters - Use
Cow<'_, str>when sometimes owned, sometimes borrowed - Create newtypes for domain concepts:
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 — the #1 AI test smell
Section titled “6. Weak assertions — the #1 AI test smell”assert!(result.is_ok()) and assert!(result.is_err()) swallow the actual
error/value on failure, printing only false.
Fix:
- Propagate with
.expect("context")or?to see the real error - Check actual values, not just existence
- For errors, verify the specific variant with
matches!or check the message - Every
assert_eq!/assert!with non-obvious operands needs a failure message
// 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”assert!(x.is_none()) prints assertion failed: false. assert_eq! shows what was actually there.
Fix:
- Use
assert_eq!(x, None)for better failure messages - For
is_some(), extract and check the inner value
// 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”Raw tokio::time::sleep before assertions is fragile — passes on fast machines, flakes in CI.
Fix:
- Use a
wait_until_asyncpolling pattern with timeout - Sleep-then-assert is only acceptable for testing 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] passes on any panic — including unrelated ones from
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 only prove the code doesn’t panic — not that it works.
Fix:
- Add assertions on return values or side effects
- If intentionally testing “no panic”, add an explicit comment documenting 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 as band-aid (#[allow(...)])
Section titled “11. Lint suppression as band-aid (#[allow(...)])”AI sprinkles #[allow(...)] to silence warnings instead of fixing root causes.
The compiler is telling you something — listen, don’t muzzle it.
Crate-level nuclear options (instant fail)
Section titled “Crate-level nuclear options (instant fail)”These suppress warnings globally and are never legitimate in production code:
// SLOP — the nuclear option#![allow(warnings)]#![allow(clippy::all)]
// SLOP — the scaffold dump (3+ together = AI signature)#![allow(dead_code)]#![allow(unused_imports)]#![allow(unused_variables)]Fix: Delete the allows and fix each warning individually. If there are too many warnings, the code has bigger problems than lint noise.
The AI scaffold cluster
Section titled “The AI scaffold cluster”These five attributes appearing together are the highest-confidence 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 has a specific fix — the allow hides which one is needed. Remove the allow, read the warning, apply the real fix.
Clippy suppression smells
Section titled “Clippy suppression smells”Red Flag (almost always slop — these suppress restrictions for a reason):
// 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 (often slop, but context-dependent — check before removing):
// 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 (style preference, not necessarily 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 = 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 wrapping C libraries may legitimately 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”Three patterns that 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”An allow that duplicates what 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”The further an allow reaches, the worse it smells:
| 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: If you must allow, scope it to the narrowest possible target and add a comment explaining 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 groupings as a heuristic when judging whether 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: Never suppress restriction lints casually. Pedantic lints are opt-in,
so suppressing them is more defensible. Complexity lints need justification.
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)]in test assertions (idiomatic)
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
Red flag check: If the allow targets a restriction lint (unwrap, panic, todo, print) in these contexts, it’s still slop. Only pedantic/style/perf lints are genuinely legitimate here.
12. Hallucinated APIs and deprecated syntax
Section titled “12. Hallucinated APIs and deprecated syntax”AI generates functions that don’t exist or uses outdated API patterns
(e.g., 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 —
simulating the OO inheritance Rust deliberately doesn’t have.
// 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() }}Deref is for smart pointers. No clippy lint catches this — review by hand.
(rust-unofficial/patterns, anti-patterns chapter.)
14. Boxing reflex
Section titled “14. Boxing reflex”Box/Arc where plain ownership or a borrow works — indirection reached for
to make the borrow checker go away.
// 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: borrowed_box, vec_box, box_collection, box_default.
Inverse case: a very large enum variant SHOULD be boxed
(large_enum_variant) — boxing isn’t wrong, unmotivated boxing is.
15. async fn with no .await
Section titled “15. async fn with no .await”Functions marked async by habit. An async fn that never awaits forces every caller into the async machinery for nothing.
// SLOPasync fn config_path() -> PathBuf { dirs::config_dir().expect("config dir").join("app")}
// CLEANfn config_path() -> PathBuf { ... }clippy: unused_async (has false-negative gaps — also check by hand).
16. #![deny(warnings)]
Section titled “16. #![deny(warnings)]”Turns every future compiler warning into a build break — the crate stops compiling on a new toolchain that added a lint.
// SLOP#![deny(warnings)]
// CLEAN — enforce in CI instead:// RUSTFLAGS="-D warnings" cargo build(rust-unofficial/patterns, anti-patterns chapter.)
17. anyhow::Error in a library’s public API
Section titled “17. anyhow::Error in a library’s public API”anyhow is for applications. A library returning anyhow::Error gives
callers nothing to match on.
// 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”Agent-speed pressure produces unsafe as a borrow-checker escape hatch.
Bun’s audit of its AI-assisted Zig→Rust port found 13,365 unsafe call sites
needing review. For every unsafe block ask: is there a safe alternative, is
the invariant documented, is it tested?
// 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”Per-language evidence for the age deslop dimension. Each pattern is a shell-specific AI tell to look for during review; most map to a ShellCheck code, giving a citable rule name to attach to a finding. Use alongside dimensions.md’s deslop rubric — this is the “Look for” detail, not a separate severity scale.
1. Unquoted variables
Section titled “1. Unquoted variables”The #1 shell bug. Breaks 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 (protects against filenames starting with -).
2. Missing or incomplete set -euo pipefail
Section titled “2. Missing or incomplete set -euo pipefail”AI scripts either omit strict mode entirely or use partial set -e without
-u and -o pipefail. Both 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/-e: Exit on error-u: Error on undefined variables (catches typos like$UESRinstead of$USER)-o pipefail: Pipeline fails if any command fails (not just the last)
All three flags together. set -e alone is a half-measure — especially
dangerous in scripts that pipe through jq/yq/grep where a failure
on the left side is silently swallowed.
Strict mode is not a cure-all
Section titled “Strict mode is not a cure-all”set -e has sharp edges (BashFAQ/105) — don’t assume it catches everything:
# 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”ls output is not machine-readable. Filenames with spaces, newlines,
or special characters break everything.
# SLOPfor file in $(ls *.txt); do process "$file"done
# CLEAN — glob directlyfor file in *.txt; do [[ -f "$file" ]] && process "$file"done
# CLEAN — fd for complex searchesfd -e txt -x process {}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 don’t nest and are harder to read.
# SLOPresult=`command`nested=`echo \`date\``
# CLEANresult=$(command)nested=$(echo "$(date)")6. [ ] instead of [[ ]]
Section titled “6. [ ] instead of [[ ]]”[[ ]] is safer: no word splitting, supports regex, no quoting surprises.
# 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”Errors go 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: SC2181.
11. cd without a fallback
Section titled “11. cd without a fallback”The highest-consequence tell: a failed cd (typo, permissions) lets every
following command — including rm -rf — run in the wrong directory.
# SLOPcd "$build_dir"rm -rf ./*
# CLEANcd "$build_dir" || exit 1rm -rf ./*ShellCheck: SC2164.
12. Iterating command output with for
Section titled “12. Iterating command output with for”for x in $(cmd) splits on whitespace, not lines — 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: SC2044 (find loops), SC2046 (unquoted $(...) generally).
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 vanish.
# 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’s flag behavior differs between the bash builtin and /bin/echo and
isn’t 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”External processes and deprecated syntax for what the shell does natively.
# SLOPi=$(expr $i + 1)let i=i+1result=$[ a + b ]
# CLEAN(( i += 1 ))result=$(( a + b ))Google Shell Style Guide: always (( )) / $(( )).
16. Bare $@ / $* for argument forwarding
Section titled “16. Bare $@ / $* for argument forwarding”Unquoted, both split on internal spaces and drop empty arguments.
# SLOPmy_func $@
# CLEANmy_func "$@"Sources
Section titled “Sources”- ShellCheck wiki (shellcheck.net/wiki/SCxxxx) — canonical slop→fix rationale per code
- Greg’s Wiki: BashPitfalls + BashFAQ/105 — the
set -ecalibration source - Google Shell Style Guide — arithmetic, quoting, loop idioms, when not to use bash at all
TypeScript / JavaScript De-slop Catalog
Section titled “TypeScript / JavaScript De-slop Catalog”Per-language evidence for the age deslop dimension. Each pattern is a TS/JS-specific AI tell to look for during review; most map to a typescript-eslint rule, giving a citable rule name to attach to a finding. Use alongside dimensions.md’s deslop rubric — this is the “Look for” detail, not a separate severity scale.
1. any as escape hatch
Section titled “1. any as escape hatch”When types get complex, AI gives up and uses any, throwing away
everything TypeScript provides.
// 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 where async/await is cleaner.
// 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 that never gets removed.
// 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
if observability is needed, or remove entirely if the code is
self-evident.
4. Array.forEach with async callbacks
Section titled “4. Array.forEach with async callbacks”forEach doesn’t await — async callbacks fire and are silently dropped.
// 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}`;}
// CLEANfunction greet(name: string | null): string { return name ? `Hello, ${name}` : "Hello, stranger";}Lint: @typescript-eslint/no-unnecessary-condition — catches provably
true/false conditions in general, not just null checks.
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 — structuredClone (available in all modern runtimes)const cloned = structuredClone(user);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. Over-importing from barrel files
Section titled “8. Over-importing from barrel files”// SLOP — grabs everything, bloats bundleimport { UserService, UserModel, UserDTO, UserMapper, UserValidator } from "./users";
// CLEAN — import only what you useimport { UserService } from "./users";9. Non-null assertion as narrowing substitute
Section titled “9. Non-null assertion as narrowing substitute”! tells the compiler to shut up; a guard tells it something true. AI-authored
PRs use ! and as at a much higher rate 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: @typescript-eslint/no-non-null-assertion.
10. Double assertion to force a type
Section titled “10. Double assertion to force a type”as unknown as T makes any value claim any type — it erases the type system
at exactly the spot most likely to be wrong.
// 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 silences forever — when the underlying error is fixed, the stale
directive stays. @ts-expect-error fails when it no longer suppresses anything.
// SLOP// @ts-ignorelegacyCall(data);
// CLEAN// @ts-expect-error — legacy API typed wrong upstream (issue #123)legacyCall(data);Lint: @typescript-eslint/ban-ts-comment (set minimumDescriptionLength).
12. Floating promises
Section titled “12. Floating promises”Fire-and-forget async calls — rejections vanish, ordering is accidental.
// 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? Say so:void saveUser(user);Lint: @typescript-eslint/no-floating-promises, @typescript-eslint/no-misused-promises.
13. enum where a union suffices
Section titled “13. enum where a union suffices”Enums imported from other languages’ habits. Literal unions are erasable, serializable, 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”catch (e: any) plus log-and-rethrow: the error is logged at every level and
handled at none.
// 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.
15. useEffect for derived state (React)
Section titled “15. useEffect for derived state (React)”Effects reached for 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 catches this (react-hooks/exhaustive-deps doesn’t) — review by hand.
See react.dev “You Might Not Need an Effect”.
Sources
Section titled “Sources”- typescript-eslint
strict-type-checkedconfig + rule docs — ground truth for every rule named above - Effective TypeScript, 2nd ed. (Vanderkam, 2024) — ch. 5 on narrowing
any’s scope - arXiv 2602.17955 — empirical AI-vs-human PR comparison (
!/asoveruse)
Review dimensions
Section titled “Review dimensions”Each dimension has its own rubric.
Dimensions answer what kind of problem. Severity answers how bad this one is. The two stay orthogonal.
Severity vocabulary
Section titled “Severity vocabulary”Four tiers, in order:
blocker > high > medium > low| Tier | Meaning |
|---|---|
blocker |
Do not merge — contract broken, exposure open, or data at risk |
high |
Fix before merge — risk of incident or rework |
medium |
Real defect — fix before next release |
low |
Annoyance — safe to merge, fix at leisure |
Severity computation
Section titled “Severity computation”Each finding’s severity is computed, not declared. Three independent contributors, max-merged, capped at blocker:
- Base — from the dimension’s per-tier rubric (see § Per-dimension rubrics below).
- Location bump —
+1tier iflocation = contractand the dimension is location-sensitive (see § Location sensitivity). - Compounding bump —
+1tier iffix-cost-later = structural.
Do not compute the formula in-head — invoke shared/scripts/severity.py compute:
python3 shared/scripts/severity.py compute \ --dimension <dim> --base <low|medium|high|blocker> \ --location <class|module|cross-module|contract> \ --fix-cost-later <contained|spreading|structural># -> blocker | high | medium | lowMental shortcut: a class-private encapsulation leak lands low; the same leak at a slice’s index re-export lands 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 | 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 | reviewer |
Location classification
Section titled “Location classification”| Tier | Definition |
|---|---|
class |
Within a single class / type / file’s private scope. Caller graph stays inside the file. |
module |
Within a single module / slice. Crosses files but stays inside the slice’s internal namespace. |
cross-module |
Reaches into another module’s internals (bypasses the public index/crust). |
contract |
Crosses an ingress/egress boundary: slice’s public index re-exports, HTTP/RPC handler signature, DB schema, language-FFI boundary, plugin extension point, published library API. |
Language-agnostic note: in projects without an explicit public-index layer (flat scripts, no crust / __init__ re-export surface, as in this repo’s src/melt, src/affinage, src/mold, src/fanout), treat a direct import of another file’s internal function across a directory boundary as cross-module, and CLI argv / stdin ingress as contract.
Location sensitivity
Section titled “Location sensitivity”The contract bump only applies to dimensions where boundary position genuinely changes how bad a finding is:
| Dimension | Contract bump? | Why |
|---|---|---|
| correctness | yes | A bug at the contract leaks into every consumer; internal bugs stay contained |
| security | yes | Tainted input crossing a trust boundary is the canonical case |
| encapsulation | yes | The whole dimension is about boundary integrity |
| spec | yes | Spec drift at the API surface contradicts the published contract |
| complexity | no | Complexity grades by function/file shape, not boundary position |
| deslop | no | Dead code is dead code wherever it lives |
| assertions | no | Test quality doesn’t change by where the SUT lives |
| nih | yes | Reinventing primitives that cross the boundary is worse than internal helpers |
| efficiency | yes | Hot path on a public handler is the typical blocker shape |
| telemetry | yes | Silent failure on an outbound call (boundary) is the canonical blocker |
Fix-cost-now
Section titled “Fix-cost-now”“How hard would it be to fix this right now?”
Bucket the blast-radius file count for the proposed fix. Do not bucket in-head — pipe the raw file/module counts through shared/scripts/severity.py bucket:
python3 shared/scripts/severity.py bucket --files <N> [--modules <M>]# -> contained | moderate | sprawlingSource priority for the raw count:
tilth_deps— primary. Returns the file set that would need to change.- LSP
find-references/find-callers— fallback when tilth is unavailable.
Worked recipe. Given a finding at path:line, run tilth_deps on the containing file. Count the distinct files in the imported-by set as --files — use the <N> dependents header count, since the Used by list emits one entry per call site and several entries can map to one file (counting raw entries overcounts). Count the distinct slice/module roots among them as --modules — the directory immediately under src/ (so src/melt and src/affinage are two modules, not the shared src parent). Then run python3 shared/scripts/severity.py bucket --files <N> --modules <M>. Falling back to LSP callers, count the same way (distinct touched files, distinct module dirs) so buckets stay comparable across tools.
Fix-cost-now is reported, not bumped. Severity decides what to fix; fix-cost-now explains effort and lets triage schedule.
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 is the same fix in six months. |
spreading |
Cost grows linearly. New code piles onto the bad pattern; each new caller adds one unit of fix work. |
structural |
Cost grows non-linearly. Consumers harden against the current shape — types get re-exported, mocks calcify, downstream APIs build on the leak. Public-API leaks, DB-schema mistakes, and ingress-contract violations live here. |
Decision. structural if the changed symbol is re-exported by any consumer or fixing it requires touching a file outside the diff. spreading if the fix is local but the diff adds new callers of the bad shape, or the pattern is being copy-pasted. contained otherwise. When two tiers apply, take the higher.
Per-dimension structural anchors:
| Dimension | structural looks like |
|---|---|
| correctness | A race or lost write at a public API boundary; consumers harden retry/mock logic around the broken atomicity. |
| security | A taint path through a published signature; every consumer must re-validate once the contract leaks the unsafe shape. |
| encapsulation | A leaked internal type re-exported from a slice index; downstream slices build on it. |
| spec | A dropped requirement now baked into downstream behaviour that later code depends on. |
| complexity | A god module new code keeps landing in; each addition compounds the untangling cost. |
| deslop | A duplicated block forked across modules; each copy diverges and multiplies the eventual merge. |
| assertions | A mocked SUT or weak harness other tests copy as the pattern to follow. |
| nih | A reinvented primitive other modules import; replacing it later means migrating every caller. |
| efficiency | An unbounded structure on a long-running path; retained references accumulate as callers grow. |
| telemetry | A hand-rolled logging shape new code standardises on; migrating to the real logger later touches every call site. |
structural triggers the compounding +1 bump in the formula. The point of carrying this tag is to surface “fix now or pay exponentially later” to the user without dressing it up as severity.
Per-dimension rubrics
Section titled “Per-dimension rubrics”Each dimension’s base-severity table — severity-by-violation-shape, before modifiers. Modifiers (location, compounding) layer on top per the formula.
correctness
Section titled “correctness”Look for: off-by-one, ordering, null/empty edge cases, silent failures, races, contradictory branches, lost writes.
| Base | Trigger |
|---|---|
blocker |
Data loss, data corruption, race in shared concurrent state, lost write, irreversible side effect on wrong input |
high |
Wrong data returned, ordering bug, silent failure with no recovery path |
medium |
Edge case in a flow with a recovery path; null/empty handling that misbehaves on rare input |
low |
Cosmetic edge case in well-bounded leaf code |
Inherited shape: a race, lost write, or contradictory branch already present in the caller graph that the diff’s new path now exercises. Expand callers one level before grading clean.
Boundaries: telemetry (no-log failure to here), security (access-control to security), deslop (silent-failure claim to here), efficiency (TOCTOU wrong-data to here), spec (contract commitment to spec, runtime risk to here; emit both). Full rules in § Dimension boundaries.
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, crypto missteps.
| Base | Trigger |
|---|---|
blocker |
Injection (SQL/shell/template/deser), authn bypass, secret in source, RCE, plaintext secret on the wire |
high |
Unvalidated input reaches dangerous sink; broken authz on internal route; weak crypto on durable data |
medium |
Tainted input reaches limited surface with secondary validation; missing rate-limit on auth-adjacent route |
low |
Missing defense-in-depth on already-validated input |
Inherited shape: a tainted-input path or missing authz that predates the diff, where the change only adds a new caller of the unsafe sink. Trace the input to its boundary before grading clean.
Definitions: a dangerous sink is any call that executes, queries, renders, deserialises, or persists its argument (SQL/shell exec, template render, eval / pickle / yaml.load, file-path open, request to an internal service). Secondary validation is an independent check downstream of the sink’s entry that constrains the value (a schema parse, an allowlist, a parameterised query) so the tainted value cannot reach the sink unconstrained.
Boundaries: telemetry (secrets-in-logs to security), correctness (access-control to security), nih (reinvented crypto/sanitizer to security). 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 reach into internals, public APIs leaking implementation types, parameters that take more context than needed, new exports without a use case, the inverse — a domain invariant lifted out of the producer and enforced above it by every caller — and error, default, and configuration decisions the producer could absorb but exports to every caller instead.
| Base | Trigger |
|---|---|
blocker |
Ingress/egress contract violation — public API leaks ORM model, infra adapter, framework type, or storage internal across the slice boundary; slice’s index re-exports an internal type |
high |
Cross-module reach into another slice’s internals, bypassing crust/index |
high |
Caller-shadowed domain invariant — a guard/validation that all callers must invoke (or redundantly do) lives outside the producer; the domain doesn’t enforce its own invariant, so a caller can skip it. Especially when a symbol is made public solely to be called from above the domain layer. |
high |
Exported special case — an error, empty/boundary case, or configuration decision every caller must handle identically, where the producer has the information to absorb it (return an empty result instead of raising; apply the safe default instead of demanding one) |
medium |
Module-internal leak (cross-file inside one slice exposes private detail) |
low |
Class-level — one class touches another’s private member within the same file |
Detection signals for the caller-shadowed invariant: a guard defined inside a slice but never called by that slice (only by external entrypoints); N callers each repeating the same check before/after one producer; a public/exported guard whose only consumers sit above the domain layer; asymmetry where some callers apply the check and others skip it. Beware the false-clean trap — this often reads as good Sliced-Bread hygiene (a private helper promoted to public + crust-exported), so a diff-scoped pass grades it clean. The violation is usually inherited, not introduced by the diff under review.
Detection signals for the exported special case: N callers wrapping the same call in the same try/except; the same literal passed for the same parameter at every call site; each caller re-deriving the same default. The test is whether the producer has the information to decide — if callers legitimately differ, the parameter is doing real work. The limiting case is a configuration knob the caller cannot set correctly (a “voodoo constant” the module should compute itself); a knob expressing genuine caller-specific policy is not a finding.
Boundaries: deslop (misplaced-invariant dup to encapsulation), complexity (boundary-leaking param to encapsulation). Full rules in § Dimension boundaries.
Note: base tier is the location tier here, so the contract bump tends to redundantly raise an already-blocker finding (capped).
Recommendation shape: “Import from <slice>/index instead of <slice>/internal/foo” / “Narrow the public surface to <minimal-type>” / “Move the <invariant> into <producer> so every caller inherits it; drop the external guard and narrow the public surface” / “Return an empty <result> instead of raising on the boundary case” / “Absorb <default> into <producer> and drop the parameter” / “Compute <constant> inside the module instead of asking every caller for it”.
Look for: behaviour in the spec but not in the diff, behaviour in the diff but not in the spec, renamed concepts or relocated boundaries, missing acceptance criteria.
| Base | Trigger |
|---|---|
blocker |
Silent drift on a security/data/correctness requirement the spec explicitly nailed down |
high |
Behavior contradicts spec |
medium |
Acceptance criterion partially implemented |
low |
Naming/style drift the user can re-align in 30s |
Inherited shape: a requirement dropped in an earlier commit that the current diff neither restores nor violates outright. Compare against the spec, not only the diff.
Spec resolution: locate the spec before grading. Search order: the durable spec corpus via python3 shared/scripts/paths.py artifact_path specs <slug> (or mold.pyz artifact-path specs <slug>), falling back to the legacy literal .cheese/specs/<slug>.md only when the resolver is unavailable (see ../../cheese/references/formatting.md § Corpus location; never hardcode .cheese/specs/); then unresolved items in .cheese/press/<slug>.md, then the PR body or linked issue (gh pr view), then a commit-message ticket ref. If none resolves, record “no spec located; searched [list]” and grade spec findings don't know rather than clean.
Boundaries: correctness (contract commitment to spec, runtime risk to correctness; emit both). 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 params / 3 nesting), files over 300 lines that grew, speculative abstractions, redundant state, parameter sprawl, stringly-typed code, explanatory-renaming comments, and the inverse failure — abstractions whose interface costs more than they hide: pass-through methods, pass-through variables, adjacent layers restating the same abstraction, wrapper types that forward every call.
| Base | Trigger |
|---|---|
high |
God function (3× budget), param sprawl threading through 3+ layers (intermediate layers read or transform it), new god module created in this diff |
high |
Shallow layer — a new module, class, or layer whose public interface is nearly as large as the functionality it hides; callers must still know the internals to use it correctly |
medium |
2× budget, generic helper with one user, redundant cached state |
medium |
Pass-through method — forwards to another method with an unchanged signature and adds no functionality; or a pass-through variable threaded through 3+ layers to reach one consumer, with no intermediate layer reading it; or adjacent layers whose abstractions are the same |
low |
Few lines over budget, mildly speculative abstraction |
Detection signals for a shallow abstraction: a method body that is one delegating call with an unchanged or near-unchanged signature; a parameter that exists in a function only to be handed to the next call; two adjacent layers whose method names map 1:1; a class whose public method count approaches its count of non-delegating statements. Dispatchers are the deliberate exception — a method that routes to different implementations by type or key is doing real work, not passing through.
Inherited shape: a god function or param-sprawl the diff extends by a few lines rather than introduces. Grade the function as it now stands, not only the added lines.
Boundaries: encapsulation (boundary-leaking param to encapsulation, exported-decision param to encapsulation), deslop (pass-through and same-abstraction layers to complexity, fake-modularity file sprawl to deslop), efficiency (cache decision to complexity, runtime cost to efficiency). Full rules in § Dimension boundaries.
No default blocker row, but complexity still reaches blocker: a base high finding with fix-cost-later: structural takes the +1 compounding bump to blocker. “No blocker row” means no base blocker, not that complexity caps at high. (Once criticality returns, the floor may push complexity findings up on critical-tier paths.)
The budget is a smell trigger, not a target. Splitting a coherent function into shallow pieces to get under 40 lines is itself a complexity finding — grade the resulting call-depth and interface cost, not the line count. When a function is over budget but has no clean decomposition, grade it clean and record why; the budget rows fire only when a decomposition exists that would leave each piece independently understandable.
Recommendation shape: “Extract <sub-function>” / “Inline <one-call helper>” / “Derive <value> instead of caching” / “Replace <string> with <enum>” / “Replace <vague-name> with <concrete-name>” / “Inline <pass-through> into its caller” / “Collapse <layer-a> and <layer-b> — same abstraction twice” / “Pass <context-object> instead of threading <param> through 3 layers” / “Keep <function> whole — the split to meet budget fragments one abstraction”.
deslop
Section titled “deslop”Look for: dead code, AI tells (generic catches, useless docstrings, “// TODO: implement”, placeholder/apology comments like “// in a real implementation”), duplicated logic, copy-paste-over-reuse, vague or container-typed names (user_data_dictionary), convention blindness (reimplementing an existing repo utility from scratch), fake modularity (single-function utils file, God class spread thin), lint-suppression band-aids (# noqa / @ts-ignore / #[allow(...)] / //nolint) masking the real fix, phantom edge-case handling for inputs nobody can name, cargo-cult boilerplate, over-abstraction for one consumer, test bloat (shallow near-duplicate tests), partial shell strict mode (set -e without -uo pipefail).
Per-language pattern catalogs with 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 |
Large duplicated logic with diverging behavior; AI residue actively misshapes flow |
medium |
Dead branch left “for reference”, duplicated small block, “// TODO: implement” committed |
low |
Vague name; single weak copy-paste |
Inherited shape: duplicated logic or a dead branch the diff copies or leaves untouched beside its change. Read the surrounding block, not only the hunk.
Boundaries: correctness (AI-residue claim to deslop, silent-failure to correctness), nih (existing helper to nih), assertions (generic catch in tests to assertions), encapsulation (misplaced-invariant dup to encapsulation), complexity (fake-modularity file sprawl to deslop, pass-through and same-abstraction layers to complexity). Full rules in § Dimension boundaries.
No default blocker row.
Recommendation shape: “Delete dead branch at <existing-helper>” / “Extract shared <helper> from the two near-duplicate blocks” / “Rename data to <noun>” / “Remove <allow/noqa/ts-ignore> and fix the underlying <lint-rule>” / “Delete the placeholder comment at
assertions
Section titled “assertions”Look for: existence assertions instead of equality, catch-any-error, no-crash-as-success, mocked SUT, time/random/external coupling.
| Base | Trigger |
|---|---|
blocker |
SUT itself is mocked; test asserts the bug as correct behavior |
high |
Test passes when the implementation is wrong (no-crash-as-success) |
medium |
Catches generic Exception; depends on time/random without bounding |
low |
toBeDefined where equality is one line away (assert x is not None where assert x == <expected> is one line away) |
Inherited shape: a weak assertion in a touched-but-unmodified test that the diff’s behaviour change now leaves under-covering. Read the touched test bodies, not only the diff hunks.
Boundaries: deslop (generic catch in production to deslop, correctness, or telemetry per the claim), telemetry (asserting on log strings to telemetry). 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; in-project utility duplication.
| Base | Trigger |
|---|---|
high |
Reinvented logging/telemetry/concurrency primitives the project already wires; reinvented crypto |
medium |
Reinvented retry, debounce, validation, UUID |
low |
Reinvented small util the stdlib already has |
Inherited shape: an in-project helper or dependency already does this; the diff re-implements it because the existing one was not visible from the changed file. Check imports and the helper set before grading clean.
Boundaries: deslop (diff-internal dup to deslop), security (crypto/sanitizer to security), telemetry (custom logger to telemetry), efficiency (algorithm choice to efficiency). Full rules in § Dimension boundaries.
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, no-op updates, TOCTOU pre-checks, memory leaks, overly broad reads.
| Base | Trigger |
|---|---|
blocker |
Unbounded cache/queue, listener/timer leak, retained references after teardown — anything that grows without bound in a long-running process |
high |
Blocking work on per-request / startup / per-render path; N+1 on a high-traffic endpoint |
medium |
N+1 on a moderate endpoint; redundant compute in a non-hot loop |
low |
Redundant compute outside hot paths |
Inherited shape: an N+1, unbounded structure, or hot-path cost already present that the diff’s new call site now triggers. Check whether the changed path runs hot or long-running before grading clean.
Boundaries: nih (import exists to nih), correctness (TOCTOU wrong-data to correctness), complexity (cache decision to complexity). Full rules in § Dimension boundaries.
Recommendation shape: “Hoist <call> out of the loop” / “Run <a> and <b> in parallel with Promise.all (or equivalent)” / “Guard the store write on a value change” / “Drop the existence pre-check; handle the error from <op> instead” / “Bound <structure> or add cleanup on <teardown>” / “Read only the needed range/columns”.
telemetry
Section titled “telemetry”Covers logging, metrics, and tracing hygiene — both presence (is the path instrumented at all?) and shape (structure / levels / context / cardinality). Non-interactive paths need real telemetry (servers, daemons, workers, outbound calls); interactive paths where the operator watches stdout do not need backend-shipped telemetry on the happy path. Secrets-in-logs stays under security; hot-path log-volume cost stays under efficiency; exceptions swallowed with no handling at all stay under correctness.
| Base | Trigger |
|---|---|
blocker |
Silent failure on critical infra (payments, auth, irreversible side effects) where the operator has nothing to grep |
high |
Silent error branches on outbound calls to external services; un-instrumented new handler on a non-interactive path |
medium |
Silent catch on a non-critical worker; un-instrumented new background loop |
low |
Missing one structured field; wrong level on dev path |
Inherited shape: a silent catch or un-instrumented loop in a touched module that the diff extends rather than introduces. Check the surrounding handler, not only the changed branch.
Boundaries: correctness (no-handling silent failure to correctness), security (secrets-in-logs to security), nih (custom logger to telemetry primary), assertions (log-string asserts to telemetry). Full rules in § Dimension boundaries.
Look for: silent error branches on non-interactive paths; outbound calls without observability; silent daemons/workers/schedulers; missing request/response instrumentation; hand-rolled logging infrastructure; missing operational hygiene (rotation/retention) on new file logging; unstructured/string-concat log messages; wrong log levels; double-logging; errors logged without context; missing correlation/trace ids; high-cardinality metric labels or span names; logs-as-metrics; print()/console.log left in production; tests asserting on log strings; unbounded list/object dumps into logs.
Recommendation shape: “Emit a structured error log (and a failure counter) in this catch block before re-raising” / “Wrap the outbound <call> in a span and add a failure-counter metric” / “Add startup + per-iteration logs to the <worker> loop with the failing item id on error” / “Add entry/exit log + latency metric to the new <handler>” / “Use the project’s existing logger / standard <stdlib-or-ecosystem-library> instead of the hand-rolled <class>” / “Configure rotation (size + age cap, retention policy) on the new file handler” / “Read log path / level from project config instead of hardcoding” / “Replace string-concat log with structured fields” / “Demote to DEBUG (or drop)” / “Log once at the boundary, not at every catch” / “Add exc_info=True (or equivalent) to capture the stack” / “Thread trace_id through the log context at the request boundary” / “Move <high-cardinality-attr> from metric label to span attribute” / “Emit a counter instead of grepping logs” / “Replace print() with the project logger” / “Assert on behavior, not on log text”.
Dimension boundaries
Section titled “Dimension boundaries”When two dimensions could tag the same path:line, this table decides the primary. The per-dimension Boundaries: lines point here. The grader dedups by file:line when writing the report, keeping the higher-base finding and noting the secondary dimension.
| Pair | Tiebreaker |
|---|---|
| correctness / telemetry | Silent failure with no logging is correctness; telemetry owns it once the failure is caught and the gap is observability. |
| security / telemetry | Secrets in logs or URLs are security regardless of surrounding code. |
| security / correctness | A behavioural bug with an access-control consequence tags security; correctness only when there is no security consequence. |
| security / nih | Reinvented crypto or a security sanitizer tags security (higher base wins); leave nih off to avoid downgrading a blocker through nih’s missing blocker row. |
| deslop / correctness | Tag by the claim: deslop when the primary claim is AI residue, correctness when it is silent failure. |
| deslop / nih | nih when a pre-existing helper or import already does it; deslop when the duplication is internal to the diff with no existing helper. |
| deslop / assertions | Generic catches in test files are assertions; in production code they are deslop, correctness, or telemetry per the claim. |
| nih / telemetry | Custom loggers tag telemetry primary (richer rubric); note the nih angle in the recommendation; do not double-tag. |
| efficiency / nih | nih when an import or library exists for the primitive; efficiency when it is an algorithm or concurrency choice with no available import. |
| efficiency / correctness | TOCTOU as wasted work tags efficiency; TOCTOU that can produce wrong data under a race tags correctness. Split by failure mode. |
| encapsulation / deslop | Duplication caused by a misplaced invariant tags encapsulation (root cause is ownership), not deslop. |
| encapsulation / complexity | A parameter that leaks context or type across a boundary tags encapsulation; raw param count or threading with no boundary concern tags complexity. |
| complexity / encapsulation (exported special case) | Extends the encapsulation / complexity row above. When a threaded parameter also carries a decision the producer has the information to make (a voodoo constant), encapsulation wins — misplaced ownership is the root cause. Structural cost alone, with no misplaced decision, stays complexity. |
| complexity / deslop | Pass-through methods and same-abstraction layers tag complexity; a single-function utils file or one-consumer over-abstraction tags deslop. Both read as fake modularity — split by whether the harm is call-depth or file sprawl. |
| spec / correctness | Emit both with a cross-reference: spec records the broken contract commitment, correctness records the runtime risk. They are orthogonal. |
| assertions / telemetry | Tests asserting on log strings are telemetry-owned. |
| complexity / efficiency | Complexity owns the structural decision to cache; efficiency owns the runtime cost of redundant work. |
Deferred: criticality inference (v2)
Section titled “Deferred: criticality inference (v2)”A fourth severity contributor — a criticality floor keyed off the file’s path/import/structural fingerprint — is bookshelved for v1. When wired, the formula extends with:
sev = max(sev, criticality_floor(file)) # inserted before the capBookshelved material lives in:
.cheese/research/severity-rubric/rubric-draft.md§ Deferred: criticality inference — full inference ladder (critical / high / standard / low), four-tier vocabulary, two consumers (severity floor + weighted fix-cost-now),.cheese/criticality.tomloverride schema..cheese/research/critical-pathways/critical-pathways.md— 35+ detection rules across six signal classes (taint sources/sinks, compliance libraries, framework convention markers, production-pathway layout, graph-structural signals, empirical Pareto from Walkinshaw 2018 ESEM).
v1 does not mine the catalogs, build the override file, or compute the floor. v1 ships without any criticality awareness; the deferred material is read-only context for the v2 ticket.
Router call and lens fan-out mechanics
Section titled “Router call and lens fan-out mechanics”Read this before any n>1 dispatch from SKILL.md § Flow step 1 / § Sub-agent fan-out.
Router call
Section titled “Router call”Compute the review range’s review_surface score via the review-surface CLI (source: src/fanout/review_surface_cli.py, wrapping src/fanout/review_surface.py::score()). Run it through the .pyz bundle — the direct script imports cli/git_utils from shared/scripts/, which are only co-staged flat inside the bundle, so running it directly (python3 src/fanout/review_surface_cli.py ...) fails with ModuleNotFoundError: No module named 'cli': python3 ${CLAUDE_SKILL_DIR}/scripts/age.pyz review-surface --repo . <base>...HEAD — the range must be the diff under review (<base>...HEAD for an already-committed branch, the bare working diff otherwise); never rely on the CLI’s bare default, which scores the working tree against HEAD and silently zeroes an already-committed branch. Grep the diff’s added lines outside skills/** and .hallouminate/** for age_route.OVERRIDE_FLAGS tokens to populate risk_flags — scoped so a diff that merely documents the override vocabulary does not trip its own tokens; a token the grep misses means no promoted lens, not a missing security lens, so treat a hit as a hint, not a guarantee. Then call:
from src.fanout.age_route import routeroute(score=<float>, risk_flags=[...], entry="age")If the host only ships the bundle, echo '{"score": <float>, "risk_flags": [...], "entry": "age"}' | python3 ${CLAUDE_SKILL_DIR}/scripts/age.pyz age-route is the fallback (JSON on stdin, route JSON on stdout).
The returned n is the fan-out mode. The base ladder is n ∈ {1, 2, 5} from score alone (<60 → 1, 60–250 → 2, >250 → 5), with n=1 reviewed single-parent (no fan-out) via SKILL.md § Flow steps 2–4, unchanged; effort (low/medium/high) dials the reviewer dispatch; overrides_hit names any risk-override token matched (auth/secrets/crypto, tenant isolation, payments/ledgers, concurrency/idempotency/ordering/retries, schema/migration/protocol/public-API change, production-destructive ops, weak integration coverage — see age_route.OVERRIDE_FLAGS for the exact grep tokens). An override no longer forces the top tier: it promotes its mapped dimension out of whichever base-ladder group the score placed it in, into its own solo lens — the group’s remaining members stay grouped as one lens. n climbs by however many dimensions were promoted, uncapped, with a natural maximum of 9 when all four override categories hit simultaneously at the top score tier. effort is high whenever any override hits or score exceeds 900; low only at n=1 (unreachable with an override, since an override always forces high); medium otherwise.
Independently of the router’s n, /age may still fork a read-only review-context sub-agent — preferably the explorer phase-agent — purely for evidence-gathering (not fan-out) when caller/dependency graph expansion from tilth_deps or the selected semantic caller search crosses multiple subsystems, especially for --comprehensive reviews. This is a separate, orthogonal dispatch from the n-way fan-out below.
For the digest contract, harness-agnostic selection rules, and what the parent never delegates, see sub-agent-gate.md.
Lens fan-out mode (n>1)
Section titled “Lens fan-out mode (n>1)”Activates on the router predicate above (n>1 and /age not itself a sub-agent). One worker per lens in the router’s returned lenses list — not per single dimension — dispatch exactly len(lenses) workers. The base ladder’s lens partitions at n>1, before any override promotion:
n=2—[correctness, spec, assertions, security, telemetry]/[encapsulation, complexity, deslop, nih, efficiency].n=5— the five cohesion-grouped lenses:[correctness, spec, assertions];[security, telemetry];[encapsulation, complexity];[deslop, nih];[efficiency].
An override promotion (mechanics above) pulls its mapped dimension out of whichever group the base tier placed it in and gives it its own solo lens; the group’s remaining members survive as one lens together. Every grouping above is chosen for thematic cohesion — encapsulation never shares a lens with efficiency or telemetry at n=5.
The seam sequence below is identical for every n>1 — only worker count and each worker’s assigned dimension set vary with n:
Seam 1 — Predicate. As defined at the section opener above.
Seam 2 — Shared context packet. The orchestrator assembles the packet once, writes it to .cheese/age/<slug>-packet.md, and each worker reads it. Eight components, and the reuse of the review-context digester as the orientation block, are documented in packet.md.
Seam 3 — Worker contract. One worker per lens. Resolve the reviewer role through ../../cheese/references/agent-resolution.md at the router’s effort dial; require read-only permissions and fresh context, a prompt-constrained general fallback allowed only with degraded: true. Each worker:
- Reviews every dimension in its assigned lens (a solo-lens worker reviews just that one dimension; a multi-dimension lens worker reviews all dimensions in its group, e.g. the
[correctness, spec, assertions]worker reviews all three). - Computes full per-finding severity for every dimension in its lens (base + location bump + compounding bump).
- Tags each finding with its dimension and an
also-relevant-to: [<dim>, ...]field when cross-dimension overlap is suspected (including overlap with a dimension owned by a different lens). - Reports every defect it notices, however minor — no severity-conservative self-filtering; the verifier pass (Seam 6) and orchestrator reconciliation (Seam 4) do the filtering.
- Returns full per-finding rows in the
SKILL.md § Outputfinding format (**[dim:sev]** path:line — claim+location / fix-cost-now / fix-cost-later / confidence+recommendation). Not an orientation digest — the§ Digest contractsize ceiling does not apply. - Does not dedup, apply boundary tiebreakers, reconcile severity across dimensions or lenses, or write the report.
After all workers return, continue at Seam 4 (reconciliation) below.
Seam 4 — Orchestrator reconciliation. After all workers return, apply the ## Dimension boundaries table (dimensions.md § Dimension boundaries) verbatim to any line meeting EITHER condition: (1) flagged by two or more workers at the same file:line; (2) tagged also-relevant-to: [d] by any worker — the orchestrator re-evaluates dimension d against that line and applies the tiebreaker (keep the higher-base finding / suppress / emit-both-with-cross-reference per the 15 rules). This consumes the also-relevant-to signal and provides the cross-dimension coverage single-parent gets for free. Lines neither flagged by ≥2 workers nor tagged also-relevant-to need no reconciliation. Group by severity. The parent owns the canonical artifact. After reconciliation, continue at Seam 6 (verifier pass), then step 5 (write + print the report path) and SKILL.md § Handoff exactly as the single-parent path does.
Seam 5 — Shared impact evidence. The packet carries the caller/dependency notes assembled through tilth_deps and the selected semantic caller search. Workers use that packet instead of rebuilding impact context independently.
Seam 6 — Verifier pass. After Seam 4 reconciliation produces the candidate findings list, a cheap verifier role (haiku/tiny tier, effort: low per the Roles x tiers table) checks each reconciled finding against the evidence slice cited in its recommendation/location fields — one verifier call per finding, schema-constrained to “verify exactly one claim.” Three outcomes:
- Confirm — the cited evidence supports the claimed severity; the finding ships unchanged.
- Downgrade/drop — the evidence does not support the claimed severity (or the claim itself); the verifier lowers the severity tier or drops the finding, and the orchestrator records the original claim and the verifier’s reasoning in the report’s confidence trail.
- Escalate — the evidence given cannot settle the claim either way (cross-cutting contract 1: “a claim no evidence can settle returns
escalate, never a guessed pass or fail”). The finding is kept at its original severity with anescalateflag — never silently dropped or silently passed through unflagged.
This is the “cheap severity-filter leg” referenced in the Roles x tiers table; it applies whenever n>1. It does not run at n=1 — the single-parent path has no reconciliation step to filter, and the reviewer’s own severity computation is the only grading pass.
Output shape invariant. The findings report (.cheese/age/<slug>.md) has the same dedup, severity grouping, and finding format in the single-parent path and every lens fan-out width. Resolution provenance may expose the selected role and topology.
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 shared/scripts/findings_cli.py render-table --report .cheese/age/<slug>.mdIf the host only ships the bundle,
python3 ${CLAUDE_SKILL_DIR}/scripts/common.pyz findings_cli render-table --report .cheese/age/<slug>.mdis the fallback. Mark any sprawling/structural-fix row as heavy. -
Ask which findings to cure. Lead each option with the verb (what the user wants to do next); the underlying selection verb is the backing detail. Lead with the recommended composite, then present the same four severity-floor options below it, in the same most-inclusive-to-least order, so the gate is predictable across every run:
- Fix mediums-and-above plus cheap lows (recommended) — equivalent to
all-medium, cheap(the composite floor defined at Compute the recommended set underSKILL.md § Handoff). The cheap lows are the small valid nits that are cheaper to fix than to defer; sprawling/structural lows are left out. - Fix everything — equivalent to
all(every finding regardless of severity). - Fix medium-severity and above — equivalent to
all-medium(the medium severity-floor from Compute the recommended set, without the cheap-lows union; addcheapto also union the contained-fix lows, i.e. the recommended composite above). - Fix high-severity and blockers — equivalent to
all-high(floor at high, includes blockers). - Fix blockers only (strict; land only the must-fix blockers and defer the rest to a follow-up) — equivalent to
all-blocker.
Then 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 shared/scripts/findings_cli.py parse-selection --report .cheese/age/<slug>.md --selection "<verb>"If the host only ships the bundle,
python3 ${CLAUDE_SKILL_DIR}/scripts/common.pyz findings_cli parse-selection ...is the fallback. -
Plate it — apply the recommended composite via
/cure <slug> --auto --open-pr --stake medium+; terminal/plateresolves topology and publishes. Carry--hard. -
Checkpoint & stop —
/wheypoint: write a resumable handoff and pause instead of curing now. -
Stop — leave the report for later — equivalent to
none.
Present all four severity options on every run even when a severity band is empty (e.g. no blockers): a floor that resolves to an empty set is a valid, predictable no-op — do not drop or reorder options based on which bands happen to be populated. If the user selects a floor (or the recommended composite) that resolves to an empty set, treat the selection as
none: report that no findings match and do not dispatch/curewith emptyresolved_ids(the non-empty-selection contract in Dispatch still holds). - Fix mediums-and-above plus cheap lows (recommended) — equivalent to
Dispatch
Section titled “Dispatch”On a non-empty selection — whether auto-selected by default or chosen at the gate — immediately dispatch /cure <slug> [--safe] [--open-pr] [--hard] with the selection locked in via context, not a CLI flag:
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, re-confirms the cited ids still exist, then owns the apply / validate / push loop. Always emit resolved_ids alongside selection — expand the verb yourself rather than leaving the field empty; /cure re-confirms against the report regardless. Propagate --safe, --open-pr, and --hard to /cure when they are in scope.
On none / Stop (only reachable via the gate), exit cleanly with the report path.
--auto substitutes a severity-floor selection and its own chain — see SKILL.md § Auto mode.
Within cook’s own fan pathway
Section titled “Within cook’s own fan pathway”/cook’s fan pathway (its retired-/ultracook mechanics, now self-hosted — see ../../cook/SKILL.md § Fan pathway) spawns age as a fresh-context sub-agent and owns the chain itself. Honour the no-chain isolation directive:
- Write
.cheese/age/<slug>.md(with the handoff slug at the top) and stop. Do not invoke/cure <slug> --auto --stake medium+from inside the sub-agent. - Set
next:from what you observe on this run, not from any guess about chain position.next: curewhen at least one finding meets the medium+ floor;next: donewhen none do. - The two-cure-pass cap is enforced by the fan pathway’s fixed chain length, not by age counting passes. The terminal age is publishable only with
next: done;next: cureor a missingnexthalts without publishing. Parallel curds and post-merge review dispatch age as a top-level fresh-context reviewer, never as nested inline self-review.
Shared context packet
Section titled “Shared context packet”The orchestrator assembles this packet once per fan-out run and writes it to .cheese/age/<slug>-packet.md. Each lens worker reads the full packet — a worker owns a lenses[i] entry, which may bundle up to three dimensions. There is no persistent cross-run cache — every run rebuilds (staleness risk + YAGNI).
Components (eight, in assembly order)
Section titled “Components (eight, in assembly order)”- Located spec — path + content, resolved via the spec-resolution order in
SKILL.md § Inputs. - Dependency manifest contents —
package.json,Cargo.toml,pyproject.toml, or equivalent for the project under review. - Project-helper index — one
tilth_searchforsanitize / validate / escape / safe / retry / debounce / loggeracrosssrc/andshared/, so workers can flag NIH or missing helper usage. - Path-context map — which entrypoints are non-interactive or hot (servers, daemons, CLI handlers, outbound callers); workers need this for location classification and telemetry coverage.
- Per-lens rubric slice + shared formula sections — the worker’s assigned lens’s rubric slice, the union of the dimension rubrics for every dimension in that
lenses[i]list, fromdimensions.md, plus§ Location sensitivity,§ Fix-cost-now, and§ Fix-cost-laterso each worker can compute severity independently. Extract each dimension’s## <Dimension name>heading to the next##heading and concatenate. - The severity machinery — the full
§ Severity computationsection. - Output contract — the per-finding fields table and finding format from
SKILL.md § Output, plus thealso-relevant-to: [<dim>, ...]field from Seam 3 — it is the cross-dimension signal Seam 4 reconciliation consumes and is not part of§ Output’s base format, so a worker following§ Outputalone would omit it. Workers emit full per-finding rows (format fields +also-relevant-to) the orchestrator can parse and reconcile unambiguously. - Dedup-ownership statement — explicit: workers do NOT dedup, apply boundary tiebreakers, reconcile severity across dimensions or lenses, or write the report. The orchestrator owns those steps (Seam 4), covering reconciliation across lenses as well as dimensions.
Orientation and citations block
Section titled “Orientation and citations block”The existing review-context sub-agent (SKILL.md § Sub-agent context gate) is reused as the packet’s orientation + citations block. Its digest is embedded in the packet — not duplicated or re-generated.
Transient file contract
Section titled “Transient file contract”- Written to
.cheese/age/<slug>-packet.mdat the start of each fan-out run. - Read-only for workers; never written or modified by workers.
- Not persisted across runs — no cross-run cache. Leave to normal
.cheese/cleanup.
Worked report example
Section titled “Worked report example”Read this alongside SKILL.md § Output for a concrete rendering of the report skeleton.
## 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.
## 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`.
## 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`.
## Confidence<`certain` | `speculating` | `don't know`> — <one-line justification including which evidence sources were unavailable>
## Next step<when press was skipped, lead with>: Hardening was skipped for this diff — run `/press <slug>` before curing, or continue reviewing as-is.Auto-fixing the recommended set via `/cure` (or the selection prompt on a reason to ask / `--safe`).Full skeleton (with placeholders)
Section titled “Full skeleton (with placeholders)”The worked instantiation above renders only the severity sections. This is the complete report shape, handoff slug through ## Next step, with every placeholder in context:
status: ok | halt: <one-line reason>next: cure | doneartifact: <path-to-press-report-or-prior-cure-if-any>durable_flags: none | <one line per flag: what durable knowledge changed -> target wiki page>baseline: none | <recorded baseline block copied from the upstream handoff — see ../cook/references/quality-gates.md><one-line orientation: what the diff does>
press: skipped
<!-- `press: skipped` is the first body line after the blank separator. Omit it entirely when a press report exists for this slug or no cook artifact does. -->
# Age Report — <slug>## Orientation<one or two factual sentences about what the diff does>## Press findings<omit this section when `.cheese/press/<slug>.md` does not exist. When it does, summarise unresolved press items in one or two bullets so `/cure` (which never reads the press report directly) sees them. When it does not exist but `.cheese/cook/<slug>.md` does, omit this section but add `press: skipped` as the first body line after the handoff slug (see above) instead.>
## Wiki context<omit this section when hallouminate is absent or grounding returned no hits. When wiki pages informed the review context, list one bullet per consulted page — `<wiki page path>:<line>` — <one-line why it informed the review> — so the user can see, and challenge, what grounded the review.>
## 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.
## 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`.
## 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`.
## Confidence<`certain` | `speculating` | `don't know`> — <one-line justification including which evidence sources were unavailable>
## Next step<when press was skipped, lead with>: Hardening was skipped for this diff — run `/press <slug>` before curing, or continue reviewing as-is.Auto-fixing the recommended set via `/cure` (or the selection prompt on a reason to ask / `--safe`).Sub-agent context gate (shared kernel)
Section titled “Sub-agent context gate (shared kernel)”Cross-skill rules for forking work to a sub-agent. Adopted from Anthropic’s effective-context-engineering guidance, Tavily’s published skills, and the cross-harness norms used by LangChain DeepAgents and the Skills standard.
Each skill names its own triggers. This file is the single source of truth for the things every skill agrees on.
Digest contract
Section titled “Digest contract”The sub-agent returns roughly 2 KB or less: structured summary, citations, gaps. No raw bodies, no full file dumps, no copy-paste of fetched content. Skills name what the digest contains — claim table, orientation paragraph, root-cause summary, etc. — but never relax the size ceiling.
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 only governs digest boundaries.
What the parent never delegates
Section titled “What the parent never delegates”- Severity grading, final verdicts, approval gates. Exception (scope-limited): a skill may delegate single-dimension grading to a per-dimension worker iff the parent retains final cross-dimension reconciliation and the verdict — the verdict and the cross-cutting grade stay central. This exception does not loosen the default for any other case.
- Dialogue, contradictions, handshakes, user-facing decisions.
- Writing the canonical artifact (report, spec, claim table) — the sub-agent supplies the digest; the parent writes the doc.
What the sub-agent owns
Section titled “What the sub-agent owns”- Bulk fetches, extracts, crawls, multi-source research.
- Many-file reads, dependency / caller graph traversals. For code navigation, start with
kind:symbolto find the definition, thenkind:callersfor call sites. Fall tocontent/regexonly when you don’t have a symbol name. - Anything yielding mostly raw bodies that the parent will not read line by line — about 5 K tokens of raw output is the industry rule of thumb for “fork it”.
Parallelism
Section titled “Parallelism”When two or more heavy units of work are independent, spawn one small sub-agent per unit in parallel and merge their digests in the parent. One sub-agent doing five things sequentially is the wrong shape.
Age router as fan-out predicate
Section titled “Age router as fan-out predicate”/age (and /affinage through it) no longer use a size-only threshold to decide fan-out — skills/age/SKILL.md § Sub-agent context gate calls src/fanout/age_route.py’s route(score=...) and forks per its n (1 / 2 / 5). This file’s digest contract, selection rules, and delegation boundaries still apply unchanged to every worker the router spawns.
Shared output discipline, reasoning posture, and depth-vs-question scoping. Skills cross-reference this file rather than restate it; when a skill omits a rule, treat the omission as opt-out.
Output discipline
Section titled “Output discipline”- Lead with the answer in written reports — the first line of a
.cheese/*artifact, written summary, or end-of-task wrap-up is the result, not the lead-up. Skip preamble (“Let me look at…”), restatement (“So you want…”), and trailing sign-offs (“Hope this helps”, “Let me know if…”). Brief conversational scaffolding earns its place in interactive dialogue when the user is exploring or aligning — the rule targets reports, not natural turn-taking. - Match shape to content. Headers and bullets are for content that is genuinely list-shaped. A two-sentence answer stays as two sentences.
- In
.cheese/*artifacts, write prose-first Markdown — Markdown headers, bullets, and tables are fine when content is list-shaped, but skip JSON/robotic schemas and ceremonial layout. US spelling, Oxford commas. Skip AI cadence — repeated em-dashed asides as decoration, “consider edge cases” filler, “robust and scalable” boilerplate, “great question” or “you’re absolutely right” openers.
Reasoning posture
Section titled “Reasoning posture”- Correct false premises before engaging. If a request rests on a wrong assumption, name the assumption and answer the better question instead of working the wrong angle.
- 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 in front of you (file content, command output, primary doc, test result).speculating— inferred from 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. Applies to design choices, library picks, and review recommendations.
- Track contradictions across the dialogue. If turn N contradicts turn N-3, flag it and resolve before moving on. The model is responsible for noticing — the user should not have to be the consistency check.
- Agree when agreement is warranted. Do not manufacture counterpoints to seem balanced. A spec the user already got right does not need re-litigation.
- Prefer satisfying a valid critique over arguing it. When a review comment or self-review nit is correct and the fix is cheap — a contained change, roughly a few lines or a localized refactor — make the change rather than draft a defense for leaving it. Push back only when the critique is wrong (the code is already correct, or the claim is ungrounded) or when satisfying it costs far more than it returns (a sprawling or structural change beyond the current scope). A justified push-back usually costs more than a small valid fix.
- Name the exact step that breaks when reasoning is invalid — not “this seems off”, but “the X assumption fails when Y because Z”.
Depth and questions
Section titled “Depth and questions”These scope to different axes — which decisions to ask about, how to phrase a question, and how much to contribute — not one dial to trade off.
- What you ask about — the decisions that are the user’s to make. Consequential or preference forks (scope, naming, trade-offs with no single right answer) are the user’s call; ask them rather than deciding silently and presenting the result as settled. On an owned decision, asking is the primary move, not a last resort when you’re stuck.
- How you phrase a question — one clear thing at a time. Preserve working memory and surface the real ambiguity instead of burying it in a multi-part barrage. This governs phrasing, never whether to ask.
- What you contribute — maximum useful depth. Full pseudocode signatures over hand-waving, named edge cases over “consider edge cases”, concrete file:line evidence over vague pointers, the actual rejected-option case over “there are trade-offs”. When the model is the one talking, lean toward more, not less.
The failure mode to watch: treating a low question count as a virtue. Tight phrasing is the goal, not few questions — don’t skip a decision that’s the user’s because you’d rather contribute than ask. And don’t ask a thin question as a substitute for thinking: if you have nothing substantive to add yet, add it first.
Out of scope
Section titled “Out of scope”- Punctuation aesthetics (em dashes, emojis). The repo’s tone allows them in skill prose; voice rules govern reasoning, not typography.
- Audience-shaping (“write for an executive”). Skills serve the user in front of them, not a generic audience.
- A ban on Markdown structure in
.cheese/*artifacts. Headers, bullets, and tables are fine when content is genuinely list-shaped; the rule targets JSON-schema-style layout and AI cadence, not Markdown itself.