pm4ai

Lintmax

Lintmax lint/format orchestrator conventions

lintmax = biome + oxlint + eslint + prettier + sort-package-json in one command; we own it.

Every lintmax version runs configless by default. lintmax (TypeScript) is the sole exception — its max-strict default is dense enough that a project needs lintmax.config.ts to opt a rule out, so it supports one; every other version (lintmax-go, lintmax-rs, …) stays config-free. A project config opts a rule out only with documented false-positive evidence, never to dodge a fix.

MUST

  • Run only bun run fix for code maintenance. Why: it fixes then verifies internally (all 5 linters twice); a clean run prints ok on a single line + exit 0 — ok IS the success signal, not silence.
  • Read failure output directly. Why: already grouped file→linter→rule, compressed line numbers, deduped across 5 linters.
  • Make ALL edits first, then run fix foreground to completion. Why: editing during a backgrounded fix races it — the formatter writes its pre-edit buffer back and silently reverts your change.
  • WHEN a fix is running, wait until pgrep -f 'lintmax|bun.*fix' is clear before editing. Why: same revert race.
  • Commit a checkpoint before any multi-file mutator (fix after stripping directives, audit/codemod, sed-all/rename-all). Why: fix mixes autofixes with your edits; git reset --hard then restores in one command.
  • Batch many edits, run fix once at the end. Why: fix is slow per run.
  • File code-lint gaps upstream against lintmax. Why: it is the only lint tool — domain-specific hand-rolled tools/*.ts checks (banned vocab, spec-vs-code diff) are fine; code-lint is not.

NEVER

  • Run bun run check / lintmax check for maintenance — check is CI-only; fix is the agent-side maintenance command. Cost: redundant after fix, wastes 2+ min re-running 5 linters.
  • | tail / | head on any lintmax command. Cost: empty output IS success; failure output is already agent-formatted — truncation hides violations.
  • lintmax check --human to “see violations”. Cost: run bun run fix and read its failure output.
  • Add a second code-lint tool — extra eslint plugins, stylelint, knip, depcheck, dependency-cruiser, size-limit. Cost: fragments lintmax’s curated surface, drifts.
  • Use the void operator. Cost: fix auto-deletes it (no-void) — void promise() → bare expr → noUnusedExpressions; () => { void mutate() }() => { undefined }, dropping the call.

void replacements

  • Unused promise: promise.catch(() => {}) or try { await ... } catch {}.
  • Async in a () => void slot (onClick): () => { mutate().catch(console.error) }, or widen the prop type to () => void | Promise<void>.
  • Async inside a useEffect body (slot type can’t be widened): wrap in an IIFE ;(async () => { ... })() or .catch(noop).
  • Unused var: rename _x or remove it.

Ignore syntax

LinterFile-levelPer-line
oxlint/* oxlint-disable rule */// oxlint-disable-next-line rule
eslint/* eslint-disable rule */// eslint-disable-next-line rule
biome/** biome-ignore-all lint/cat/rule: reason *//** biome-ignore lint/cat/rule: reason */

Ignore strategy

  • Fix every legit, fixable finding by fixing the code, never the rule; ignore is last resort. Why: a found-and-fixable finding disabled is the severity/effort loophole that lets a real defect rot — the rule stays, the code changes. The only legitimate disable is a documented false-positive-rate, logged as an exception.
  • File-level disable WHEN a file has many unavoidable same-rule violations (sequential DB mutations, standard React patterns, external images); per-line for an isolated one. Why: scale-appropriate.
  • File-level directive at absolute file top, above imports/code (incl 'use client'/'use node'); per-line on the line ABOVE the code. Why: per-line inline trips no-inline-comments.
  • WHEN 2+ linters flag one line, file-level for one + per-line for the other. Why: stacking multiple per-line above one line is banned.
  • One top eslint-disable per file, multiple rules comma-joined; keep one canonical block, remove duplicates. Why: dedupe.
  • WHEN a file-level biome-ignore-all exists, drop the redundant per-line biome-ignore for that same rule. Why: file-level already covers every line.
  • NEVER 5+ per-line ignores for one rule. Cost: use file-level instead.
  • Don’t hand-remove dead directives or add one “just in case”. Why: fix auto-removes UNUSED file-level oxlint-disable / biome-ignore-all (both /** and // forms) by strip-relint-in-place; if a rule doesn’t fire, fix drops it and check fails on it.

Cross-linter

  • Same rule in 2 linters (biome noAwaitInLoops + oxlint no-await-in-loop) = double enforcement, not conflict — never disable one. Why: both must pass.
  • Suppress a shared eslint/oxlint rule on eslint’s side. Why: oxlint auto-picks up eslint rules and is faster.
  • oxlint eslint/sort-keys is disabled in lintmax. Why: conflicts with perfectionist (ASCII vs natural sort).

Never-ignore rules

lintmax check FAILS on these suppressions, used or unused — no suppress-for-now path reaches CI. Fix the code:

  • @typescript-eslint/no-unsafe-* (assignment, call, member-access, return, argument) — use proper types.
  • @typescript-eslint/no-explicit-any — define the actual type.
  • @ts-ignore / @ts-expect-error / @ts-nocheck — fix the type error.
  • @typescript-eslint/no-non-null-assertion — handle the null case.

Fixes, not suppressions:

  • Test-file exception: @ts-expect-error + no-explicit-any allowed in test files only (asserting a wrong type is rejected); the rest forbidden everywhere.
  • Untyped third-party dep (types resolve to any: broken exports.types, unresolved typeof import(...)): cast through a typed facade at one boundary — const get = rawGet as <T>(k: string) => Promise<T | undefined>, or const x: unknown = await loader.init(); return x as MonacoApi with a minimal interface. Never as any.
  • Non-null (x[i]!): null-check (const v = x[i]; if (v) ...) or const-tuple ([...] as const) so fixed indices type as defined.
  • no-unsafe-* on a visible-shape stub: (() => undefined) as never (bottom type, no visible ops); ((..._: unknown[]) => ({})) as never still trips. Tighten with never / branded / generic.

Safe-to-ignore

  • oxlint: promise/prefer-await-to-then (Promise.race, ky chaining).
  • eslint: no-await-in-loop, max-statements, max-depth, complexity (sequential ops) · no-unnecessary-condition (narrowing) · promise-function-async (thenable returns) · max-params · @next/next/no-img-element (external images) · react-hooks/refs.
  • biome: style/noProcessEnv (env files) · performance/noAwaitInLoops (sequential ops) · nursery/noForIn · performance/noImgElement · suspicious/noExplicitAny (generic boundaries).

Playbook maintenance

  • Merge each new lesson into the most relevant existing section immediately; correct rules in place, remove superseded guidance. Why: single source of truth, no append-only “recent lessons” buckets.

On this page