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
| Concern | Python | TypeScript |
|---|---|---|
| Package manager | uv (never pip, never poetry) | bun (never npm/pnpm) |
| Lint + format | ruff (check --fix, format) | biome check |
| Types | basedpyright (never mypy) | tsc --noEmit |
| Tests | pytest | bun test / Playwright |
| Security | bandit on app code only | — |
| Markdown | markdownlint-cli | markdownlint-cli |
| Commit messages | commitizen (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
$schemaon the same version as the@biomejs/biomedevDependency. Groudon currently claims schema2.3.15against^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:
| Stage | Hooks | Why |
|---|---|---|
pre-commit | file hygiene, ruff --fix, ruff-format, bandit, biome check --write, basedpyright, tsc | Fast. Must stay under a few seconds or people start using --no-verify. |
pre-push | pytest, builds, coverage gates | Slow and needs a database. Wrong thing to block a commit on. |
commit-msg | commitizen | Conventional 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-msgPlain 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
| Repo | uv/bun | ruff | Types | pre-commit | CI lint |
|---|---|---|---|---|---|
| honcho | ✅ | ✅ standard | basedpyright | ✅ | types only |
| groudon | ✅ | ✅ standard | basedpyright | ✅ | ❌ |
| kyogre | ✅ bun | — | tsc | ❌ | ❌ |
| excadrill | ✅ | E,F,I,W | pyright basic | ❌ | ❌ |
| minccino | ✅ | E,F,I,W | pyright basic | ❌ | ❌ |
| machamp | ✅ | ❌ | ❌ | ❌ | ❌ |
| metagross | ✅ | no select | mypy | ❌ | ❌ |
| tentacruel | ✅ | ❌ | ❌ | ❌ | tests |
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:
- Add one
lintjob to every repo, with that exact name.uv syncthenuv run ruff check .,uv run ruff format --check .,uv run basedpyright; for TS packagesbun install --frozen-lockfilethenbun run lintandbun run typecheck. Roughly fifteen lines; fold it into Honcho’s existingstaticanalysis.yml. - One org ruleset on
plastic-labs, targeting default branches across all repos, requiring thelintstatus 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. - Only then consider a reusable workflow.
plastic-labs/.githubdoes 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.