Architecture layers
The codebase is organized into three main layers:- cli/ — User-facing commands. Parses flags, runs preflight checks, calls orchestration, renders output. No business logic.
- src/ — Orchestration. Multi-step workflows, side effects, Modal decorators. Depends on adapters.
- libs/ — Adapters wrapping external SDKs/APIs. Each
libs/<service>/is self-contained and idiomatic to one external service.
- webhooks/ — Standalone Modal apps for event handling. Depend on libs and src.
- data-gen/ — Independent data products.
Boundary rules
No cross-lib imports
libs/<x> must not import from libs/<y>. If two adapters must coordinate, route through src/ orchestration instead.Exception: Shared utilities (libs.telemetry, libs.logging, libs.filesystem) are importable from anywhere.No orchestration in libs/
Adapters must be callable in isolation. No side effects, no Modal decorators, no orchestration logic.
libs/<x> exposes idiomatic Python functions and types.No business logic in cli/
The CLI layer parses arguments, validates input, and calls
src/ workflows. It does not contain application logic.Enforcing boundaries with tach
Boundaries are checked automatically by tach in two places:-
Locally — Run
uv run tach checkbefore pushing: -
In CI — The
trunk-check.ymlworkflow runsuv run tach checkon every PR. Violations block merges.
Common violations and fixes
”libs.<x> cannot depend on libs.<y>”
If two adapters genuinely share code (beyond the utilities exceptions above):
- Move the shared code to a utility — Declare it in
tach.tomlwithutility = trueif it’s cross-cutting (e.g., file operations). - Route through src/ — Have both adapters expose functions, then coordinate them in
src/orchestration. - Duplicate the type — For small, self-contained types, duplication can be simpler than introducing a dependency.
”src cannot depend on libs.<x>”
This is expected — src/ depends on all libs adapters. If a libs/ module imports from src/, invert the dependency:
- Remove the import from
libs/ - Pass the needed value (e.g., an app name) as a parameter or environment variable
- Move the Modal-aware logic into
src/
”cli cannot depend on specific libs”
The CLI layer can import types and utilities from any adapter (for validation, rendering), but should avoid deep orchestration logic. If you’re writing workflow code incli/, move it to src/ instead.
Configuration (tach.toml)
Thetach.toml file at the repo root defines:
source_roots— Where to search for modules (the repo root)exclude— Paths to skip (tests, docs, scripts, etc.)modules— Explicit layer definitions and utility declarationsforbid_circular_dependencies— Prevent circular importsignore_type_checking_imports— AllowTYPE_CHECKINGblocks to bypass import rules (used by webhook type stubs)
tach.toml source for the full configuration.
Resources
- Tach documentation — https://github.com/tach-org/tach
- AGENTS.md — Architecture rules for this repo (in the root)
- trunk-check.yml — CI workflow that enforces boundaries