Contract surfaces where components interact. When a change crosses one of these, investigate consumer impact before you finish the work — not after.
What makes this project's boundaries unusual: most consumers are outside this repo and cannot be
migrated in lockstep — a published PyPI package, an orchestrator model reading tool descriptions,
and arbitrary OpenAI clients. There is no "update both sides in one commit" option for the surfaces
marked external below. Full contracts live in api-contract.md.
- Producer:
tanglebrain/serve/views.py(pure translation),serve/server.py(dispatch + socket) - Consumer: any OpenAI-compatible client, anywhere. Will never read our docs.
- Contract:
POST /v1/chat/completions,GET /v1/models. Request/response shapes are OpenAI's, not ours — compatibility is defined upstream. Errors are{"error": {message, type, code}}viaerror_envelope. - Crossing it means: the
modelrouting directive semantics, the error taxonomy, or the streaming contract (one flushed SSE event per delta; mid-stream failure ends with an{"error": ...}event and no[DONE]).
- Producer:
tanglebrain/mcp_server.py(tool definitions + descriptions),tanglebrain/delegate.py(routing logic) - Consumer: an orchestrator model. It reads tool descriptions rather than documentation, cannot ask a clarifying question, and cannot be retrained when a signature changes.
- Contract: the four tool signatures, the
target>task> local precedence, per-itemstatusvalues (ok/no_fit/error), input-order results, andNoDelegateFitsurfacing as an instruction rather than an error. - Crossing it means: any signature change, and any change to a tool description — the description is the contract for this consumer. A misleading description causes silent misrouting, not an error.
- Trap: the
delegatedescription enumerating the target menu is built once at server startup. A roster change is invisible to a running server.
- Producer:
tanglebrain/cli.py - Consumer: operators' shell history, scripts, aliases.
- Contract: flag names and semantics. Precedent (
--route) is that a superseded flag becomes an accepted no-op rather than being removed.
- Producer:
tanglebrain/roster.py(RosterEntry,Invoke) - Consumer: hand-authored operator YAML living outside the repo, plus
roster_edit.pyand the GUI. - Contract: field names, types, and defaults. A roster
idis doubly public — it is themodelvalue on the HTTP surface and thetargetvalue on the MCP surface. - Crossing it means: renaming or removing a field breaks operator configs that no migration can reach. New fields must be optional with safe defaults.
- Producer:
tanglebrain/adapters/base.py - Consumer:
router.py,selector.py,delegate.py - Contract:
run(prompt, opts) -> text; failures normalize toAdapterError; streaming is an optionalrun_streamcapability, never assumed. - Crossing it means: widening the base interface — which forces every adapter to implement or stub a capability most backends lack.
- Producer:
tanglebrain/measurement.py - Consumer: the
--statsrollup, the GUI, and every record written by an older version. - Contract: always-present fields plus optional
task_id/parent_task_id/origin. A missing field reads as "not applicable", never as an error. - Crossing it means: adding a required field, or changing the meaning of
spend_avoided_usdor thetaskvsdelegatekind split that keeps the headline from double-counting.
- Producer:
cli.py(mintstask_id, injectsTANGLEBRAIN_TASK_ID) - Consumer:
run_delegate, via an orchestrator process we do not own - Contract: the env var name, and that a missing value degrades to
unlinkedrather than raising. - Crossing it means: renaming the variable silently unparents every delegation with no error
anywhere. See
architecture.md.
- Producer:
tanglebrain/gui/views.py+server.py - Consumer:
gui/static/*.html(vanilla JS, same repo — the one boundary where both sides move together) - Contract: JSON shapes, and the fixed allow-list of editable roster fields (
enabled,can_orchestrate,budget_usd_month,good_at) which is the mass-assignment control. Widening it is a security change.
- Producer:
.claude-plugin/marketplace.json→plugins/tanglebrain-delegate/ - Consumer: Claude Code's plugin loader
- Contract: manifest schema plus the console-script name. Guarded by
tests/test_plugin_manifest.py.
- Producer:
pyproject.toml - Consumer: every
pip install - Contract: console-script names, the optional
delegateextra, dependency constraints,requires-python. - Crossing it means: the v0.20.1 failure class. Guarded by
tests/test_packaging.py— update that test deliberately when #90 lifts themcp < 2cap; never delete it to green a build. Verify from a clean venv against real PyPI, since a source checkout has the dependency already importable and cannot see the break.
| Level | Exists | When to run | Location |
|---|---|---|---|
| Unit | Yes | Every change | tests/test_*.py — hermetic, HTTP mocked at the adapter seam |
| Integration | Partial | Changes crossing internal boundaries | tests/test_serve.py, test_gui.py exercise dispatch() end-to-end without sockets |
| Contract | Yes | Packaging, plugin, or API-surface changes | tests/test_packaging.py, tests/test_plugin_manifest.py, tests/test_openai_compat.py |
| End-to-end | Opt-in | Before release, and after any adapter change | tests/test_live.py, gated by TANGLEBRAIN_LIVE=1 (make test-live) |
Recorded, not fixed.
- No automated test covers the
TANGLEBRAIN_TASK_IDorchestrator hop. It is verified live against one orchestrator (Claude Code), by hand. It cannot be tested hermetically because the middle hop belongs to software this project does not own. - No socket-level test for either HTTP server.
dispatch()is tested directly; theThreadingHTTPServerwrapper and the loopback bind itself are not. Given the bind is the entire authorization model, a test asserting it never binds off-loopback would be cheap and would guard the project's highest-consequence invariant. #98.