Codebase-specific smell catalog with calibrated grep recipes: duck-typing at boundaries, yargs argv re-assertions, value-discarding promise tails, console.* in library code, exhaustive `never` over-broad unions, single-method `Pick` deps, `*Like` boundary shape copies. Use for a periodic audit pass, scoping a cleanup PR, after a major refactor, or reviewing a primitive's consumers. Pairs with refactoring (fix mechanics) and post-implementation-review (second-read hub).
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Recurring smell categories worth hunting periodically. Each category has a grep pattern, a real example from the repo (if one exists), and a "why it matters" so you know whether a hit is a real smell or expected.
Related skills: See
post-implementation-reviewfor the full second-read ritual after implementation. Seerefactoringfor the methodology of fixing what you find. Seeone-sentence-testfor the cohesion audit that frames whether an abstraction earns its keep.
The categories below were validated against the actual codebase by repeated agent audits. They're not generic style nits: each one signals a specific kind of contract problem in a TypeScript-heavy framework codebase.
Pattern: (x as { foo?: unknown }).foo or as Record<string, unknown> accesses where the shape isn't statically known.
grep -rn "as\s*{" packages/cli packages/workspace --include="*.ts" -B2 -A2Why it matters: Each duck-type is a contract gap. The receiver doesn't know what it's getting; the producer doesn't know what's expected. Often justified at literal system boundaries (e.g., parsing user-provided configs, reading from third-party APIs), but unjustified inside the framework's own code.
Example (justified): packages/cli/src/util/handle-attachments.ts:41-45: duck-types sync and awareness because the CLI bundles are user-defined and arbitrary. The 30-line comment block above the helper documents exactly why.
Example (unjustified): if (entry.handle.whenReady) was duck-typing whenReady until it became a typed optional on Document. The diagnostic told us the contract was incomplete; the fix was to declare the field.
How to triage a hit: read the ~30 lines above the duck-type. If there's a comment explaining why static typing isn't possible at this boundary, leave it. If there's no explanation and the producer is an internal type, either widen the producer's type to include the field, or consolidate the access into a duck-typing helper that's itself well-documented.
argv as Record<string, unknown> Re-AssertionsPattern: yargs argv re-asserted to a broad shape at every command handler.
grep -rn "argv\s*as\s*Record<string, unknown>" packages/cli --include="*.ts"Why it matters: yargs types argv as loosely as it can. Every CLI command handler does const args = argv as Record<string, unknown> and then keys into it three or four times. Each access is a type-system escape: argv.foo as string, argv.bar as number. The type system isn't catching missing flags or typos at the source.
Why it persists: yargs's typed builder API is verbose and authors trade off ergonomics for safety. The trade is real, but worth periodically auditing: especially when adding a new flag, check whether the access pattern crosses the threshold where a typed argv extraction helper would pay off.
Triage: a single command with two flag accesses isn't worth a typed wrapper. Five flags or three commands sharing the same accessor pattern is. Look for repeated key access to the same field across command files; that's the signal to extract.
.then(() => {}))Pattern: explicit value-discarding tails on promise chains.
grep -rn "\.then\s*\(\s*(\(\)|=> \{\}|=> undefined)" packages --include="*.ts"Why it matters: usually indicates the type at the receiving end is too narrow. Authors write .then(() => {}) to convert Promise<T> into Promise<void> because the field accepting the promise is typed Promise<void> instead of Promise<unknown>. The ceremony adds zero semantic value.
Recipe: when you see this pattern, look at where the promise is being assigned. If the consumer awaits-and-discards (typical for readiness/teardown barriers), widen the field type to Promise<unknown>. The tail disappears.
Example fix: this codebase's readiness fields such as whenReady, whenLoaded, and whenConnected were widened from Promise<void> to Promise<unknown> exactly to eliminate this ceremony. See spec specs/20260424T000000-self-gating-attachments.md for the rationale.
False positive: if the .then(() => result) returns a meaningful transformed value, leave it. The smell is specifically the "discard the value" form.
Pattern: console.log / console.error / console.warn outside of CLIs, tests, and benchmarks.
grep -rn "console\.(log|error|warn)" packages/workspace packages/sync --include="*.ts" | grep -v testWhy it matters: library code that calls console directly can't be redirected, silenced, or piped to a custom sink (in-memory test introspection, telemetry shipping). Every console.log is a hardcoded output decision the consumer can't override.
The framework's logger: wellcrafted/logger provides createLogger(name) plus pluggable sinks (consoleSink, memorySink, composeSinks). Three established injection patterns:
// Module-scoped (default)
const log = createLogger('module-name');
// Optional parameter (for libraries that may want a caller-supplied logger)
function createX({ log = createLogger('x') }: { log?: Logger } = {}) { ... }
// Config-injected (used in openCollaboration)
const log = config.log ?? createLogger('collaboration');Triage: this category is verified periodically: the codebase has historically been clean. Treat any hit as a regression and route through wellcrafted/logger before merging. New console.* in library code should be refused at PR review unless the call site is explicitly a CLI command, test, or benchmark.
never Checks as Union-Churn SignalsPattern: const _exhaustive: never = x after a switch on a discriminated union.
grep -rn ": never\s*=" packages --include="*.ts" -B10Why it matters: each never check is a contract promise: "if this union grows a variant, every consumer with this check will break." That's good: it's the type system catching missed cases. But if adding a single variant breaks five different switches, the variants are probably overlapping, the union is too broad, or the same logic is being implemented in too many places.
Example (justified): packages/cli/src/util/emit-peer-errors.ts:89 exhaustively switches over RpcError variants. RpcError has well-bounded variants (well under five), the switch is the canonical formatter, and other code routes through it rather than re-implementing. Single point of enforcement, healthy use of the pattern.
Triage: count call sites on the discriminated type. If 1-2 switches enforce exhaustiveness, fine. 5+ is a smell: consider:
False positive: type-narrowing on a result type at a boundary is healthy, even if there are several. Look for the pattern of "every consumer has to know about every variant" as the actual signal.
Pick DependenciesPattern: dependency injection shaped as Pick<Thing, 'method'>.
rg "Pick<[^>]+,\s*['\"][^'\"|]+['\"]\s*>" packages appsWhy it matters: a single-method pick can keep an old object boundary alive after the caller only needs one operation. The type looks narrow, but the object name still tells readers to think about the whole capability family.
Example fix: packages/workspace/src/daemon/unix-socket.ts used Pick<Hono, 'fetch'> for the socket binder. The binder's job is "bind one request handler to a unix socket and harden the socket file", so the dependency became a UnixSocketRequestHandler function instead of a Hono-shaped object.
Triage: write the one-sentence job of the caller and mentally inline the picked method. Keep the object only when that sentence names the object or the caller coordinates the object's life cycle. If the sentence only names one verb, accept a named capability function in the caller's language and update tests to fake that function. Do not replace Pick<Thing, 'method'> with Thing['method'] unless Thing is still the caller's real concept.
False positive: Pick is fine for data projection, DTO trimming, and multi-field view models. A single non-method field like Pick<Session['session'], 'expiresAt'> is not this smell.
Pattern: local helper types that copy upstream shapes instead of deriving from the owner.
rg "type\s+\w+Like\b|interface\s+\w+Like\b" packages apps
rg "as\s+\w+Like\b" packages apps
rg "as\s+Record<string, unknown>" packages apps
rg "Pick<[^>]+,\s*['\"][^'\"|]+['\"]\s*>" packages apps
rg "Parameters<typeof\s+[^>]+>\[[0-9]+\]" packages appsWhy it matters: copied shapes create two sources of truth. A Like type,
single-method Pick, or Parameters<typeof fn>[n] contortion can look precise,
but often says "this layer knows an upstream object exists and only wants one
piece of it." That is a boundary leak. Either derive from the owning runtime
type, schema, factory, or function signature, or name the one capability the
caller actually needs.
Triage: read nearby context before judging. Classify each hit as:
*.test.ts or setup helpersRecipe: clear candidates usually collapse one of four ways: derive the type
from the owner, replace object-shaped dependency injection with a named
capability function, move incomplete fake objects into tests with satisfies,
or delete one-property option aliases that only rename the function signature.
False positive: Parameters<typeof fn>[n] is fine when it derives a public
helper type from a stable exported function. It becomes a smell when tests use it
to reverse-engineer an unnamed seam, or when the index gymnastics hide the fact
that the caller wants a smaller named capability.
if/else Where a switch Belongs)Pattern: an if (x.type === '...') / else chain that folds an entire closed discriminated union (every branch maps a variant to a different output) with no satisfies never pin. The inverse of category 5.
grep -rn "\.error\.name === '" packages apps --include="*.ts" | grep -v testWhy it matters: the else silently absorbs any variant added later; a switch with default: x satisfies never makes the producer's new variant break the consumer's build instead.
Validated: a sweep of workspace/sync/cli/api found four (run-handler.ts, run.ts, list.ts, materializer.ts), all fixed. Re-run after adding variants to a wire or IPC error union.
The pattern itself lives elsewhere: the rule, the predicate/guard/fold triage, and the before/after are in the define-errors skill ("Consuming a Variant Union"). This entry is only the detection recipe: use it to find hits, use that skill to classify them.
Tested and rejected as not-actually-smells in this codebase:
kind: branches: exhaustiveness via never enforces this aggressively. No dead branches survive.packages/workspace; spot-checks always hit consumers. Would need deeper dependency analysis to find real dead ones.*-manager.ts, *-handler.ts): none found. Naming matches responsibility.@ts-expect-error / @ts-ignore: ~234 of them, but all justified (test doubles, duck-typing at system boundaries with comments).If a future audit finds patterns consistent with these categories, they should be added here. If a category proves false (zero hits across multiple sweeps), demote it.
When kicking off a review pass:
The goal is systematic detection, not ad-hoc cleanup. These patterns repeat; codifying them here means future review passes don't re-derive them.
cb12bcc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.