Repository navigation
v0.21.0 — measurement store split, supply-chain bounds, panel accessibility
TangleBrain 0.21.0 — the first release since 2026-08-01, covering 25 commits.
Upgrade note: nothing in this release changes how an existing install behaves on upgrade. The measurement store is now two files (totals.json beside usage.jsonl) and compaction can fold old rows into the permanent half, but nothing triggers compaction yet — the size cap that invokes it is still to come (#101).
If you use the delegate extra: this release requires mcp >= 2, < 3. Installs that need mcp 1.x should stay on 0.20.1, the last release that allowed it.
Added
-
Old rows fold into the lifetime totals, and the totals are written first — a compaction that
adds the oldest rows intototals.jsonand only then removes them fromusage.jsonl. This is
the write half of the measurement store; nothing triggers it automatically yet, so no install
changes behaviour on upgrade.The order of the two writes is the whole feature. Interrupted between them, a compaction
leaves rows counted in both halves and the figure reads too large. The opposite order would drop
rows before anything recorded them — a smaller figure, no evidence, nothing left to recompute
from. Over-counting is a bug; under-counting is the loss of the only claim this product makes
about itself, so both directions are pinned by tests that force the interruption rather than
waiting for one. What the ordering does not buy: nothing is persisted to mark which rows were
folded, so an inflated figure is not attributable and does not correct itself — the automatic
trigger has to settle whether a watermark is owed before an unattended crash can inflate it.Writes are durable, not merely atomic: the staging file is fsynced before the rename and the
directory after, becauseos.replaceorders the two writes against a killed process but not
against a power loss. A totals file that is present but unparseable is not folded onto —
reading it as zeros is right for a rollup, which renders, and wrong for a writer, which destroys,
so compaction refuses and leaves both halves on disk.The fold runs the same summation the read path runs, so a figure cannot change merely because
rows moved across the seam — a property test asserts exact equality across generated logs, and
across six successive folds with appends between them. Rows that stay are copied
through unparsed, so a field added by a newer TangleBrain, or a line left torn by an interrupted
append, survives the rewrite that keeps it.Writing the totals also completes their forward-compatibility contract, which until now existed
only on the read side. Ignoring an unknown key on read would have deleted it on write, so the
first compaction run by an older TangleBrain would have permanently destroyed fields a newer one
wrote. The writer now carries unrecognised fields through at any depth. Carrying is not
maintaining — an older version cannot add the folded rows to a field it does not know, so such a
value goes stale rather than being lost — and that limit is stated indata-model.mdand
boundaries.mdrather than implied.A process-level lock covers the whole read-fold-truncate. It cannot cover a second TangleBrain
process: an append during a compaction is written to the file being replaced and lost, and two
overlapping compactions read the same stored totals so the later write discards the earlier fold.
Accepted for a single-operator local tool and written down: an advisory file lock would hold on
POSIX only, and a guarantee that silently does not hold on a supported platform is worse than a
stated limitation. -
Lifetime totals are a file of their own —
totals.json, written beside the usage log in the
state root and read bytanglebrain --statsand the GUI panel. The spend-avoided headline is now
the sum of stored lifetime aggregates and the per-task rows still on disk, rather than a re-read
of every row ever written. This was the read half; the fold that writes it is the entry above.
An absent file reads as zeros, so on a log that has never been compacted the figure is
byte-for-byte the figure of the previous release — verified against the live 36-record log, where
every shared figure is identical and only the two labels below differ.Splitting the two is what lets a later release bound the row window without the lifetime claim
shrinking underneath it — and it is what makes a wrong figure attributable, since a discrepancy is
then either in the stored totals or in the window, never diffused across both. The format carries
every question--statsasks of it, including the set of pricing revisions the figure spans:
that evidence lives per-row today and compaction destroys it, so it is captured at the format's
birth rather than retrofitted once the rows are gone. The live log already spans two revisions.Forward compatibility is the same contract the usage record honours — an unknown key is ignored,
a missing key reads as zero — and for the same reason: several versions of TangleBrain write this
store over its life and an append-only store has no migration story. There is deliberately no
schema-version field; a version number advertises that a breaking revision is possible, and the
additive-only contract exists so that one never has to be. An absent, truncated, or corrupt file
degrades to a smaller number, never to an error. -
The install contract is published as data — an
installReferencekey in
.claude-plugin/marketplace.jsonnaming the marketplace entry (ref: "main",
autoUpdate: true), the plugin key to enable, and the console script that must be present.
Until now the only statement of how TangleBrain should be installed was prose inREADME.md,
and prose is not checkable: the entry/plugin marketplace addactually wrote carries noref
and noautoUpdate, a shape that reads as governed, resolves to nothing wherever the plugin is
not already cached, and let a local checkout sit four releases behind in silence. An install
monitor caught the incomplete entry but could not repair it, because there was nothing canonical
to repair it to.tests/test_plugin_manifest.pyderives every field from the thing it describes rather than
restating it — the marketplace key from the manifest's own name, the enabled-plugin key from both
manifests, the required command frompyproject.toml's console scripts, the repo from the
plugin's homepage. A confidently wrong install reference is worse than none, because a reader who
trusts it stops looking for a better source. -
A written deprecation policy —
docs/design/deprecation-policy.md.
The policy existed in practice (usage-log fields additive-by-default,--routeretained as an
accepted no-op) but had never been stated, so it could not be relied on from outside the repo or
applied consistently inside it. It covers the four consumer-facing surfaces — CLI flags, the MCP
tool surface, the usage-log record shape, and the two HTTP surfaces — and states the announcement
path and horizon (deprecated in N, not removed before N+2, never in a patch).Two things it settles that were genuinely open. Pre-1.0 honesty: semver says
0.y.zowes
users nothing, so the document separates what semver permits (everything) from what TangleBrain
commits to anyway, and says plainly that those commitments are policy rather than semver until
1.0. Dependency floors: raising a major floor is a breaking change even when no TangleBrain
code changes, because the user's install resolves differently. Users on the older major are not
stranded but pinned — every previous release stays on PyPI, and the CHANGELOG entry names the
last release that allowed the old major rather than leaving it in an issue thread.It also draws a line it does not cross: whether a new major is worth adopting given its
transitive dependency surface is weighed on the issue that makes the call, not pre-authorized by
the policy. The policy governs how a floor is announced, not whether to raise it. -
key_ref: file:PATHnow warns when the credential file is group- or world-readable.
ARCHITECTURE.mddescribes the intended posture as a0600file and nothing verified it, so
a world-readable key was read in silence. Resolution now stats the file first and writes a
warning to stderr naming the path and its octal mode. It warns rather than fails,
deliberately: refusing to run would break a working setup over a condition the operator may
have accepted, while a warning still surfaces the misconfiguration at the moment it matters.
The notice fires once per file per process — the credential is resolved on every routed
request, and a per-call warning would train the operator to ignore it. POSIX only; Windows
mode-bit semantics differ, so the check is a clean no-op there rather than a guess. The
warning never echoes the credential. -
Failed tasks and lost failover attempts are recorded in the usage log (#100). A task that
fails at every backend now writes akind: "failure"record carrying the per-backend attempt
list, and a task served only after failover carries the attempts it lost in a new optional
failuresfield. Both are held out of the spend-avoided headline the way delegate records
already are, and--statsshows aTasks failedline (with the lost-attempt count) once there
is anything to report. The new fields are optional and additive, so existing readers and
pre-existing log lines are unaffected. -
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), uncheckedkey_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).
Changed
-
The
--statsblock says which figures are lifetime and which are not. The heading now reads
spend avoided (cloud-equivalent, lifetime), and the delegate tree'sLinked to:line is marked
(within the current row window); the GUI panel carries both labels too. That one line is the
only figure in the block that cannot be folded into the permanent totals — it holds one key per
parent task id, so its size grows without bound — and an unlabelled window-scoped split sitting
under a lifetime headline reads as a lifetime count. No number changed. -
State moved out of the cache tier — the rotation cursor, the usage log and config backups now
live under~/.local/share/tanglebrain/(honoringXDG_DATA_HOME;TANGLEBRAIN_STATE_DIRstill
overrides everything).~/.cacheis defined as a directory any cleanup tool may delete at will,
and nothing TangleBrain kept there was a cache: the usage log is the only record of accumulated
spend-avoided and is never reconstructible, because prompt and response text is never persisted
by design; a config backup is the only copy of something the operator hand-edited. Both were one
brew cleanupfrom gone.Existing installs migrate on first run, and lose nothing. Every entry in
~/.cache/tanglebrain/is copied forward before anything reads state, the originals are left
in place so a downgrade still finds its history, and a single stderr notice names both
directories. The copy is per-entry and skips what is already present, so an interrupted migration
completes on the next run instead of skipping wholesale, and a completed one is a no-op. A
migration that fails says so on stderr rather than raising — an incomplete log understates
savings, and an operator has to be able to tell that from a genuinely small number.Two details are enforced rather than asserted. The migration copies every entry it finds rather
than a named list of expected files, because a hard-coded list is a claim about a directory's
contents that decays silently the moment anything new is written there. And the "every console
script migrates before it reads state" claim is a test that derives its list from
pyproject.toml's[project.scripts], so adding a fifth entry point without wiring the
migration fails the build.This closes the placement half of
#101; the log is still unbounded and
that half stays open. -
The delegate extra now requires
mcp >= 2, < 3.tanglebrain/mcp_server.pyis migrated to
the 2.x API:mcp.server.fastmcp.FastMCPbecamemcp.server.mcpserver.MCPServer. The tool
surface is unchanged —delegate,delegate_local,delegate_targetsanddelegate_manykeep
their names, parameters and behavior, and the decorators andrun()are identical, so an
orchestrator sees no difference.This is a breaking change for installs, announced as one per
docs/design/deprecation-policy.md: the floor is
load-bearing, sincemcp.server.mcpserverexists in no earlier major. Users who need mcp 1.x
should install TangleBrain 0.20.1, the last release that allowed it — it remains on PyPI and
keeps working. Only users of the optional[delegate]extra are affected; the core install is
untouched.Two things worth knowing before upgrading. The extra now installs
httpx2alongside
TangleBrain's ownhttpx(andhttpcore2alongsidehttpcore), because mcp 2.x moved HTTP
stacks — wasteful rather than dangerous, and the alternative was holding the SDK at a superseded
major indefinitely. Total installed distributions are effectively unchanged (30 → 29 in a
measured comparison), because 2.x declares directly much of what 1.x pulled transitively;
opentelemetry-apiandtruststoreare genuinely new. -
Every declared dependency now carries an upper bound.
httpx >= 0.27, < 1and
PyYAML >= 6.0, < 7replace open-ended constraints. This changes what a fresh
pip install tanglebrainresolves to: a futurehttpx1.0 orPyYAML7.0 will no longer be
picked up silently. The bound sits at the major boundary deliberately — tighter caps buy nothing
and create resolution conflicts for anyone installing TangleBrain alongside other packages, which
is a real cost paid by users to prevent a break semver already announces. What made themcp
2.0.0 breakage expensive was not the release but the unbounded constraint that let it land in
every fresh resolve with no announcement and no commit to blame.
Fixed
-
A backup is never left short under a valid name. The pricing and roster saves copied
straight to their final backup path, so an interrupted copy left a truncated file that a later
restore would read as whole — and a backup is consulted exactly when the original is already
gone. Both now stage the copy beside the target and rename it into place, the same way the writes
themselves have always worked. Staging names also carry a unique suffix, so two writers racing on
one target can no longer interleave their bytes into a shared.tmpand rename the mixture in. -
The GUI panel's colour layer now meets WCAG 2.1 AA, and is asserted rather than audited once
(#115). Nine of twenty-seven pairs failed, all of them muted text or control outlines. Two
findings are worth repeating.--text-mutedcleared 4.5:1 against the page background but not
against the two surfaces it is actually used on (4.34:1 on cards, 3.89:1 on the elevated tiles) —
measuring against the page alone would have declared it conformant. And form fields were outlined
at 1.09:1 against their own fill, so the boundary of a text input carried essentially no contrast;
that outline is now its own token, kept separate from the decorative border so the fix does not
restyle cards and tables that never needed it. Raising the rest-state outline then dropped the
focus outline to 1.40:1 against it — passing SC 1.4.11 while quietly failing SC 2.4.7 Focus
Visible — so the focus colour moved too.tests/test_gui_contrast.pyparses the palette out of
the stylesheet and checks every pair, plus two structural assertions that stop the pair table
falling behind the stylesheet: every:roottoken must be measured or explicitly exempt, and no
colour literal may live outside:root. Decorative borders and disabled controls are exempt by
the explicit carve-outs in SC 1.4.11 and 1.4.3; both exemptions are recorded in
docs/design/nonfunctional-requirements.mdrather than left silent. -
Two adapters coerced caller-supplied options past their own error contract.
optsis a
Mapping[str, object], and both the openai-compat and CLI adapters ran a bareint()/float()
over it — so a non-numericmax_tokensortimeoutescaped as a rawTypeError, past the
AdapterErroreach method documents and pastcli.main's handler that turns that contract into
one clean line instead of a traceback. Both now raise the documented type. A numeric string still
coerces: guarding the failure path is not licence to tighten the success path, and a test pins
that half too. Surfaced by adopting mypy (#113). -
from_entrychecks the invariant it claimed in a comment. Both adapters passed
entry.invoke.base_urland.modelthrough with the note "validated non-None by the roster
loader". True of any entry that came throughload_roster— butInvokeis a public dataclass
that can be built directly, and aNonereaching that path surfaced as aTypeErrorfrom inside
the HTTP request rather than as the documentedAdapterError. It is now a check, with a test. -
A delegate failure again tells the orchestrator what went wrong. The mcp 2.x migration
silently dropped the reason: 1.x surfacedError executing tool X: endpoint down, while 2.x
reports a bareError executing tool Xfor any exception that is not aToolError. The tools
now wrapAdapterErrorandRouterErrorso the message survives, and the result still carries
is_error. Losing the reason matters more than it sounds — "endpoint down" and "key_ref file
not found" call for completely different responses, and the operator reading the transcript is
the one who has to tell them apart. A no-fit is deliberately not wrapped: it is a routing
signal telling the orchestrator to do the work itself, and reporting it as an error would make a
working delegation look broken. -
An unreadable
key_reffile now fails with a clean error instead of a traceback. The
permission check stats the file, but the read that followed was unguarded: a file the process
could not open (wrong owner, restrictive mode, removed mid-run) raised a rawPermissionError,
which is outsideresolve_key_ref's documentedRaisescontract and outside theAdapterError
clausecli.maincatches. The operator saw a stack trace where every other credential failure
prints one line. It now raisesAdapterErrornaming the path and the OS reason. -
A paid last-resort backend reached by failover no longer receives the delegate tool. The
fix for #96 asserted the pin/delegate rule at the call sites that had the symptom; the router's
failover loop still passed a blanketinject_delegate=True, so atier: apiCLI entry that is
notcan_orchestratewas built as an orchestrator whenever the rotation exhausted. The decision
now lives in one place —build_adapterderives it from the entry's owncan_orchestratewhen
the caller has no opinion — so every current and future call site is correct by default rather
than by restating the rule. The delegate server still passesFalseexplicitly, which is the
no-recursion rule and not an absence of opinion. -
--modelon acan_orchestrateentry no longer strips its delegate tool. Pinning a
backend built its adapter withoutinject_delegate, so an orchestrator pinned with--model
ran the whole task alone — no delegate tool registered, no warning, no error. The only visible
symptom was a lower spend-avoided figure in--stats, which reads as a routing mystery rather
than a bug. Pinning which backend serves a request is a different decision from whether
that backend may delegate, and one must not silently imply the other. The same defect was
present on the streaming path (run_once_stream) and is fixed alongside it; delegation now
tracks the entry's owncan_orchestrateflag on both. -
--rosterhelp text now states the real default. It claimed the default was the packaged
tanglebrain/config/roster.yaml; the actual resolution is$TANGLEBRAIN_ROSTER, then
~/.config/tanglebrain/roster.yamlif it exists, then the packaged example. This matters more
than a typo: the operational runbook's first diagnostic step for a mis-route is "check which
roster is actually in play," and--helpanswered it incorrectly — sending an operator to read
and edit the packaged example while their own config was live. The symptom of editing the wrong
roster is that nothing changes, which reads as a routing bug rather than a documentation one.
Internal
-
Filed #131 for the accessibility
surface #115 did not cover. The new Accessibility section names keyboard traversal,
screen-reader semantics, motion and reflow as unaudited — anddocs/design/README.mdpromises,
twice over, that every gap the design docs disclose has a tracking issue. Disclosing a new one in
prose with nothing behind it would have quietly broken that promise on the same page that makes
it. The gap ledger and the design-overview Artifact both carry the row now. -
Closed the observations the verify pass demoted. Two were defects rather than polish. Editing
project-state.yamlto retire the answered contrast question had orphaned apriority: lowline
into the next entry, giving it a duplicate key that YAML silently resolves last-wins — the #92
question's priority had flipped frommediumwithout anything saying so. And the contrast suite's
colour-property guard omittedborder-bottomandborder-rightwhileborder-bottomis used
twice in the shipped stylesheet, so its docstring's "every colour-bearing declaration" was still
not true. The pattern is now hoisted to module scope and shared with a new test that runs it
against synthetic CSS for every syntax and every border side — the guard's coverage is asserted
in-repo rather than claimed, and narrowing it now reds the suite. Also: the streaming type guard
is araiserather than anassert, becausepython -Ostrips asserts and a stripped guard
would have handed the client a dict as the byte iterator. -
Resolved the Critic findings for Chunk C. The substantive ones were a stale-claim class and a
real threshold bug.ApiAdapter.from_entrywas a near-verbatim copy of its base differing only in
one string, and this chunk had deepened it by copying a new guard into both; the kind is now a
class attribute and the subclass has no override.ignore_missing_importswas global, which would
have silenced a renamed or misspelled first-party import — exactly the defect the gate was
adopted to catch — and is now scoped tomcp.*. The mypy pin was bounded at the next major by
reflex, but mypy adds checks at a minor, which reds untouched PRs in a lockfile-free CI; both
dev tools are now bounded at their real breaking-change unit. The contrast suite classified a
20.8px normal-weight value as WCAG large text, asserting 3:1 where 1.4.3 requires 4.5:1, and
missed two pairings that exist in the shipped stylesheet (--dangeron the output pane, the
button outline on hover). Its literal guard now checks colour-bearing properties rather than
hunting hex, sorgba(),hsl()and bare CSS names cannot slip past — and the docstring now
states plainly what the guards do not cover (a new pairing of two existing tokens) instead of
implying the AA claim is fully mechanical. -
De-anchored every line-numbered citation into
.pyfiles, and retired a claim that outlived its
correction. "The codebase's one deliberately broad exception handler (measurement.py:376-378)"
appeared in six renderings; the previous commit fixed the one it happened to be editing and left
the rest — including a normative Direction statement, which would have made the eight waivers this
chunk legitimised read as departures. Every site now cites by symbol and points at the grep. The
cited line ranges had already drifted:376-378is the linkage block today. -
Adopted ruff and mypy as defect gates; declined the formatter and
--strict, with reasons
(#113).make lintnow runs both,make testdepends on it, and CI runsmake test, so a gate
cannot be green locally and absent in CI. Both tools sit in a newdevextra — nothing there is
imported at runtime, so the minimal-dependency posture, which is about what a user installs, is
untouched. Each half of the ruling was measured against this codebase first:ruff formatwould
have rewritten 41 of 45 files while catching no defects, andmypy --strictreported 62 errors
against default mode's 11, 39 of themtype-argceremony. The rule set selects for defects and
deliberately omits the style families, which are the formatter question under another name. The
full ruling, and what would justify revisiting either decline, is in
docs/design/nonfunctional-requirements.md, "Code quality gates". -
Twelve inert suppressions removed or made real. The codebase was already written as though
these tools were running: it carried# noqa: BLE001,N802,F401andE731directives, plus
a# type: ignore[arg-type]inmeasurement.pythat named the wrong error code. Every one
suppressed a rule nothing had selected, so none of them did anything. That is the same defect
#113 describes for annotations, one level up — a decision documented but not enforced.RUF100is
now in the rule set so a waiver that stops being needed fails the build. -
A latent closure bug and a truncating comparison in the suite. A
lambdain
test_openai_compat.pyclosed over a loop variable rather than capturing it — harmless only
because each iteration consumes its lambda before the next rebinds it — andtest_roster_edit.py
zipped two line lists withoutstrict, so an edit that changed the line count would truncate
to the shorter side and could still satisfy "exactly one line differs". Both found by ruff on
adoption. -
Packaging tests assert parsed constraints instead of the characters of a requirement string.
The upper-bound check looked for a literal<anywhere in the requirement, which an environment
marker satisfies with no ceiling at all (foo >= 1 ; python_version < "3.13"passed) — and the
mcpcheck was whitespace-sensitive, so the semantically identicalmcp>=2,<3failed it and a
formatter run would have redded the suite over nothing. Both now compare against the version
specifier with the marker stripped and whitespace normalized. Verified both ways: the marker-only
form now fails, the unspaced form now passes. -
Retired the
#90admissions the same bundle closed.deprecation-policy.mdand
api-contract.mdstill described the mcp floor decision as pending — one of them in the sentence
justifying why the policy was written when it was, so a reader assessing whether it was authored
under pressure from a live decision got the wrong answer. The bundle applied the closed-gap rule
to #92 and #114 and missed the issue it closed itself. -
The design-doc gap table says what its completeness claim covers. It read "Every gap these
documents disclose has an issue" whilesecurity-model.mddiscloses two ratified non-goals — no
roster integrity check, permissions warned rather than enforced — that deliberately have none.
The claim now scopes itself to open work and says non-goals are decisions rather than backlog. -
installReferenceverified against the consumer, not only against itself. The five new tests
prove the key is self-consistent; they cannot provemarketplace.jsonstill loads with an
unrecognized top-level key, which is the failure class that produced v0.20.1.claude plugin validatereports validation passing withUnknown field 'installReference'. Claude Code ignores it at load time.— ignored being the intended outcome. Recorded inoperations.mdwith the
command, so it is re-checkable rather than a remembered assurance. -
The deprecation policy's own precedent is now a test.
deprecation-policy.mdcommits that a
removed CLI flag becomes an accepted no-op rather than an error, and cites--routeas the
standing example — but--routewas declared in the parser and read nowhere, with no test
behind it. Deleting it would have passed the whole suite while breaking exactly the forgotten
scripts the commitment exists to protect.tests/test_cli.pynow pins both halves: the flag
still parses, and it reachesrun_onceas no argument at all, so it is inert rather than merely
tolerated. Verified by deleting the flag, which now fails. -
The deprecation policy now says what "announced as breaking" means before 1.0. The policy
requires a dependency-floor bump to be announced as breaking; TangleBrain's release tooling maps
the major-bump marker (the word, followed by a colon) to a major bump, which from0.20.xis
1.0.0— a claim about the project's maturity that no single change earns. The policy now states
that pre-1.0 a breaking change ships as a minor bump announced in prose, and reserves the marker
for after 1.0. Announcement is a duty to the reader; the marker is a lever on the version number,
and conflating them either understates the break or overstates the release.Note the marker is deliberately not written literally in this entry: the bump step matches it
anywhere in the body, so it cannot distinguish a marker from a mention of one. Writing this entry
the obvious way would have forced the 1.0.0 it exists to warn against. -
The design-doc gap table pointed at the wrong issue for the deprecation policy. The row
reading "No written deprecation policy" linked #90 (the mcp 2.x migration) rather than #114,
which is the issue that actually writes the policy. #90 is where the gap becomes live — an
mcp >= 2floor is the first real test of an unwritten rule — but a reader following the link
arrived at a migration rather than the policy work. A wrong link satisfies the table's
completeness claim while defeating its purpose, which is worse than an absent row. -
CI now runs on a weekly schedule, not only on push. A dependency can break the build with no
commit to blame, and a push-only CI never notices: after themcp2.0.0 releasemainread
green for days because its last run predated the release, and the failure first surfaced on an
unrelated markdown-only PR. The scheduled run resolves dependencies fresh — no lockfile, no cache
— so it sees what a user's install would resolve to today. A lockfile was considered and rejected
for CI: a green run against frozen versions says nothing about what a fresh install gets, which is
the exact signal wanted here.workflow_dispatchis enabled alongside it so the canary can be
exercised on demand rather than only by waiting a week. -
tests/test_packaging.pyasserts the rule, not one instance of it. The upper-bound check
covered only themcprequirement; it now iterates every requirement inproject.dependencies
and in everyoptional-dependenciesextra, so a dependency added to a new extra is held to the
same rule without anyone remembering to extend the test. Themcp-specific test was not deleted
in the consolidation but sharpened to assert< 2rather than merely an upper bound —
mcp >= 2, < 3satisfies the general rule while shipping a delegate extra that cannot import,
so the two tests now fail on different mutations rather than restating one another. -
observability.md's first gap now carries its issue. "Nounlinkedvisibility" was the
one gap in the design set with no tracking issue behind it, which brokeREADME.md's claim that
every disclosed gap has one. Filed as #123 and linked from both, with the actual defect stated:
a lostTANGLEBRAIN_TASK_IDhop and a genuinely parentless sub-call land in the same bucket, so
the count answers no question. -
The design-doc gap table lists every gap the documents disclose.
docs/design/README.md
claims "every gap these documents disclose has an issue" and then omitted #97 — capability
routing ranking by cost only, named inapi-contract.md. A completeness claim with a missing
row is worse than no claim, because it stops the reader checking. -
The loopback bind is now a tested contract, for both HTTP surfaces.
tanglebrain-guiand
tanglebrain-serveare unauthenticated by design and spend real backend quota, so the
127.0.0.1bind is not a default — it is the whole authorization model, and widening it does
not weaken the posture but voids it. Nothing tested it: a one-character edit to0.0.0.0
passed green.tests/test_bind_address.pyasserts the address handed to the server rather
than that a server starts, since a test connecting over localhost passes just as happily
against0.0.0.0. It covers the three ways the invariant can be voided — widening the
literal, binding every interface with an empty host, and adding a--hostflag that leaves
the literal untouched — the last of which the address assertions alone do not catch. No
production code changed and no socket is opened. -
README.mdleads with the install command. A visitor arriving from the PyPI listing had to
scroll past the pitch to find out how to install; thepip install tanglebrainline now sits
directly under the badges, alongside a MIT license badge that makes the licensing explicit
without a click. -
Reconciled
FEATURES.mdandPROJECT-MAP.mdwith the current generator. Both landed in the
repo carrying a superseded TangleClaw scaffold:FEATURES.mddocumented the abandoned
file.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 a
featurelabel the repo has never had, sogh issue createfailed outright on it and
UI-filed issues landed unlabeled. Both now useenhancement; thebackendlabel
add-backend.mdalso referenced has been created. Every label named across the three
templates now resolves.