tai42
monorepo. The core layers define and implement the contract, the plugins build
on it, and the e2e suite proves them together. Contribute in the member
directory the change touches, under the repository’s CONTRIBUTING guide.
The repository
tai42
The monorepo — every first-party package as a workspace member.
Plugins
Every shipped plugin and its member path, in the Plugins section.
The shared discipline
Every member holds to the same rules, enforced in the monorepo’s CI:- The leaf rule. Among
tai42-*packages, the skeleton importstai42-contract,tai42-kit, andtai42-clionly; a plugin importstai42-contract(andtai42-kitwhere it needs shared helpers) only. No plugin imports the skeleton — providers register through thetai42_apphandle when the manifest loads them. Each member’sruffbanned-apiwall enforces this: the monorepo makes the imports physically reachable, so the wall is the guard. - Providers stay out of the core. Connectors, storage, config, backends, and monitoring ship as plugins that register at manifest load — never imported by the skeleton.
- Optional drivers stay optional. A heavy client driver reaches the engine through an extra; a module whose extra is missing fails loudly at import, never a silent skip.
- Errors surface loudly. A missing module, a malformed config entry, or a failed sub-step raises and propagates.
- Typed packages. Every package ships
py.typedand keeps the type-checker clean.
Develop
One clone, one sync. The workspace resolves everytai42-* member in-tree, so no other checkout has to be present:
core/contract, core/kit, core/cli, core/skeleton) carry their dev tooling as a dev extra rather than the dev dependency group — append --extra dev to their sync. --no-sync runs each gate against the synced environment instead of re-resolving. See the member’s README for the exact extras it installs, and the repository’s CONTRIBUTING guide for the rest — any secret scan, and the licensing statement.
How dependencies resolve
The workspace sources are the resolution.[tool.uv.workspace] lists core/*, plugins/*, and e2e; each member’s [tool.uv.sources] points its tai42-* siblings at { workspace = true }, so uv sync resolves them from the in-tree members, never from the index. One uv.lock at the repository root pins the whole set.
- A cross-member change is atomic. Editing a member and widening a dependent’s version cap land in one commit, one lock, one PR — no publish step, no propagation PR, and no per-repo lock that goes stale when a sibling bumps.
- CI resolves the same workspace. Each member’s gate runs
uv sync --locked --package <name>against the committed root lock, so a repo’s gate proves it against exactly the members a contributor develops with. There is noUV_NO_SOURCES, no sibling-clone step, and nouv lock --no-sources. - The
e2esuite proves the fleet together. Cross-member drift surfaces in the integration run, not in each member’s own gate.
End-to-end suites
Thee2e member boots the real multi-process deployment and drives it over HTTP; its ui/ workspace drives the Studio in a browser. One setting, TAI_E2E_TARGET, selects what every end-to-end suite drives — this one and the browser suites of the Studio and plugin repositories:
- Unset — the run builds its own stack and drives it.
- Set — the run builds nothing and drives a running stack: a bare origin (
https://stack.example.com), or the name of a target file in the suite’stargets/directory.
url, key_env — the name of the environment variable that holds the login key — and provides, the facts the stack cannot report about itself. Everything it can report is read once from its GET /api/system/kinds.
Every test declares what it needs from a stack, in one vocabulary:
Against a target, a test whose needs are met runs; every other test is skipped with the unmet need as the reason, and a test that declares nothing is treated as built-stack only. When the run builds its own stack the declarations change nothing.
In CI the jobs read the target from the
e2e_target input of a manual run, else from the TAI_E2E_TARGET repository variable, and the key from the TAI_E2E_KEY secret. Every run writes a JUnit XML report (junit.xml) in which a skipped test carries its reason; CI uploads it as an artifact and renders it into the job summary. The suite’s README covers the details.
Release
The monorepo releases through one manifest-moderelease-please train. Scope each conventional-commit by the member directory it touches (fix(agents): …, feat(toolbox): …); a fix:/feat: commit on a member opens a release PR for that package alone. Merging it tags tai42-<pkg>-v<ver> — the component-scoped tag that lets 32 packages share one repository — and publishes that package to PyPI.
Before it publishes, the release diffs each package’s public API against its previous release tag with griffe and fails when the surface change outruns the version bump it rides. A breaking surface change must ride a conventional commit marked breaking — a major bump for a package at >=1.0, though a 0.x package may take a breaking change in a minor.
The gate’s mode is configuration. label-honesty allows a 0.x package the minor-breaking slot; strict removes it, so a breaking change then requires the 1.0 graduation.
See also
- The layering — how contract, kit, cli, and skeleton relate.
- Plugins — every listed plugin and what it provides.

