One toolchain per language, one set of configs, enforced by CI. This page is the canonical copy — Honcho is the reference implementation for Python, Groudon’s dashboard/ for TypeScript.

The toolchain

ConcernPythonTypeScript
Package manageruv (never pip, never poetry)bun (never npm/pnpm)
Lint + formatruff (check --fix, format)biome check
Typesbasedpyright (never mypy)tsc --noEmit
Testspytestbun test / Playwright
Securitybandit on app code only
Markdownmarkdownlint-climarkdownlint-cli
Commit messagescommitizen (conventional commits)same

No exceptions worth having. If a repo uses something else it is drift, not a decision.

Python

Drop these into pyproject.toml. They are identical in Honcho and Groudon today, so they are the standard by definition.

[tool.ruff.lint]
select = [
    "E",    # pycodestyle
    "F",    # Pyflakes
    "UP",   # pyupgrade
    "B",    # flake8-bugbear
    "SIM",  # flake8-simplify
    "I",    # isort
]
ignore = ["E501", "B008", "COM812"]
 
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends"]

E501 is off because ruff format already wraps; B008 because FastAPI’s Depends() in a default argument is the framework’s own idiom.

[tool.basedpyright]
reportIgnoreCommentWithoutRule = false
reportMissingTypeStubs = false
reportUnusedCallResult = false
reportCallInDefaultInitializer = false
reportAny = false
reportExplicitAny = false
reportImplicitOverride = false
allowedUntypedLibraries = ["langfuse"]

basedpyright is strict out of the box; those seven are the noise we agreed to turn off, and they are identical in Honcho and Groudon. Everything else stays on. Set include/exclude per repo and extend allowedUntypedLibraries only for a library that genuinely ships no stubs. Honcho additionally sets reportImportCycles = false; adopt it only if a repo actually has cycles.

Dev group, every Python repo:

[dependency-groups]
dev = ["ruff>=0.11.2", "basedpyright>=1.29.4", "pre-commit>=4.3.0", "pytest>=8.2.2"]

Pin the Python version in .python-version and read it from there in CI (actions/setup-python with python-version-file) so the two cannot drift.

Test config, shared. [tool.coverage.report] below is byte-identical in Honcho and Groudon — copy it verbatim:

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
addopts = "--strict-markers"     # extend per repo: -n auto, --cov=..., --ignore=...
testpaths = ["tests"]
 
[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
]

--strict-markers is the one that matters: without it a typo’d @pytest.mark. silently does nothing. Declare every marker in markers = [...].

Markdown

.markdownlint.json at the repo root. Use Groudon’s — it disables rules by name only:

{
  "default": true,
  "line-length": false,
  "no-duplicate-heading": false,
  "single-h1": false,
  "ol-prefix": false,
  "fenced-code-language": false,
  "first-line-h1": false
}

Honcho’s file sets each of those a second time by number (MD013, MD024, MD025, MD029, MD040, MD041) — same effective config, six redundant lines. Groudon has the opposite problem: it has the config file but no markdownlint hook in .pre-commit-config.yaml, so nothing ever reads it. Both are one-line fixes.

TypeScript

biome.json at the package root. Copy Groudon’s dashboard/biome.json: 80-column, 2-space, single quotes, semicolons as needed, ES5 trailing commas, recommended rules on, organizeImports as an assist action, and useSortedClasses for Tailwind. Keep vcs.useIgnoreFile: true so biome respects .gitignore.

The three configs we have (Honcho’s TS SDK, Kyogre, Groudon dashboard) already agree on all of that. Where they differ, prefer:

  • noExplicitAny: "warn" — not "off" (Kyogre), not "error".
  • useSortedClasses: { level: "warn", fix: "safe" } in an app with Tailwind.
  • Keep $schema on the same version as the @biomejs/biome devDependency. Groudon currently claims schema 2.3.15 against ^2.1.4; that’s a bug.

Scripts, same names in every package.json, because CI and pre-commit call them by name:

"lint": "biome check .",
"lint:fix": "biome check . --write",
"format": "biome format . --write",
"typecheck": "tsc --noEmit"

Pre-commit

Copy Honcho’s .pre-commit-config.yaml and swap the path regexes. There is no inheritance mechanism in pre-commit and building one is not worth it — the third-party hooks are already remote refs, and the local hooks are repo-specific commands (uv run basedpyright, cd dashboard && bun run typecheck) that cannot be shared anyway.

What matters is which stage each hook runs at:

StageHooksWhy
pre-commitfile hygiene, ruff --fix, ruff-format, bandit, biome check --write, basedpyright, tscFast. Must stay under a few seconds or people start using --no-verify.
pre-pushpytest, builds, coverage gatesSlow and needs a database. Wrong thing to block a commit on.
commit-msgcommitizenConventional commits, so changelogs generate.

Scope every hook with a files: regex. Running bandit over tests or basedpyright over migrations produces noise nobody reads.

Install once per clone — this is the step people forget:

uv run pre-commit install --install-hooks
uv run pre-commit install --hook-type pre-push --hook-type commit-msg

Plain pre-commit install only wires the pre-commit stage. Without the second line the pre-push and commit-msg hooks are configured and never run.

Where we actually are

Repouv/bunruffTypespre-commitCI lint
honcho✅ standardbasedpyrighttypes only
groudon✅ standardbasedpyright
kyogre✅ buntsc
excadrillE,F,I,Wpyright basic
minccinoE,F,I,Wpyright basic
machamp
metagrossno selectmypy
tentacrueltests

Known bug in both configured repos: .pre-commit-config.yaml pins ruff-pre-commit at v0.8.4 while pyproject.toml asks for ruff>=0.11.2. The hook and uv run ruff are running different linters. Bump the rev.

Enforcing it

Pre-commit is a convenience, not a gate — --no-verify skips it and CI never notices, because no workflow runs ruff or biome today. Fix that first, then make it a rule:

  1. Add one lint job to every repo, with that exact name. uv sync then uv run ruff check ., uv run ruff format --check ., uv run basedpyright; for TS packages bun install --frozen-lockfile then bun run lint and bun run typecheck. Roughly fifteen lines; fold it into Honcho’s existing staticanalysis.yml.
  2. One org ruleset on plastic-labs, targeting default branches across all repos, requiring the lint status check plus linear history and a PR review. A ruleset applies org-wide from one place; branch protection has to be configured per repo, so use a ruleset.
  3. Only then consider a reusable workflow. plastic-labs/.github does not exist yet. Copying fifteen lines into eight repos is cheaper than a new repo until the job changes twice.

The ruleset is what makes this self-enforcing: a repo that hasn’t adopted the standard fails the required check and can’t merge, so drift surfaces as a red CI run instead of a wiki page nobody rereads.