All notable changes to TangleBrain are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
Published the design documents as
docs/design/. Eight documents covering runtime architecture, the four API contract surfaces, the data model and what survives a crash, the security model, contract boundaries, observability, nonfunctional requirements, and operations — plus an index. They state what the project promises and why, and they name their own gaps rather than only their strengths. Linked fromREADME.mdandCONTRIBUTING.md, which now asks that changes to routing, adapters, or either HTTP surface update the matching document in the same PR. -
Filed every gap the design documents disclose as a tracking issue, so each admission carries a fix path: the missing loopback-bind test (#98), unchecked
key_reffile permissions (#99), unrecorded failures and lost failover attempts (#100), theusage.jsonlgrowth and cache-tier placement question (#101), and the incorrect--rosterhelp text (#102). Three coordination-layer gaps found while designing a multi-backend setup are filed alongside them: no way to designate an orchestrator rather than round-robin (#95),--modelsilently stripping an orchestrator's delegate tool (#96), and capability routing being unable to route upward (#97).
-
Reconciled
FEATURES.mdandPROJECT-MAP.mdwith the current generator. Both landed in the repo carrying a superseded TangleClaw scaffold:FEATURES.mddocumented the abandonedfile.js:linepointer format (the generator now mandates stablefile.js#symbolNameanchors, precisely because line pointers rot) and named its third sectionMethodologies / Enginesrather thanGovernance / Engines, andPROJECT-MAP.mdomitted theTangle-Shareddoc group. Both files now matchTangleClaw/lib/projects.js, the three auto-stubbedTBDentries are resolved, and the<!-- describe -->placeholders are filled. -
Issue templates now declare labels that exist.
feature.mdandadd-backend.mdasked for afeaturelabel the repo has never had, sogh issue createfailed outright on it and UI-filed issues landed unlabeled. Both now useenhancement; thebackendlabeladd-backend.mdalso referenced has been created. Every label named across the three templates now resolves.
- The
delegateextra no longer installs an SDK it cannot import (#87).mcp2.0.0 (2026-07-28) renamedFastMCPtoMCPServerand removed themcp.server.fastmcppathtanglebrain/mcp_server.pyimports, so the open-endedmcp >= 1.0constraint resolved to a broken install —pip install "tanglebrain[delegate]"produced a delegate server that failed at import, and CI went red on every branch. The constraint is nowmcp >= 1.0, < 2; a newtests/test_packaging.pyasserts the upper bound stays, since nothing at runtime exercises a dependency constraint and the gap went unnoticed until a resolve happened to pick up the new major. Migrating to the 2.x API is tracked separately — lifting the cap moves the floor tomcp >= 2and drops 1.x users, which is a decision rather than a bump.
-
Antigravity CLI (
agy) as the gemini replacement orchestrator (#61) — the packaged example roster documents anantigravitysub entry (agy -p {prompt},parse: plain; verified live on agy 1.0.10: bare response on stdout, ~5s, rides the old CLI's~/.geminiOAuth), restoring the third orchestrator slot the 2026-06-18 gemini sunset emptied. No delegate injection yet — agy exposes no per-invocation MCP flags; #81 tracks wiring the local-delegate tool when it does. The live suite exercises the entry whenagyis installed. -
Serve-origin marker + parent-task attribution (#74) — usage records now carry an
originfield (cli|gui|serve) so serve-mode traffic is distinguishable from CLI and panel runs, andtanglebrain --statsshows the per-origin split (records predating the field roll up asuntagged, never guessed at). OpenAI-compat callers can additionally send an optionalX-TangleBrain-Parent-Taskheader carrying their own task/session identity — trimmed, capped at 128 chars, recorded onto the usage record asparent_task_idfor cross-system attribution, never routed on. Both are additive record fields; old records and readers are unaffected.
- Library streaming core (c13-S1 of #73) — adapters gain an optional
StreamingAdaptercapability (run_stream(prompt, opts) -> Iterator[str]), implemented by the openai-compat adapter as httpx SSE pass-through (the paidapiadapter inherits it; both billing gates are untouched — they act at selection time, before any adapter exists). Newrun_once_stream()besiderun_once: identical path precedence, task-id minting, and gates; direct-adapter paths (pin /--local/ gate-local) stream incrementally when the backend can, everything else — including the router path, per the ratified c13 v2 scope — delivers the completed text as a single-item stream. Metering parity: streamed responses are recorded on stream completion, with partial text recorded when a stream dies or is abandoned mid-way (real backend spend), and nothing recorded for a stream that failed before its first token. - True incremental streaming in
tanglebrain-serve(c13-S2, closes #73) —stream: truenow delivers realchat.completion.chunkdeltas as the backend produces them, for backends that can stream (pinnedopenai-compat/apientries and the classifier-gate local path); cli-kind backends and the full-routerautopath keep the single-chunk delivery (ratified v2 scope). The view primes the pump — the first delta is pulled before headers commit, so every failure up to the backend connection returns a plain JSON error with the right status, never broken SSE — and the handler writes one flushed, close-delimited SSE event per delta. A stream that dies mid-way ends with one in-stream{"error": ...}event and no[DONE]. The finish chunk always carries the estimatedusageblock and thetanglebrainextension. Verified live with the realopenaiclient: multiple incremental chunks from the pinned local backend, and anautoround-trip.
- Knob GUI POST hardening (#72), mirroring the serve endpoint's guards:
POST /api/*now requiresContent-Type: application/json(415 otherwise) — the panel's own requests already send it, and the check keeps no-preflight cross-origin browser requests (e.g. a malicious page POSTingtext/plainto/api/run, which spends real backend quota) from ever reaching a view. A malformedContent-Lengthheader now returns a clean JSON 400 instead of a traceback and a dropped connection, and negative values are clamped.
- Server mode (
tanglebrain-serve) — the router as a local OpenAI-compatible endpoint (issue #70, S1).POST /v1/chat/completionsfronts the same routing path the CLI uses: themodelparam is a routing directive (auto= full router, a roster id = explicit pin, unknown ids → a clearmodel_not_founderror), chatmessagesarrays are flattened to a role-tagged transcript (non-text content parts rejected loudly), andGET /v1/modelslistsauto+ the roster ids.stream: trueis emulated in v1 — the completed response is framed as a single SSE chunk (true incremental streaming is a follow-up). The endpoint binds127.0.0.1only and ignores theAuthorizationheader (local callers need no key); POSTs must sendContent-Type: application/json(415 otherwise — keeps no-preflight cross-origin browser requests from reaching routing); the paid-API tier stays behind both existing billing gates, and served requests are metered exactly like CLI runs. Zero new runtime dependencies (stdlibhttp.server, mirroring the knob panel's pure-dispatchsplit).run_once(return_served=True)'s served summary now also carries the mintedtask_id(the endpoint reuses it as the completion id, linking a response to its usage record).
- Automated PyPI publishing via trusted publishing (
.github/workflows/publish.yml). Publishing a GitHub release now builds, checks, and uploads to PyPI through OIDC — no API token stored anywhere. Guards: the release tag must match thepyproject.tomlversion, andtwine checkmust pass before upload. One-time PyPI-side setup: add the repo/workflow/environment as a trusted publisher under the project's Publishing settings. - PyPI-listing polish. Trove classifiers + full
[project.urls]set (Repository, Changelog, Issues, Releases) inpyproject.toml; README relative links converted to absolute GitHub URLs so the PyPI project page renders them correctly; PyPI version + Python-versions badges added. Takes effect on the PyPI page with the next uploaded release.
- TangleBrain is now on PyPI —
pip install tanglebrain(add[delegate]for the MCP server) replaces the from-GitHub / from-clone install as the primary path. v0.16.0 published; install docs in the README and the plugin README updated accordingly. Publishing also removes the dependency-confusion window the 0.16.0 review flagged (the name can no longer be squatted).
- Claude Code plugin for the delegate MCP server (one-click mode-4, closes #63). The repo is now
its own Claude Code plugin marketplace (
.claude-plugin/marketplace.json), listing atanglebrain-delegateplugin (plugins/tanglebrain-delegate/) that registers the existingtanglebrain-delegatestdio MCP server declaratively. Install becomes two commands —/plugin marketplace add Jason-Vaughan/TangleBrain+/plugin install tanglebrain-delegate@tanglebrain— instead of manualclaude mcp add. The plugin wires the pip-installed console script (documented prerequisite: install the[delegate]extra from GitHub or a clone — TangleBrain is not on PyPI); it does not vendor the Python code. Manifest drift (renamed console script, broken source path, name mismatch) is CI-guarded bytests/test_plugin_manifest.py.
- Refreshed README + ARCHITECTURE for the shipped feature set. The README status line is now
version-agnostic (links the latest release + CHANGELOG instead of a fixed version that drifts), and
the delegation bullet describes the full scatter-gather capability (route by id/capability, fan
out concurrently, metered + linked to the parent task) instead of the old local-only phrasing.
ARCHITECTURE.mdis stamped v0.15.0 and its per-parent-task-tree passages — which still called the tree "deferred" / "the remaining stretch" — now describe it as shipped (theTANGLEBRAIN_TASK_IDpropagation mechanism). Docs-only; no code change. - Skip the
geminilive CLI test — the CLI sunset (#61). ThegeminiCLI sunset for individuals on 2026-06-18 (migrated to Antigravity) and now exits with anIneligibleTierError, soLiveCliTest.test_gemini_returns_textcan no longer pass. It now skips with a pointer to #61; the full live suite is green again against the supported backends (claude + codex + local). Thegeminiorchestrator entry is disabled in the operator roster (out of rotation). Test-only.
- Per-parent-task delegation tree (cross-process linkage, scatter-gather roadmap #39 stretch /
closes #52). Each delegated sub-call is now linked back to the specific top-level task that
spawned it, across the process boundary. The CLI mints a task id per routed task; the
orchestrator-CLI adapter injects it as
TANGLEBRAIN_TASK_IDinto the orchestrator's environment (only when the delegate tool is injected), the orchestrator forwards it to the MCP delegate child it spawns, andrun_delegatereads it back to stamp each delegate record'sparent_task_id. Task records gain atask_id, delegate records gain aparent_task_id(both written only when present, so existing records and readers are unaffected).tanglebrain --statsand the rollup gain aby_parentgrouping — "Linked to: N parent task(s)" — with sub-calls run outside a propagated task grouped asunlinked. The linkage was manually verified live through the real claude→MCP-delegate boundary (the env survives the orchestrator's subprocess hop; the parent and delegate records shared the same id) — the orchestrator-forwards-env hop is a load-bearing assumption, not a TangleBrain-enforced guarantee, so a delegate that loses the env degrades safely tounlinked(never an error). This was the deferred half of the scatter-gather epic whose entry criterion was a live-verification spike — now done. - Knob panel surfaces the delegation tree. The panel's "Delegated sub-tasks" card now shows a
Linked to stat (
N parent task(s), with anyunlinkedsub-calls noted) — GUI parity with thetanglebrain --statsrollup, so the per-parent-task linkage is visible in the panel, not just the CLI. Read-only; no new endpoint (the data already ridesview_stats's rollup payload).
pyproject.tomlpackage metadata carried the purged "cost-tiered / flat-rate subscriptions" framing that the public-rollout neutralization scrubbed everywhere else (#42). Both thedescriptionand thecost-tieredentry inkeywords(→local-llm) — the metadata rendered on the repo and any package index — now match the neutral positioning used in the README andARCHITECTURE.md: "A local-first, config-driven LLM router across OpenAI-compatible backends you own."
- Gated live smoke check for delegate parent-task linkage (closes #55). A
TANGLEBRAIN_LIVE-gated test routes a delegation-inducing prompt through the real router → orchestrator →delegate_localand asserts each delegate record'sparent_task_idmatches the parent task'stask_id— a standing guard for the load-bearing "orchestrator forwards env to the MCP child" assumption (it skips, never fails, if the orchestrator doesn't delegate that run, since delegation is emergent). Test-only; gated off in CI.
- Delegate observability + metering (scatter-gather roadmap #39, slice 6). Delegated sub-calls
are now metered: every
run_delegateexecution (including eachdelegate_manyitem — metered at one seam) is logged as akind: delegateusage record with its served backend + estimated tokens.tanglebrain --statsand the knob panel gain a "Delegated sub-tasks" breakdown by backend (count, est tokens, informational cloud-equiv). Delegate records are kept out of the "spend avoided" headline so a sub-call's saving is never double-counted against its parent task, and concurrent fan-out appends are serialized by a process-level lock. Records carry a newkindfield (task/delegate; older records read astask). The per-parent-task tree (cross-process linkage) is deferred. Closes the deferred metering noted since the measurement layer landed.
- Documented the synthesis/reduce pattern (scatter-gather roadmap #39, slice 4). README +
ARCHITECTURE now spell out that the orchestrator synthesises
delegate_manyresults itself (it holds the original task context), and offloads a mechanical stitch with an ordinarydelegate(task=…)call — so no dedicated reducer tool ships. Documentation of existing behaviour; no code change. The reduce step stays the orchestrator's by design until observability data (a later slice) shows a TB-side reducer would earn its keep.
- Parallel fan-out (
delegate_many). A new MCP tool lets an orchestrator fan several sub-tasks out concurrently in one call and collect them, instead of delegating one at a time. Each item ({prompt, target?, task?, max_tokens?}) routes independently — a batch can mix backends — and runs on aThreadPoolExecutorover the existing syncrun_delegate(plain Python, no new deps). Results come back in input order with a per-itemstatus(ok/no_fit/error); one failing sub-task never sinks the batch. Concurrency is bounded by a system-derived default (os.cpu_count()), an operator override (newdelegate_max_concurrencyinsettings.yaml— pin it to your backend's real parallelism, e.g.OLLAMA_NUM_PARALLEL), and an optional per-callmax_concurrencythat may lower it. Dispatch + collect only — synthesis stays the orchestrator's job. Third slice of the scatter-gather roadmap (#39).
- Capability-routed delegation. The
delegateMCP tool gains ataskparameter: instead of naming a backend id, an orchestrator can ask for a capability (agood_attag, e.g.code) and TangleBrain selects the cheapestcan_delegatebackend good_at it (localbeforesub, ties by declared order) — sub-task-level task-fit mirroring the request-level router. Precedence istarget(explicit id) >task(capability) > free local default. Paidapibackends are never auto-selected bytask(the ratified paid-is-last-resort invariant; reach one only via an explicittarget). When no backend fits atask, the tool hands the sub-task back to the orchestrator to do itself — a returned instruction, not an error (a newNoDelegateFitsignal caught at the MCP boundary). Second slice of the scatter-gather roadmap (#39).
- Generalized / tiered delegate. An orchestrator can now offload a sub-task to a configured
backend, not just the free local model. The
tanglebrain-delegateMCP server gains two tools alongside the unchangeddelegate_local:delegate(prompt, target?, max_tokens?)routes to any roster entry flagged the newcan_delegate: true(mirrorscan_orchestrate), anddelegate_targets()lists the configured menu (id,tier,good_at,cost,kind) so the orchestrator can pick by fit; thedelegatetool's description also enumerates the menu, built at server startup. Targets are invoked as leaves (no recursive delegation);apitargets stay behind the billing gate. Secret-safe (the menu never emits akey_ref). The shipped roster flags its local tiercan_delegate: trueand carries a commented non-local target example. Non-local delegate spend is not metered in this version (orchestration-tree observability is tracked on the scatter-gather roadmap, #39). First slice of #39. Closes #38. - Project logo. A snake-and-circuit-brain mark now brands the README (hosted in the
project-assetsrepo) and the knob panel —tanglebrain-guiships a packaged copy, serves it at/logo.png, and uses it as the page header + favicon.
- README restructured around Problem → Solution. A "Cloud-by-Default Routing / routing debt" problem statement and a "Local-First Router You Own" solution lead the page, plus a "Standalone, or part of the Tangle family" section (welcomes forks/PRs; notes optional integration with TangleClaw). Status line corrected to v0.10.0 — first public release.
- README surfaces the OAuth-/local-first credential model and prompt-aware routing. Clarifies that TangleBrain prefers your local models and authenticated (OAuth) tool sessions — never injecting an API key into a CLI — with the raw-API-key tier a deliberate, gated opt-in; and that an optional classifier reads each request and routes grunt work to the free local backend. The measurement bullet is reframed as cost measurement (spent vs avoided). Doc-only; no feature change.
- Knob-panel header copy. The panel subtitle now reads "roster & pricing config · local spend-avoided rollup" (was a stale "read-only — cost-tiered router config …"; the panel has been editable since the pricing/roster knobs landed).
First public release.
- Neutral positioning + local-only default roster (public-OSS rollout, R2a). Reframed the project
as a local-first, config-driven router across OpenAI-compatible backends you own. The packaged
config/roster.yamlnow ships one active entry — the free local tier; the subscription / authenticated-CLI tier (claude/codex/gemini) ships commented out as an opt-in example like the paid tier, so a fresh clone routes to local out of the box. README rewritten for newcomers (neutral headline, capability list,--local-first quickstart); newARCHITECTURE.md(clean-room, neutral) andDISCLAIMER.md(subscription/CLI adapters are opt-in and your responsibility under each provider's ToS; paid tier is bring-your-own-key, off by default).PackagedRosterTestupdated to the one-active-entry reality. - Generic shipped roster + external roster discovery. The bundled
config/roster.yamlis now a generic example (free local tier points at Ollama onlocalhost:11434, opt-in subscription-CLI entries, no maintainer infra). Your real roster lives outside the repo and is auto-discovered:TANGLEBRAIN_ROSTERenv →~/.config/tanglebrain/roster.yaml(XDG) → the packaged example. So agit pullnever clobbers your config, and the package ships nothing deployment-specific. The--rosterflag still takes precedence. Part of the public-OSS rollout.
tanglebrain --versionprints the package version (fromtanglebrain.__version__) and exits. Closes #29.- Contributor mechanics (public-OSS rollout, R2b).
CONTRIBUTING.md(dev setup viamake venv/make test, branch & PR conventions, What/Why/Test-plan, and "adding a backend is a config edit" first-contribution framing),CODE_OF_CONDUCT.md(Contributor Covenant v2.1), GitHub issue templates (bug,feature,add a backend/adapter), and a pull-request template with a What/Why/Test-plan body and a docs-updated checklist. README gained a Contributing section. roster.packaged_roster_path()(the bundled example) androster.default_roster_path()discovery, mirroring the existing state-dir resolution pattern.
- Dropped local-tooling references from product files. Neutralized cosmetic mentions of the
local development tooling in
CHANGELOG.md,tanglebrain/gui/views.py,tanglebrain/gui/server.py(the--porthelp text), and the.gitignorecomment — they described the maintainer's local workflow, not the product. No behavior change. - Aligned code docstrings, comments, the CLI
--helptext, and shipped config comments with the project's documentation. A consistency pass so the in-code descriptions match the README/ARCHITECTURE framing — the router is described as orchestrator rotation + failover for resilience — and a generic example hostname replaces a deployment-specific one in the tests. Docstrings/comments/strings only — no behavior change (verified by an AST-token structural diff). Closes #30. A GitHub Actions workflow (.github/workflows/ci.yml) runsmake test(the hermetic suite) on every push tomainand on pull requests, across Python 3.10/3.11/3.12. TheTANGLEBRAIN_LIVE-gated tests stay skipped (CI has no backend). README gained a CI status badge. CI immediately surfaced a test-isolation gap — three--model "claude"CLI tests relied on the dev machine's ambient~/.config/tanglebrain/roster.yaml(the packaged example is local-only since R2a) — now fixed to pin a self-contained roster. - Live e2e test (
tests/test_live.py) pins the direct-local path (run_once(..., local=True)) and asserts it was served by the active roster's own local entry (roster-agnostic). Barerun_oncehas routed through the frontier-first router since the default flip, so the acceptance assertion had quietly stopped exercising the local path (#24). Test-only.
- Local classifier gate (plan §6 evolution path), off by default. An optional cheap local
classify can now run in front of the router: it rates each request's complexity using free local
gpt-oss and sends trivial work straight to free local (skipping the rate-limited subs), while
frontier work falls through to the normal frontier-first router. This preserves sub rate-limit
runway when rotation alone isn't enough.
- Off by default — new
classifier_gate_enabledsetting (config/settings.yaml, defaultfalse); per-run--gate/--no-gateoverride the setting. Built ahead of the §8 data trigger, so existing routing behaviour is unchanged until it's turned on. - Fail-safe by design — the classifier rates task complexity (not "can the local model do
it?"), and any ambiguity, parse miss, or classifier error resolves to frontier, so the gate
can never trap a hard task on the local tier. New
tanglebrain/classifier.py; gated work is metered withpath=gate-local.
- Off by default — new
- Editable roster in the knob panel (plan §5/§9.2). The
tanglebrain-guiroster card is now editable for a focused set of per-entry scalar fields —enabled,can_orchestrate,budget_usd_month, andgood_at— each row with its own Save. Completes the deferred half of the C5 knob GUI (pricing became editable in C5b).- Comment-preserving, zero new deps: a new
tanglebrain/roster_edit.pyedits the targeted value on the targeted line in place, so every inline comment, blank line, the nestedinvokeblock, and the commented paid-API example survive byte-for-byte — no YAML round-trip library. Adding/removing/reordering entries and editing theinvokeblock stay hand-edits (out of scope). - Write-safety mirrors C5b: edits are validated (and the candidate is re-parsed with the real
loader before any write, so a surgical slip can never land a malformed roster), the prior file
is backed up to
<state_dir>/backups/roster-<ts>.yaml, and the write is atomic. The panel sends only changed fields and confirms before overwriting the trackedconfig/roster.yaml. - New
views.save_roster_view()+POST /api/roster.
- Comment-preserving, zero new deps: a new
tanglebrain.__version__now derives from the installed package metadata (importlib.metadata.version) instead of a hardcoded literal, so it always trackspyproject.tomland can no longer drift from the released version — it had been frozen at0.1.0since C1 while releases moved on to 0.7.0 (#17). Falls back to0.0.0+unknownwhen imported from an uninstalled source checkout.
-
C6b — last-resort paid-API routing. The frontier-first router can now fall through to a paid
tier: apientry as a genuine last resort (plan §6): only after everycan_orchestratesub has failed/exhausted, and only when theapi_billing_enabledgate is on. With the gate off (the default) the router never reaches a paid tier — behavior is unchanged. Part of #2.- Enabled
apientries are tried in roster order; a paid success is surfaced onRouter.last_served(so the run is meteredtier=api,spend_avoided=0) but does not advance the orchestrator rotation cursor. Paid failures fail over to the nextapientry and are listed in theRouterErrorwith the same[rate-limit]annotation as orchestrators. - The router requires at least one orchestrator to be present — it never paid-routes a roster with
no subs to exhaust (use
--model <id>for an explicit paid call).Router(... settings=)is injectable; it defaults to the packagedconfig/settings.yaml.
- Enabled
-
C6c — paid-API visibility in the knob panel + runbook. Closes #2. The
tanglebrain-guiroster card now surfaces each entry'senabledkill-switch (adisabledpill) andbudget_usd_month(a display-onlybudget: $N/monote), and shows a Paid-API billing: ON/OFF banner from the global gate — so an operator never misreads a paid entry's ownenabledflag as "live" when the global gate is off. Newview_settings()view +GET /api/settingsroute (reads onlyconfig/settings.yaml; no key file touched). All read-only — per the v1 decision, TangleBrain does not meter or enforce spend; the hard budget cap stays LiteLLM-side on the virtual key.- README gains a step-by-step runbook for minting a budget-scoped LiteLLM virtual key on your
LiteLLM gateway and wiring it via
key_ref, plus how to pause spend (enabled: falseor the gate).
- README gains a step-by-step runbook for minting a budget-scoped LiteLLM virtual key on your
LiteLLM gateway and wiring it via
- C6a — paid-API tier scaffolding (off by default). A new
apiadapter and the global billing gate that guards it. Atier: apiroster entry now parses fully but is never routable until it is explicitly enabled — preserving today's safe, zero-paid-spend default (issue #2).- The gate: new
tanglebrain/settings.py+config/settings.yamlwithapi_billing_enabled(defaultfalse). A missing settings file defaults the gate off; a malformed one is a hard error (never a coincidental enable).selector.build_adapterbuilds anapientry only when the global gate and the entry's ownenabledflag are both on, else raises clearly. - The adapter:
tanglebrain/adapters/api.pyApiAdapter— paid APIs are LiteLLM-fronted, so it reuses the OpenAI-compat transport and references a scoped LiteLLM virtual key viakey_ref(never a raw provider key, resolved lazily at call time). - Roster fields:
apiinvoke now requiresbase_url+model+key_ref; new per-entryenabled(kill-switch, defaulttrue) andbudget_usd_month(display-only in v1 — the hard cap is enforced LiteLLM-side on the virtual key). A commented example entry ships inroster.yaml. - Last-resort routing (wiring
apiinto the router) is not in this change — that is C6b.
- The gate: new
- C5b — editable pricing in the knob panel. The panel's pricing card is now editable: change the
input/output $/MTok, the reference-model label, and the placeholder flag, then Save to persist
to
tanglebrain/config/pricing.yaml. Closes #13.- Write-safety: strict validation before any write (rejects non-numeric/negative rates and an
empty reference model — nothing is persisted on a bad value); the file is written atomically
(temp +
os.replace) and the prior version is backed up to<state_dir>/backups/first. - Comment-preserving: the canonical methodology header is re-emitted on every save, so GUI/ programmatic edits never strip it — no new dependency. (Roster editing stays out — its dense inline comments need a comment-preserving mechanism, deferred to a later chunk.)
- New
measurement.validate_pricing()/save_pricing(); the panel writes the tracked repo config so an edit is git-visible and committed by the operator.
- Write-safety: strict validation before any write (rejects non-numeric/negative rates and an
empty reference model — nothing is persisted on a bad value); the file is written atomically
(temp +
cli.run_once()gained an optionalreturn_served=Truethat also returns the served{path, tier, model}. The knob panel uses it to report which tier handled a run without re-reading the usage log — removing the C5a best-effort race. Default behavior (returns a bare string) is unchanged.
- C5a — knob GUI (read-only panel), a simple dark-themed panel. A new
tanglebrain-guiconsole script serves a thin, localhost-only web panel (stdlibhttp.server+ a single vanilla HTML/CSS/JS page — zero new runtime dependencies) on port 3250. The panel: views the live roster (§5), the pricing reference, and the local C4 spend-avoided rollup, and runs a prompt through the router (prompt in → final out), showing which tier/model served it (read from the C4 usage log; panel runs are metered automatically). First slice of plan §10's "C5 — Knob GUI".- Read-only this chunk — config editing (write-back to YAML) is deferred to C5b (#13).
- Secret-safety: the roster view emits
key_refas the stored reference string only; it is never resolved and no key file is read, so no secret material reaches the browser. - Binds
127.0.0.1only — the panel spends real sub rate-limit quota when it runs prompts and reads the roster, so it must not be network-exposed. Newtanglebrain/gui/package; HTTP routing is a puredispatch()over testable view functions (tanglebrain/gui/views.py).
- C4 — measurement / "spend avoided" rollup (plan §8). Every routed task is now logged as one
JSON line in an append-only usage log (
~/.cache/tanglebrain/usage.jsonl, honoringTANGLEBRAIN_STATE_DIR): the execution path, tier, model, estimated tokens, and the cloud-equivalent cost it avoided.tanglebrain --statsrolls those records up into a "spend avoided" figure — what the work would have cost on a paid frontier API. Closes #10.- Uniform token estimation: CLI subs expose no usable token counts, so tokens are estimated
with a single
chars/4heuristic over the visible prompt + response, applied identically to every tier — one consistent (if approximate) methodology. No adapter or routing behavior change. - Config-driven pricing (
tanglebrain/config/pricing.yaml) carrying a local pricing source'scostSavedanchor — Claude Sonnet at $3/$15 per MTok (methodology ratified 2026-06-13) — so avoided spend is valued consistently. Aplaceholderflag (false by default) makes the rollup render a PLACEHOLDER caveat if the anchor is ever forked before re-ratifying. - New module
tanglebrain/measurement.py; the router now exposesRouter.last_servedso the CLI metering seam can record which tier handled each task. All measurement I/O is fault-tolerant — a logging failure never affects the returned answer, and a corrupt log line never breaks the rollup. - Scope: meters top-level routed tasks only (the three
run_oncepaths). The gpt-oss MCP delegate's sub-calls are intentionally not metered (they run inside an already-counted sub task).
- Uniform token estimation: CLI subs expose no usable token counts, so tokens are estimated
with a single
- C3b — frontier-first is now the default, and orchestrators offload grunt to free local
(BEHAVIOR CHANGE).
tanglebrain "prompt"(no flags) now routes through the frontier-first router instead of going straight to the local tier; pass--localfor the old direct-to-gpt-oss behavior, or--model <id>to pin an entry. Each orchestrator is now invoked with the C2bdelegate_localtool available, so it decomposes the task and offloads sub-tasks to the free local backend — the offload behind frontier-first decompose (plan §6). Closes #7.- Config-driven injection: a new
invoke.delegate_argsroster field carries the per-CLI flags that register + allow the delegate, with{delegate_mcp_json}/{delegate_mcp_command}tokens substituted at runtime (the delegate runs aspython -m tanglebrain.mcp_server, so it resolves without PATH assumptions). Adding/adjusting a CLI is a config edit (§5). - Verified live, all three orchestrators delegate to gpt-oss: claude (
--mcp-config+--allowedTools, API key scrubbed), codex (-c mcp_servers…+ approval bypass), gemini (after a one-timegemini mcp add+--approval-mode yolo). See the README for gemini's setup.
- Config-driven injection: a new
-
C3 — frontier-first router (control plane).
tanglebrain/router.pyroutes a task to a frontier sub acting as orchestrator, rotating the role across thecan_orchestratesubs with automatic failover for resilience (plan §6).- Task-fit selection: a
--task <good_at-tag>hint prefers orchestrators good at it (falling back to all when none match — a preference, not a gate). Auto-classification stays deferred (§6: "only if volume demands"). - Rotation: round-robin across orchestrators, with the cursor persisted across processes
(
~/.cache/tanglebrain/router-state.json, override viaTANGLEBRAIN_STATE_DIR) so successivetanglebraininvocations actually spread load. Missing/corrupt state resets to 0, never crashes. - Failover: on an
AdapterErrorfrom one orchestrator, advance to the next; if all fail, raiseRouterErrornaming each failure (rate-limit-looking ones are annotated[rate-limit]). - Exposed via
tanglebrain --route [--task <kind>]. The CLI default stays local-first — the router becomes the default in C3b (#7), once the local-delegate is wired into orchestrator runs (routing whole tasks to subs without local offload would burn rate limits for no cost benefit). - Lives in its own module; the C1 selector stays minimal. Rotation/failover are proven by the hermetic suite (round-robin, wraparound, failover, persisted cursor); a gated live test confirms a real route returns text end-to-end.
- Task-fit selection: a
-
C2b — gpt-oss MCP local-delegate.
tanglebrain-delegate, an MCP server exposing a singledelegate_local(prompt, max_tokens?)tool, lets a frontier orchestrator (claude / codex / gemini) offload grunt work to the free local tier (gpt-oss-120b) at $0 — the mechanism behind frontier-first decompose (plan §6). Closes #4.tanglebrain/delegate.py:run_local_delegate(...)— the routing logic, reusing C1's roster +select_local+OpenAICompatAdapter(no duplicated LiteLLM/endpoint/key logic). MCP-free so it stays hermetically testable; failures surface to the orchestrator (no retry).tanglebrain/mcp_server.py: a thinFastMCPwrapper exposing the syncdelegate_localtool (its docstring is the orchestrator-facing contract). Console entrytanglebrain-delegateserves over stdio. Verified end-to-end: a real MCP stdio client calls the tool and gets gpt-oss text back.- The
mcpSDK is an optional dependency —pip install "tanglebrain[delegate]"; the core install stays lean (httpx + PyYAML). README documents per-CLI registration.
-
C2 — CLI adapters for the three subscription tools (claude / codex / gemini), with env-scrub. The subscription tier is now invocable end-to-end through the uniform
run(prompt, opts) -> textinterface.CliAdapter(tanglebrain/adapters/cli.py): runs a sub CLI as a subprocess (never via a shell) and returns its final text. Prompt injection is config-driven — a{prompt}token in the rostercmdis substituted (gemini's-p {prompt}), otherwise the prompt is appended as the final argument (claude, codex).- Env-scrub (§7), the safety-critical piece:
invoke.scrub_envstrips named vars from a copy of the environment handed to the subprocess (the parentos.environis never mutated), soclaude -puses its own authenticated session rather than the injectedANTHROPIC_API_KEY. Proven by a live test: claude reports the key asUNSET. - Output parsers selected per entry via a new
invoke.parseroster field:claude-json(single{"result": ...}object),gemini-json({"response": ...}), andplain(stripped stdout, for codexexec). Parsers were written against real captured CLI output. AdapterErrorpromoted totanglebrain/adapters/base.pyso the openai-compat and CLI adapters and the routing layer share one error type (re-exported fromopenai_compatfor backwards-compatible imports).selector.build_adapternow builds thecliadapter;selector.select_by_idplus a newtanglebrain --model <id>flag let a named sub be driven end-to-end. This is an explicit override, not the §6 frontier-first router (still C3).- Roster
cmdfor claude switched fromstream-jsonto--output-format json(a single parseable object). The gpt-oss MCP local-delegate (the other half of plan §10's C2 line) was split out to issue #4 (C2b), to land near C3 where it has a consumer.
- C1 — repo skeleton + roster loader + openai-compat adapter. One request now routes to
the free local tier (a local gpt-oss model via LiteLLM) end-to-end.
- Python package skeleton (
tanglebrain/),pyproject.toml,Makefile, andtests/following the project's conventions (stdlibunittest, venv-based test target,make lint/test). - Roster config loader (
tanglebrain/roster.py): parses the YAML roster into typed objects. The roster is config-driven and open-ended — adding a model is an entry edit, not a code change. The starting roster isgpt-oss-120b+ the three subscription CLIs. openai-compatadapter (tanglebrain/adapters/openai_compat.py) with the uniformrun(prompt, opts) -> textinterface, calling the local LiteLLM endpoint directly. Returns only the finalcontent(dropsreasoning_content); defaultsmax_tokensto 2048 per the C0 budget lesson. Resolves the scoped key via the contract'skey_ref.- Local-first selector (
tanglebrain/selector.py) and CLI entry point (tanglebrain/cli.py) wiring roster → local entry → adapter → text. - Brought the project's planning and design docs into this repo (the current architecture is
documented in
ARCHITECTURE.md). - Baseline hygiene files:
README,LICENSE,CHANGELOG,.gitignore.
- Python package skeleton (
- Resolved the two parked design decisions (PM, 2026-06-16; see issue #2 and plan §9.6–9.7):
paid-API billing will be gated by an explicit
api_billing_enabledflag (default off), with each paid key atier: apiroster entry carrying a per-key enable toggle + budget cap, fronted through LiteLLM (TangleBrain references a scoped virtual key — preferred over a raw provider key, which is not foreclosed but stays behind the toggle). Reconciled contract invariant #3 accordingly — it now softens, not reverses (the durable rule is no paid billing without the explicit toggle). No code behavior change yet; the paid-API tier itself is a later chunk (#2).