tanglebrain 0.23.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tanglebrain-0.23.0/LICENSE +21 -0
- tanglebrain-0.23.0/PKG-INFO +541 -0
- tanglebrain-0.23.0/README.md +504 -0
- tanglebrain-0.23.0/pyproject.toml +142 -0
- tanglebrain-0.23.0/setup.cfg +4 -0
- tanglebrain-0.23.0/tanglebrain/__init__.py +23 -0
- tanglebrain-0.23.0/tanglebrain/adapters/__init__.py +22 -0
- tanglebrain-0.23.0/tanglebrain/adapters/api.py +40 -0
- tanglebrain-0.23.0/tanglebrain/adapters/base.py +143 -0
- tanglebrain-0.23.0/tanglebrain/adapters/cli.py +366 -0
- tanglebrain-0.23.0/tanglebrain/adapters/openai_compat.py +422 -0
- tanglebrain-0.23.0/tanglebrain/atomic.py +130 -0
- tanglebrain-0.23.0/tanglebrain/classifier.py +99 -0
- tanglebrain-0.23.0/tanglebrain/cli.py +486 -0
- tanglebrain-0.23.0/tanglebrain/config/pricing.yaml +14 -0
- tanglebrain-0.23.0/tanglebrain/config/roster.yaml +132 -0
- tanglebrain-0.23.0/tanglebrain/config/settings.yaml +39 -0
- tanglebrain-0.23.0/tanglebrain/delegate.py +499 -0
- tanglebrain-0.23.0/tanglebrain/gui/__init__.py +10 -0
- tanglebrain-0.23.0/tanglebrain/gui/server.py +192 -0
- tanglebrain-0.23.0/tanglebrain/gui/static/index.html +446 -0
- tanglebrain-0.23.0/tanglebrain/gui/static/logo.png +0 -0
- tanglebrain-0.23.0/tanglebrain/gui/views.py +208 -0
- tanglebrain-0.23.0/tanglebrain/integrity.py +410 -0
- tanglebrain-0.23.0/tanglebrain/mcp_server.py +246 -0
- tanglebrain-0.23.0/tanglebrain/measurement.py +1349 -0
- tanglebrain-0.23.0/tanglebrain/roster.py +415 -0
- tanglebrain-0.23.0/tanglebrain/roster_edit.py +207 -0
- tanglebrain-0.23.0/tanglebrain/router.py +427 -0
- tanglebrain-0.23.0/tanglebrain/selector.py +125 -0
- tanglebrain-0.23.0/tanglebrain/serve/__init__.py +8 -0
- tanglebrain-0.23.0/tanglebrain/serve/server.py +257 -0
- tanglebrain-0.23.0/tanglebrain/serve/views.py +466 -0
- tanglebrain-0.23.0/tanglebrain/settings.py +132 -0
- tanglebrain-0.23.0/tanglebrain/totals.py +355 -0
- tanglebrain-0.23.0/tanglebrain.egg-info/PKG-INFO +541 -0
- tanglebrain-0.23.0/tanglebrain.egg-info/SOURCES.txt +64 -0
- tanglebrain-0.23.0/tanglebrain.egg-info/dependency_links.txt +1 -0
- tanglebrain-0.23.0/tanglebrain.egg-info/entry_points.txt +5 -0
- tanglebrain-0.23.0/tanglebrain.egg-info/requires.txt +10 -0
- tanglebrain-0.23.0/tanglebrain.egg-info/top_level.txt +1 -0
- tanglebrain-0.23.0/tests/test_api_adapter.py +119 -0
- tanglebrain-0.23.0/tests/test_atomic.py +124 -0
- tanglebrain-0.23.0/tests/test_bind_address.py +117 -0
- tanglebrain-0.23.0/tests/test_classifier.py +95 -0
- tanglebrain-0.23.0/tests/test_cli.py +805 -0
- tanglebrain-0.23.0/tests/test_cli_adapter.py +530 -0
- tanglebrain-0.23.0/tests/test_delegate.py +569 -0
- tanglebrain-0.23.0/tests/test_gui.py +717 -0
- tanglebrain-0.23.0/tests/test_gui_contrast.py +288 -0
- tanglebrain-0.23.0/tests/test_integrity.py +619 -0
- tanglebrain-0.23.0/tests/test_key_permissions.py +138 -0
- tanglebrain-0.23.0/tests/test_live.py +326 -0
- tanglebrain-0.23.0/tests/test_mcp_server.py +215 -0
- tanglebrain-0.23.0/tests/test_measurement.py +2211 -0
- tanglebrain-0.23.0/tests/test_model_pin_delegation.py +193 -0
- tanglebrain-0.23.0/tests/test_openai_compat.py +490 -0
- tanglebrain-0.23.0/tests/test_packaging.py +116 -0
- tanglebrain-0.23.0/tests/test_plugin_manifest.py +135 -0
- tanglebrain-0.23.0/tests/test_roster.py +270 -0
- tanglebrain-0.23.0/tests/test_roster_edit.py +244 -0
- tanglebrain-0.23.0/tests/test_router.py +716 -0
- tanglebrain-0.23.0/tests/test_selector.py +103 -0
- tanglebrain-0.23.0/tests/test_serve.py +752 -0
- tanglebrain-0.23.0/tests/test_settings.py +102 -0
- tanglebrain-0.23.0/tests/test_version.py +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Vaughan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tanglebrain
|
|
3
|
+
Version: 0.23.0
|
|
4
|
+
Summary: A local-first, config-driven LLM router across OpenAI-compatible backends you own.
|
|
5
|
+
Author: Jason Vaughan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jason-Vaughan/TangleBrain
|
|
8
|
+
Project-URL: Repository, https://github.com/Jason-Vaughan/TangleBrain
|
|
9
|
+
Project-URL: Changelog, https://github.com/Jason-Vaughan/TangleBrain/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/Jason-Vaughan/TangleBrain/issues
|
|
11
|
+
Project-URL: Releases, https://github.com/Jason-Vaughan/TangleBrain/releases
|
|
12
|
+
Keywords: llm,router,ollama,openai-compatible,litellm,local-llm
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: httpx<1,>=0.27
|
|
29
|
+
Requires-Dist: PyYAML<7,>=6.0
|
|
30
|
+
Provides-Extra: delegate
|
|
31
|
+
Requires-Dist: mcp<3,>=2; extra == "delegate"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy<2.4,>=2.3; extra == "dev"
|
|
35
|
+
Requires-Dist: types-PyYAML<7,>=6; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="https://raw.githubusercontent.com/Jason-Vaughan/project-assets/main/tanglebrain-logo-512.png" alt="TangleBrain logo" width="180">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
# TangleBrain
|
|
43
|
+
|
|
44
|
+
[](https://github.com/Jason-Vaughan/TangleBrain/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/tanglebrain/)
|
|
46
|
+
[](https://pypi.org/project/tanglebrain/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1-Click Installation (PyPI)
|
|
51
|
+
pip install tanglebrain
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A **local-first, config-driven router across OpenAI-compatible backends you own.**
|
|
55
|
+
|
|
56
|
+
## The Problem: Cloud-by-Default Routing
|
|
57
|
+
|
|
58
|
+
Most AI tooling sends **every** request to a paid cloud API by default — even the trivial ones, even
|
|
59
|
+
when you already run capable models on hardware you own. The spend accrues invisibly, you're coupled
|
|
60
|
+
to a single provider's endpoint, and the moment you want to blend a local model, an authenticated CLI
|
|
61
|
+
you already pay for, and a bring-your-own-key API, you end up hand-wiring glue and editing source just
|
|
62
|
+
to change *where* a request goes. There's no single, plain place to declare "here are the backends I
|
|
63
|
+
have — route across them, in this order," and no measurement of what you're actually spending versus
|
|
64
|
+
avoiding.
|
|
65
|
+
|
|
66
|
+
**This is routing debt: vendor lock-in, invisible spend, and routing logic that lives in code instead
|
|
67
|
+
of config.**
|
|
68
|
+
|
|
69
|
+
## The Solution: A Local-First Router You Own
|
|
70
|
+
|
|
71
|
+
TangleBrain keeps the whole roster of backends in one editable YAML file and routes each request to
|
|
72
|
+
the backend you've configured — a free local model server by default. It favors credentials you
|
|
73
|
+
already hold: your **local models** and your **authenticated, OAuth-logged-in tools** come first,
|
|
74
|
+
while **raw API keys stay a separate, explicitly-gated opt-in** rather than the default (it never
|
|
75
|
+
injects a key into a CLI — your tool uses its own session). An optional classifier can **read each
|
|
76
|
+
request and route by its complexity** — sending the grunt work to your free local model where it's
|
|
77
|
+
cheap, and reserving heavier backends for what actually needs them. And every routed task is logged
|
|
78
|
+
with an **estimated cloud-equivalent cost**, so you can see what you're spending versus avoiding.
|
|
79
|
+
Adding or removing a backend is a config edit, not a code change.
|
|
80
|
+
|
|
81
|
+
## Standalone, or part of the Tangle family
|
|
82
|
+
|
|
83
|
+
TangleBrain runs entirely on its own — clone it, point it at your backends, and go. It's MIT-licensed
|
|
84
|
+
and open to contributors: forks and pull requests are welcome. It's also designed to drop in
|
|
85
|
+
seamlessly alongside [TangleClaw](https://github.com/Jason-Vaughan/TangleClaw) and the wider **Tangle
|
|
86
|
+
family** of tools, so it works the same whether you run it solo or as part of that ecosystem.
|
|
87
|
+
|
|
88
|
+
**Status:** publicly released and actively developed — see the
|
|
89
|
+
[latest release](https://github.com/Jason-Vaughan/TangleBrain/releases) and [`CHANGELOG.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/CHANGELOG.md).
|
|
90
|
+
|
|
91
|
+
## What it does
|
|
92
|
+
|
|
93
|
+
- **Local-first routing** — ships pointing at a free local model server; nothing leaves your machine
|
|
94
|
+
unless you configure a backend that does.
|
|
95
|
+
- **OAuth- and local-first credentials** — prefers your local models and your authenticated
|
|
96
|
+
(OAuth-logged-in) tool sessions; it never injects an API key into a CLI. The raw-API-key tier is a
|
|
97
|
+
deliberate opt-in, off by default behind explicit gates.
|
|
98
|
+
- **Prompt-aware routing** — an optional classifier reads each request and sends trivial / grunt work
|
|
99
|
+
straight to your free local model where it's cheap, reserving heavier backends for what actually
|
|
100
|
+
needs them (off by default, fails safe).
|
|
101
|
+
- **Config-driven roster** — every routable backend is one entry in a plain YAML list; add, remove,
|
|
102
|
+
or reorganize backends by editing config.
|
|
103
|
+
- **Pluggable CLI-backed orchestration** — drive authenticated command-line tools as orchestrators,
|
|
104
|
+
with rotation and failover across them for resilience.
|
|
105
|
+
- **Multi-target sub-task delegation (scatter-gather)** — an orchestrator can decompose a task and
|
|
106
|
+
offload sub-tasks through MCP tools: to the free local backend, or to any configured backend **by id
|
|
107
|
+
or by capability** (a `good_at` tag), and **fan several out concurrently** in one call. Each
|
|
108
|
+
delegated sub-call is metered and linked back to the specific top-level task that spawned it, then
|
|
109
|
+
the orchestrator reviews and synthesises the results.
|
|
110
|
+
- **Cost measurement** — every routed task is logged with an estimated cloud-equivalent cost;
|
|
111
|
+
`tanglebrain --stats` rolls up what you've spent versus avoided.
|
|
112
|
+
- **Your prompts are not in the log** — text is measured, then discarded; only derived counts and
|
|
113
|
+
routing metadata are written. There is no redaction step to trust because nothing needing
|
|
114
|
+
redaction reaches the writer. The one qualification is on the error path, stated in full under
|
|
115
|
+
[Cost avoided](#cost-avoided-measurement).
|
|
116
|
+
- **Knob GUI** — a localhost panel to view the roster, pricing, and rollup, edit a focused set of
|
|
117
|
+
config knobs, and run a prompt.
|
|
118
|
+
- **Gated paid-API tier** — bring-your-own-key overflow, off by default behind two independent
|
|
119
|
+
switches.
|
|
120
|
+
|
|
121
|
+
See [`ARCHITECTURE.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/ARCHITECTURE.md) for how the pieces fit together, [`CHANGELOG.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/CHANGELOG.md)
|
|
122
|
+
for development history, and [`DISCLAIMER.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/DISCLAIMER.md) for the opt-in / bring-your-own-key
|
|
123
|
+
posture.
|
|
124
|
+
|
|
125
|
+
## Tiers
|
|
126
|
+
|
|
127
|
+
| Tier | Example | Default |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| **Free local** | a local model via Ollama / any OpenAI-compatible server you run | **active** |
|
|
130
|
+
| **Subscription / authenticated CLI** | command-line tools you've installed and logged in (e.g. `claude -p`, `codex exec`, `agy -p`) | opt-in (commented) |
|
|
131
|
+
| **Paid API** | bring-your-own-key overflow (any OpenAI-compatible endpoint you hold a key for) | opt-in, off by default |
|
|
132
|
+
|
|
133
|
+
> **Opt-in adapters & your responsibility.** The subscription / authenticated-CLI tier and the
|
|
134
|
+
> paid-API tier are **opt-in** — you enable them by editing your own roster. Driving an authenticated
|
|
135
|
+
> CLI is your responsibility under that provider's Terms of Service, and the paid tier is
|
|
136
|
+
> bring-your-own-key. Read [`DISCLAIMER.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/DISCLAIMER.md) before enabling either.
|
|
137
|
+
|
|
138
|
+
## Install
|
|
139
|
+
|
|
140
|
+
Requires Python ≥ 3.10.
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
pip install tanglebrain # from PyPI ("tanglebrain[delegate]" adds the MCP server)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Or for development, from a clone:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
make venv # create .venv and install -e . (dev deps included)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Use
|
|
153
|
+
|
|
154
|
+
The roster of routable backends is a plain, editable YAML list — adding or removing a backend is a
|
|
155
|
+
config edit, not a code change. The shipped
|
|
156
|
+
[`tanglebrain/config/roster.yaml`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/tanglebrain/config/roster.yaml) is only a **generic example** with
|
|
157
|
+
a single active entry (a local Ollama backend); keep your real roster **outside the repo** so updates
|
|
158
|
+
never clobber it. It's auto-discovered in order: `$TANGLEBRAIN_ROSTER` →
|
|
159
|
+
`~/.config/tanglebrain/roster.yaml` → the packaged example. Copy the example to
|
|
160
|
+
`~/.config/tanglebrain/roster.yaml` and edit it there (or pass `--roster <path>`).
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
# Route to the free local backend directly — works out of the box once a local server is running:
|
|
164
|
+
.venv/bin/tanglebrain --local "Write a haiku about local inference."
|
|
165
|
+
|
|
166
|
+
# Show the cost-avoided rollup for every task this machine has routed:
|
|
167
|
+
.venv/bin/tanglebrain --stats
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The default `tanglebrain "…"` (no `--local`) uses the **orchestrator router**. Since the packaged
|
|
171
|
+
roster ships with no active orchestrators, that path needs at least one opt-in backend enabled first
|
|
172
|
+
— see below. Until then, use `--local` for the local backend.
|
|
173
|
+
|
|
174
|
+
### Orchestrator routing (opt-in)
|
|
175
|
+
|
|
176
|
+
Enable one or more orchestrator backends by uncommenting an entry in your roster (subscription /
|
|
177
|
+
authenticated-CLI examples are provided, commented out, in the shipped roster) and reading
|
|
178
|
+
[`DISCLAIMER.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/DISCLAIMER.md) first. With at least one orchestrator active:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
# Default: route through an orchestrator. Rotates across the configured orchestrators and fails over
|
|
182
|
+
# on error; an orchestrator can offload sub-tasks to a configured backend (see "Delegate (MCP)"):
|
|
183
|
+
.venv/bin/tanglebrain "Refactor this module and add tests."
|
|
184
|
+
.venv/bin/tanglebrain --task code "Refactor this function for clarity." # task-fit hint
|
|
185
|
+
|
|
186
|
+
# Force a specific roster entry (explicit override, bypasses the router):
|
|
187
|
+
.venv/bin/tanglebrain --model my-backend "Summarize this long document."
|
|
188
|
+
|
|
189
|
+
# Opt into the local classifier gate for this run (trivial → local backend, else router):
|
|
190
|
+
.venv/bin/tanglebrain --gate "What's the capital of France?"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
An orchestrator is any roster entry flagged `can_orchestrate: true`. The router prefers an
|
|
194
|
+
orchestrator whose `good_at` matches the `--task` hint, rotates across the eligible set for
|
|
195
|
+
resilience, and on an error fails over to the next; if all fail it reports each failure.
|
|
196
|
+
|
|
197
|
+
### Classifier gate (optional, off by default)
|
|
198
|
+
|
|
199
|
+
By default every (non-`--local`) request goes through the router. You can put a **cheap local
|
|
200
|
+
classifier in front**: it rates each request's complexity on the local backend and sends **trivial**
|
|
201
|
+
work straight to the local backend, while **frontier** work falls through to the router. Enable it
|
|
202
|
+
persistently with `classifier_gate_enabled: true` in
|
|
203
|
+
[`tanglebrain/config/settings.yaml`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/tanglebrain/config/settings.yaml), or per run with `--gate` /
|
|
204
|
+
`--no-gate`. It is **off by default** and **fails safe** — any classifier error or ambiguity routes
|
|
205
|
+
to frontier, so a hard task is never trapped on the local tier. (Fail-safe covers the
|
|
206
|
+
*classification*; a trivial-classified task that then fails to execute on local surfaces that error,
|
|
207
|
+
the same as `--local` — it doesn't silently re-route.) Gated runs show up as `gate-local` in
|
|
208
|
+
`--stats`. If the gate ever seems to route *everything* to frontier, the classify call is likely
|
|
209
|
+
truncating — raise its token budget.
|
|
210
|
+
|
|
211
|
+
### Cost avoided (measurement)
|
|
212
|
+
|
|
213
|
+
Every routed task is logged as one JSON line in an append-only usage log
|
|
214
|
+
(`~/.local/share/tanglebrain/usage.jsonl`, or under `XDG_DATA_HOME` /
|
|
215
|
+
`TANGLEBRAIN_STATE_DIR`): path, tier, model,
|
|
216
|
+
estimated tokens, and the **cloud-equivalent cost it avoided** — what the work would have cost on a
|
|
217
|
+
paid frontier API.
|
|
218
|
+
|
|
219
|
+
`tanglebrain --stats` reports a **lifetime** figure for **this machine**, summed from two files:
|
|
220
|
+
`totals.json` beside the log holds permanent aggregates, and the log holds the recent rows.
|
|
221
|
+
Compaction folds the oldest rows into the totals and then drops them, so the log stays a bounded
|
|
222
|
+
window without the lifetime figure shrinking as it shrinks. It runs on a size cap (5 MiB, roughly
|
|
223
|
+
15,000 records) — no maintenance, and nothing to schedule. Delete either file and the figure falls
|
|
224
|
+
back to whatever the other one holds — a smaller number, never an error. When the store cannot do
|
|
225
|
+
its job at all — a log that is not writable, or a `totals.json` that is present and unreadable —
|
|
226
|
+
`--stats` prints a `⚠ measurement:` line beside the figure, so a short number is never presented as
|
|
227
|
+
a confident one; what it checks, and the limits of that check, are in
|
|
228
|
+
[`docs/design/observability.md`](docs/design/observability.md) § Store health.
|
|
229
|
+
|
|
230
|
+
**Lifetime, not fleet-wide.** Each machine keeps its own log and totals, and nothing merges them —
|
|
231
|
+
merging is a decided non-goal rather than a missing feature, because a combined figure would need
|
|
232
|
+
stable machine identity, cross-host de-duplication and a conflict rule for compaction running
|
|
233
|
+
independently on each. A second machine starting at zero is the design working. If you do want a
|
|
234
|
+
combined view, the format is one JSON object per line so the logs concatenate. Read the result
|
|
235
|
+
under a throwaway state root rather than writing it back over a live log:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
mkdir -p /tmp/merged
|
|
239
|
+
cat machine-a/usage.jsonl machine-b/usage.jsonl > /tmp/merged/usage.jsonl
|
|
240
|
+
TANGLEBRAIN_STATE_DIR=/tmp/merged .venv/bin/tanglebrain --stats
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Use `TANGLEBRAIN_STATE_DIR` specifically, not `XDG_DATA_HOME`.** Every entry point migrates a
|
|
244
|
+
pre-0.21 cache-tier state root forward before it reads anything, `--stats` included.
|
|
245
|
+
`TANGLEBRAIN_STATE_DIR` is the one override the migration reads too, so it resolves source and
|
|
246
|
+
destination to the same directory and copies nothing; point `XDG_DATA_HOME` at a scratch root
|
|
247
|
+
instead and the migration copies `~/.cache/tanglebrain` into it, so the "merged" view silently
|
|
248
|
+
carries a third machine's history. See
|
|
249
|
+
[`docs/design/operations.md`](docs/design/operations.md) for what that view does and does not cover.
|
|
250
|
+
|
|
251
|
+
Tokens are *estimated* with a uniform `chars/4` heuristic over the visible prompt + response — the
|
|
252
|
+
authenticated CLIs expose no usable token counts, so one consistent (if approximate) methodology is
|
|
253
|
+
applied to every tier. The reference frontier price lives in
|
|
254
|
+
[`tanglebrain/config/pricing.yaml`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/tanglebrain/config/pricing.yaml) — tune it to whatever frontier
|
|
255
|
+
model you want to compare against. A `placeholder` flag makes the rollup render a PLACEHOLDER caveat
|
|
256
|
+
when the rates are rough. Logging is best-effort and never affects the returned answer.
|
|
257
|
+
|
|
258
|
+
**What the log never contains: your prompts, and the model's replies.** Each task's text is run
|
|
259
|
+
through the `chars/4` estimate and then discarded — only the derived counts and the routing metadata
|
|
260
|
+
are written. That is what makes the log safe to keep forever and safe to render in a browser. It is
|
|
261
|
+
a property of the shape of the code rather than a filter you have to trust: there is no redaction
|
|
262
|
+
step because nothing that would need redacting ever reaches the writer.
|
|
263
|
+
|
|
264
|
+
**The exception, stated plainly: backend error messages.** When a backend fails, the diagnostic
|
|
265
|
+
explaining why is persisted next to the failed attempt, because a router that hides why it fell back
|
|
266
|
+
is not debuggable. Every one of those messages that could carry a model's reply now reports the
|
|
267
|
+
reply's *shape* — `52 chars of text`, `object with keys ['result', 'subtype']` — instead of quoting
|
|
268
|
+
it, and a test drives a real malformed response onto disk to prove nothing of it survives.
|
|
269
|
+
|
|
270
|
+
Two things are still kept as-is, deliberately. **Strings only a provider or a CLI produces** — an
|
|
271
|
+
HTTP error body, a stream's error envelope, a failed subprocess's stderr — stay readable, because
|
|
272
|
+
rendering `invalid api key` as `object with keys ['error']` would gut the diagnostic for the single
|
|
273
|
+
most common setup failure there is. Those are error metadata rather than model output, but an
|
|
274
|
+
upstream that echoes your request back inside one — a 400, a content-filter rejection — would put
|
|
275
|
+
that text in the log; that turns on what the provider returns, not on TangleBrain. And **the shape
|
|
276
|
+
summary itself has to guess once**: it names an object's keys when they look like schema, so a key
|
|
277
|
+
that is a single identifier-shaped token is reproduced rather than counted, and that one can come
|
|
278
|
+
from the model's own reply. Telling a schema field name from content is not decidable, so it is a
|
|
279
|
+
judgement by construction rather than something a later fix closes.
|
|
280
|
+
[`docs/design/security-model.md`](docs/design/security-model.md) enumerates both and is the
|
|
281
|
+
authoritative accounting.
|
|
282
|
+
|
|
283
|
+
**Editing the price never restates history.** Each task is priced when it runs and keeps that
|
|
284
|
+
figure, so tuning `pricing.yaml` cannot retroactively inflate what you have already saved. The
|
|
285
|
+
`Pricing ref:` line names the revision a figure was computed under; once your history spans an
|
|
286
|
+
edit it reports how many revisions it spans instead of asserting one.
|
|
287
|
+
|
|
288
|
+
### Knob panel (`tanglebrain-gui`)
|
|
289
|
+
|
|
290
|
+
A thin **localhost-only** web panel over the config — zero extra dependencies (stdlib `http.server`
|
|
291
|
+
+ a single vanilla HTML/CSS/JS page):
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
.venv/bin/tanglebrain-gui # serves http://127.0.0.1:3250/ (Ctrl-C to stop)
|
|
295
|
+
.venv/bin/tanglebrain-gui --port 3260 # override the port if 3250 is busy
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The panel has two views, switched from the sidebar and addressable by fragment. **Chat**
|
|
299
|
+
(`#/chat`, where it opens) lets you **run a prompt** through the router, showing which tier/model
|
|
300
|
+
served it. **Settings** (`#/settings`) holds the knobs: the cost-avoided rollup, the roster, and the
|
|
301
|
+
pricing reference. The **pricing card is
|
|
302
|
+
editable** — change the rates / reference label / placeholder flag and Save; it writes the tracked
|
|
303
|
+
`tanglebrain/config/pricing.yaml` (strict validation, atomic write, a backup to the state dir, and
|
|
304
|
+
the methodology header preserved), so the edit is git-visible for you to commit. The **roster is
|
|
305
|
+
editable for a focused set of per-entry fields** — `enabled`, `can_orchestrate`, `budget_usd_month`,
|
|
306
|
+
and `good_at` (each row has its own Save). Edits are surgical and **comment-preserving**: only the
|
|
307
|
+
targeted value on the targeted line changes, so the curated inline comments and the nested `invoke`
|
|
308
|
+
block survive byte-for-byte (same validate → backup → atomic-write safety as pricing; the candidate
|
|
309
|
+
is re-parsed before any write). Adding/removing entries and editing the `invoke` block are still
|
|
310
|
+
hand-edits. The panel binds `127.0.0.1` only: running a prompt spends real backend quota and it reads
|
|
311
|
+
the roster, so it is never network-exposed. The roster view shows each entry's `key_ref` as the
|
|
312
|
+
reference string only — secrets are never resolved or sent to the browser.
|
|
313
|
+
|
|
314
|
+
### Server mode (`tanglebrain-serve`) — the router as an OpenAI-compatible endpoint
|
|
315
|
+
|
|
316
|
+
`tanglebrain-serve` exposes the same routing path the CLI uses as a local
|
|
317
|
+
**OpenAI-compatible endpoint**, so any OpenAI-compat consumer (an agent framework, an engine
|
|
318
|
+
config, a plain `openai` client) can point its `base_url` at TangleBrain and pick a *routing
|
|
319
|
+
strategy* instead of a model. Zero extra dependencies (stdlib `http.server`):
|
|
320
|
+
|
|
321
|
+
```sh
|
|
322
|
+
.venv/bin/tanglebrain-serve # serves http://127.0.0.1:3251/v1 (Ctrl-C to stop)
|
|
323
|
+
.venv/bin/tanglebrain-serve --port 3261 # override the port if 3251 is busy
|
|
324
|
+
|
|
325
|
+
curl -s http://127.0.0.1:3251/v1/chat/completions \
|
|
326
|
+
-H 'Content-Type: application/json' \
|
|
327
|
+
-d '{"model": "auto", "messages": [{"role": "user", "content": "Refactor this module."}]}'
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The `model` param is a **routing directive**:
|
|
331
|
+
|
|
332
|
+
| `model` | Behavior |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `auto` (or absent) | The full router — identical to a bare `tanglebrain "…"` run (classifier gate honored per settings). |
|
|
335
|
+
| a roster entry id | Explicit pin, parity with `--model <id>`. |
|
|
336
|
+
| anything else | A clear `model_not_found` error — never a silent fallback. |
|
|
337
|
+
|
|
338
|
+
`GET /v1/models` lists `auto` plus every roster id. Chat `messages` arrays are flattened to a
|
|
339
|
+
role-tagged transcript for the serving backend; text content parts are concatenated, and non-text
|
|
340
|
+
parts (images) are rejected loudly rather than dropped. `max_tokens` is honored; sampling knobs
|
|
341
|
+
(`temperature`, `top_p`, …) and tool definitions are accepted and ignored — the serving backend
|
|
342
|
+
controls its own generation, and orchestrator CLIs bring their own tools. The response's `model`
|
|
343
|
+
field reports **which backend actually served**; the requested directive and routing detail ride
|
|
344
|
+
in a `tanglebrain` extension field, and `usage` carries the same `chars/4` estimate the
|
|
345
|
+
measurement log uses (served requests are metered exactly like CLI runs).
|
|
346
|
+
|
|
347
|
+
Served requests are attributed in the usage log: each record carries `origin: "serve"` (CLI runs
|
|
348
|
+
tag `cli`, panel runs `gui`), and `tanglebrain --stats` shows the per-origin split. A caller can
|
|
349
|
+
additionally send an optional `X-TangleBrain-Parent-Task` header carrying its own task/session
|
|
350
|
+
identity — trimmed, capped at 128 chars, recorded onto the usage record as `parent_task_id` for
|
|
351
|
+
cross-system attribution, and never routed on. The reverse linkage already exists: the response's
|
|
352
|
+
completion id is `chatcmpl-<task_id>`, the same task id the usage record carries.
|
|
353
|
+
|
|
354
|
+
Caveats, by design:
|
|
355
|
+
|
|
356
|
+
- **Streaming is real where the backend can stream.** `stream: true` delivers incremental
|
|
357
|
+
`chat.completion.chunk` deltas from backends that stream (a pinned `openai-compat`/`api`
|
|
358
|
+
entry, or the classifier-gate's local path). Backends that can't — subprocess CLIs, and the
|
|
359
|
+
full-router `auto` path they serve — still work but deliver the completed response as a
|
|
360
|
+
single chunk, which can take minutes when an orchestrator CLI serves the request; set
|
|
361
|
+
generous client read timeouts for those. A stream that dies mid-way ends with an in-stream
|
|
362
|
+
`{"error": ...}` event and no `[DONE]` terminator, never a fake `finish_reason: stop`.
|
|
363
|
+
- **Localhost-only, keyless.** The endpoint binds `127.0.0.1` (not configurable) and **ignores
|
|
364
|
+
the `Authorization` header** — local callers need no key, and a client that insists on sending
|
|
365
|
+
a dummy bearer works as-is. It is deliberately never network-exposed: a request spends real
|
|
366
|
+
backend quota, and real money if the paid-API gates are on. Those gates are unchanged — server
|
|
367
|
+
mode adds no paid path that the CLI doesn't have. POSTs must send
|
|
368
|
+
`Content-Type: application/json` (every OpenAI client does; this also keeps no-preflight
|
|
369
|
+
cross-origin browser requests from ever reaching routing).
|
|
370
|
+
|
|
371
|
+
### Delegate (MCP) — let an orchestrator offload sub-tasks to a configured backend
|
|
372
|
+
|
|
373
|
+
`tanglebrain-delegate` is an MCP server that lets an orchestrator offload bulk sub-tasks instead of
|
|
374
|
+
running them itself, then review the results — a decompose → delegate → review loop that is emergent
|
|
375
|
+
from the orchestrator simply having the tool (no graph engine required). It reuses the same roster +
|
|
376
|
+
adapters as the CLI above, so endpoints and keys live in one place. It exposes four tools:
|
|
377
|
+
|
|
378
|
+
- **`delegate_local(prompt, max_tokens?)`** — route a sub-task to the free local tier (the $0
|
|
379
|
+
default).
|
|
380
|
+
- **`delegate(prompt, target?, task?, max_tokens?)`** — route a sub-task to a *configured* backend,
|
|
381
|
+
two ways (precedence: `target` > `task` > local):
|
|
382
|
+
- **`target`** — an explicit roster id flagged `can_delegate: true`. The orchestrator names the
|
|
383
|
+
exact backend.
|
|
384
|
+
- **`task`** — a capability tag (a `good_at` value, e.g. `code`). TangleBrain picks the **cheapest
|
|
385
|
+
`can_delegate` backend** good_at it (`local` before `sub`); the orchestrator just says *what kind
|
|
386
|
+
of work it is* and doesn't need to know ids. **Paid `api` backends are never auto-selected by
|
|
387
|
+
`task`** (reach one only by naming it as `target`). If nothing fits, the tool hands the sub-task
|
|
388
|
+
**back to the orchestrator to do itself** — not an error, just a signal that it's the most capable
|
|
389
|
+
backend available.
|
|
390
|
+
|
|
391
|
+
A target is invoked as a leaf (it never gets its own delegate tool — no recursion); `api` targets
|
|
392
|
+
named explicitly still obey the billing gate, so a paid target raises rather than spending while
|
|
393
|
+
billing is off.
|
|
394
|
+
- **`delegate_many(tasks, max_concurrency?)`** — fan **several sub-tasks out concurrently** in one
|
|
395
|
+
call and collect them, instead of delegating one at a time. Each item is `{prompt, target?, task?,
|
|
396
|
+
max_tokens?}` (same routing as `delegate`), so a batch can mix backends. Returns a JSON array, one
|
|
397
|
+
entry per task **in input order**, each `{index, status}` — `ok` (+`text`), `no_fit` (+`message`),
|
|
398
|
+
or `error` (+`error`); a failing sub-task never sinks the others. Concurrency is bounded
|
|
399
|
+
automatically from the host (`os.cpu_count()`), overridable by the operator
|
|
400
|
+
(`delegate_max_concurrency` in `settings.yaml` — pin it to your backend's real parallelism, e.g.
|
|
401
|
+
`OLLAMA_NUM_PARALLEL`) and lowerable per call. Dispatch + collect only — the orchestrator
|
|
402
|
+
synthesises the results.
|
|
403
|
+
- **`delegate_targets()`** — list the configured targets (`id`, `tier`, `good_at`, `cost`, `kind`)
|
|
404
|
+
so the orchestrator can decide based on what's available. The `delegate` tool's description also
|
|
405
|
+
enumerates them (built at server startup; the tool reflects the live roster).
|
|
406
|
+
|
|
407
|
+
Make a backend a delegate target by flagging its roster entry `can_delegate: true` (mirrors
|
|
408
|
+
`can_orchestrate`). The shipped example flags the local tier, so the menu is non-empty out of the
|
|
409
|
+
box. **Delegated sub-calls are now metered**: each is logged as a `kind: delegate` usage record, and
|
|
410
|
+
`tanglebrain --stats` (and the knob panel) show a "Delegated sub-tasks" breakdown by backend (count,
|
|
411
|
+
est tokens, informational cloud-equiv). These are kept **out of** the "spend avoided" headline so a
|
|
412
|
+
sub-call's saving is never double-counted against its parent task. Any non-local target is opt-in and
|
|
413
|
+
your responsibility under that provider's terms — see [DISCLAIMER.md](https://github.com/Jason-Vaughan/TangleBrain/blob/main/DISCLAIMER.md).
|
|
414
|
+
|
|
415
|
+
**Synthesising fan-out results.** The full pattern is decompose → fan out (`delegate_many`) →
|
|
416
|
+
**reduce** → answer. TangleBrain ships the dispatch primitives but deliberately does *not* own the
|
|
417
|
+
reduce step: the orchestrator gets the results array back and combines it itself, because it holds
|
|
418
|
+
the original task context that makes for good synthesis — something a fresh reduce backend lacks. If
|
|
419
|
+
the reduction is instead **mechanical and large** (concatenating generated files, merging many
|
|
420
|
+
summaries into one list — where the original intent doesn't matter), offload that stitch too with a
|
|
421
|
+
normal `delegate(prompt="Combine these results: …", task="summarization")` call, keeping the heavy
|
|
422
|
+
formatting off your frontier budget. No separate "reduce" tool is needed — the existing `delegate`
|
|
423
|
+
covers it.
|
|
424
|
+
|
|
425
|
+
It needs the optional `mcp` dependency:
|
|
426
|
+
|
|
427
|
+
```sh
|
|
428
|
+
pip install -e ".[delegate]" # or: make venv (installs the extra)
|
|
429
|
+
tanglebrain-delegate # serve over stdio (for a manual smoke test)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Register it with an orchestrator CLI (exact flags vary by CLI version — check `<cli> mcp --help`):
|
|
433
|
+
|
|
434
|
+
```sh
|
|
435
|
+
# Claude Code:
|
|
436
|
+
claude mcp add tanglebrain-delegate -- tanglebrain-delegate
|
|
437
|
+
# Codex: add a stdio MCP server entry pointing at `tanglebrain-delegate` in its MCP config.
|
|
438
|
+
# Antigravity CLI (`agy`, the gemini CLI's successor): no per-invocation MCP flags as of
|
|
439
|
+
# 1.0.10 — antigravity orchestrates without the delegate for now (TangleBrain #81).
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Claude Code users can instead install it as a **plugin** — this repo is its own plugin marketplace
|
|
443
|
+
(see [`plugins/tanglebrain-delegate/`](https://github.com/Jason-Vaughan/TangleBrain/tree/main/plugins/tanglebrain-delegate)):
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
/plugin marketplace add Jason-Vaughan/TangleBrain
|
|
447
|
+
/plugin install tanglebrain-delegate@tanglebrain
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The plugin registers the server declaratively — it wires the `tanglebrain-delegate` command, it
|
|
451
|
+
doesn't vendor it — so the console script must be on `PATH`: `pip install "tanglebrain[delegate]"`
|
|
452
|
+
from PyPI, or `pip install -e ".[delegate]"` from a clone as above.
|
|
453
|
+
|
|
454
|
+
To point the server at a non-default roster, set `TANGLEBRAIN_ROSTER=/path/to/roster.yaml` in its
|
|
455
|
+
environment.
|
|
456
|
+
|
|
457
|
+
### Paid-API tier (opt-in, off by default)
|
|
458
|
+
|
|
459
|
+
Paid API is the genuine last resort — it costs real money, so it is **disabled by default** and
|
|
460
|
+
gated by a single explicit switch. A `tier: api` roster entry parses and is inspectable at all
|
|
461
|
+
times, but it is **never routable** until you turn it on. See [`DISCLAIMER.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/DISCLAIMER.md) for
|
|
462
|
+
the bring-your-own-key posture.
|
|
463
|
+
|
|
464
|
+
The durable rule: *no paid billing without the explicit toggle.* Two independent gates must both be
|
|
465
|
+
on for a paid entry to build:
|
|
466
|
+
|
|
467
|
+
1. **Global gate** — `api_billing_enabled: true` in `tanglebrain/config/settings.yaml` (ships
|
|
468
|
+
`false`).
|
|
469
|
+
2. **Per-entry switch** — `enabled: true` on the roster entry (a per-key kill-switch).
|
|
470
|
+
|
|
471
|
+
Custody is **by reference, never embedding**: TangleBrain never holds a raw key in config —
|
|
472
|
+
`key_ref` points at an env var (`env:OPENAI_API_KEY`) or a `0600` key file
|
|
473
|
+
(`file:~/.config/tanglebrain/keys/paid.key`). Prefer fronting paid APIs through a budget-capped
|
|
474
|
+
gateway or a scoped key so spend is bounded **at the source**. A paid entry also records
|
|
475
|
+
`budget_usd_month` for visibility — TangleBrain does **not** enforce spend; cap it at your
|
|
476
|
+
gateway/provider. A commented example entry is at the bottom of `tanglebrain/config/roster.yaml`.
|
|
477
|
+
|
|
478
|
+
Once both gates are on, a paid entry runs either when selected explicitly (`--model <id>`) or as the
|
|
479
|
+
router's **genuine last resort** — the default `tanglebrain "…"` router falls through to an enabled
|
|
480
|
+
`api` entry only after *every* orchestrator has failed/exhausted. It tries paid entries in roster
|
|
481
|
+
order and never paid-routes a roster that has no orchestrators to exhaust first.
|
|
482
|
+
|
|
483
|
+
> **Live status:** the paid tier is **hermetically tested but never run against a real paid
|
|
484
|
+
> endpoint** — by design (TangleBrain is deliberately bring-your-own-key; we don't mint billable keys
|
|
485
|
+
> just to test). The hooks are in place and the routing/gating/visibility are proven; the live
|
|
486
|
+
> `router → ApiAdapter → key → provider` round-trip is unverified until an operator wires a real key.
|
|
487
|
+
> See [#23](https://github.com/Jason-Vaughan/TangleBrain/issues/23). Treat it as hermetically correct
|
|
488
|
+
> but live-unproven, and file a fix if a live provider needs one.
|
|
489
|
+
|
|
490
|
+
#### Runbook — enabling a paid key
|
|
491
|
+
|
|
492
|
+
1. **Get a key for any OpenAI-compatible endpoint you control** — a provider directly, OpenRouter, or
|
|
493
|
+
a self-hosted gateway (e.g. LiteLLM). Prefer a **budget-capped / scoped** key so spend is bounded
|
|
494
|
+
at the source; TangleBrain doesn't enforce spend itself.
|
|
495
|
+
2. **Store it outside the repo.** Reference an env var (`key_ref: env:OPENAI_API_KEY`) or a `0600`
|
|
496
|
+
file (`*.key` is gitignored):
|
|
497
|
+
```sh
|
|
498
|
+
install -m 600 /dev/stdin ~/.config/tanglebrain/keys/paid.key <<< 'sk-your-key'
|
|
499
|
+
```
|
|
500
|
+
3. **Add the roster entry** (uncomment/adapt the example at the bottom of your roster): `tier: api`,
|
|
501
|
+
`invoke.kind: api`, `base_url` = your endpoint, `model` = the model id it exposes,
|
|
502
|
+
`key_ref` = the env/file reference above, `enabled: true`, and `budget_usd_month: 25`
|
|
503
|
+
(display-only — match what you capped at the source).
|
|
504
|
+
4. **Flip the global gate**: set `api_billing_enabled: true` in `tanglebrain/config/settings.yaml`.
|
|
505
|
+
5. **Verify** in the knob panel (`tanglebrain-gui`): open **Settings** in the sidebar — the panel
|
|
506
|
+
opens on Chat — and the roster card there shows a **Paid-API billing: ON** banner and the entry's
|
|
507
|
+
`budget: $25.00/mo` note; or run `tanglebrain --model <id> "…"` for an explicit paid call. To pause spend without editing keys, set the entry's `enabled: false` (a
|
|
508
|
+
per-key kill-switch) or flip the global gate back to `false`.
|
|
509
|
+
|
|
510
|
+
## Develop
|
|
511
|
+
|
|
512
|
+
```sh
|
|
513
|
+
make help # list targets
|
|
514
|
+
make lint # ruff (lint) + mypy (type-check)
|
|
515
|
+
make test # lint + type-check + the unit test suite (hermetic; HTTP is mocked)
|
|
516
|
+
make test-live # opt-in: hit the real local endpoint your roster points at, end-to-end
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
There is deliberately no formatter — see [CONTRIBUTING.md](CONTRIBUTING.md) for what that means
|
|
520
|
+
when you send a patch, and `docs/design/nonfunctional-requirements.md` for why.
|
|
521
|
+
|
|
522
|
+
## Design documents
|
|
523
|
+
|
|
524
|
+
The reasoning behind the system — what each surface promises, what data survives a crash, what's
|
|
525
|
+
protected and what's accepted risk — is published in
|
|
526
|
+
[`docs/design/`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/docs/design/README.md).
|
|
527
|
+
|
|
528
|
+
These are deliberately candid: each document names its own gaps, and every gap named there links to
|
|
529
|
+
a tracking issue. If you're evaluating whether to depend on TangleBrain, or looking for somewhere to
|
|
530
|
+
start contributing, that directory is the honest picture.
|
|
531
|
+
|
|
532
|
+
## Contributing
|
|
533
|
+
|
|
534
|
+
Contributions are welcome — see [`CONTRIBUTING.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/CONTRIBUTING.md) for dev setup, branch/PR
|
|
535
|
+
conventions, and good first contributions (adding a backend is usually a config edit, not a code
|
|
536
|
+
change). All participation is governed by our [`CODE_OF_CONDUCT.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/CODE_OF_CONDUCT.md), and the
|
|
537
|
+
opt-in / bring-your-own-key posture is in [`DISCLAIMER.md`](https://github.com/Jason-Vaughan/TangleBrain/blob/main/DISCLAIMER.md).
|
|
538
|
+
|
|
539
|
+
## License
|
|
540
|
+
|
|
541
|
+
[MIT](https://github.com/Jason-Vaughan/TangleBrain/blob/main/LICENSE).
|