v0.2.0
A feature release. The headline: the plugin now ships its own dashboard, so it works on install rather than after a frontend integration.
The SDK ships its own UI
The plugin used to ship a feedback widget and nothing else — the 23 screens existed only as source, compiled by whichever project the scaffolder had stamped them into. Two bugs followed from that, and both were reported:
builderd, the standalone daemon, served its UI out ofBUILDER_WEB_DIR, defaulting to a sibling development project's build output. On any machine without that checkout built, the daemon came up healthy and served no pages at all.- Installing the plugin into an existing application wired up the whole Go side and every screen 404'd, because the host's own SPA has no
/issuesroute and never will.
web/dist is now embedded with go:embed and carried inside the binary, exactly as sdk/dist already was. Copy the binary anywhere and the screens come with it. BUILDER_WEB_DIR survives as a dev override only.
The screens mount at /builder/* — deliberately not /issues, /agents, /chat. Those are words a host application is likely to have used already, and whose handlers its own router would answer first. One prefix, claimed once, is what makes the bundle safe to drop into somebody else's product. Vite's base and the Go WebMount constant are the same string, so asset URLs and the mount cannot drift — and a test asserts it.
web/src is a symlink to blueprint/_project/web/src: one copy of the 23 screens, not two. A fix to a screen lands in the embedded dashboard and in every scaffolded project at once.
The cost is worth naming: the bundle is a few megabytes and lands in the host application's binary too. That is the price of a plugin that works on install.
builderd hosts several apps in one shell
A product is rarely one origin. BUILDER_TARGETS="app=…,auth=…,dashboard=…" frames them side by side with a switcher. BUILDER_TARGET (singular) still works and renders a byte-identical shell, so no existing deployment changes.
Isolation is the part that had to be right. Each framed document is its own JavaScript realm with its own bridge, buffers and picker. What they share is the parent window, so the shell routes on event.source — the window the browser says sent the message — and not on the origin, because two hosted apps may legitimately share one. Pin and screenshot are refused for any non-active frame.
Every forwarded answer is stamped with the app's identity from the shell's own registry, never from the frame. A compromised product cannot file under its neighbour's name, and the stamp rides into the issue's stored browser context — the difference between "a bug somewhere in the product" and "a bug in auth".
/api/health is answered by the daemon itself now, reporting service: "builderd". It previously polled a route the product registers, took the 404 as "down", and drew "API offline" while running perfectly.
Custom apps
Add a screen without forking the plugin. An app is a directory under apps/<slug> with an app.json and a ui.js, discovered by a boot scan — it gets a tile in the launcher and a route at /apps/<slug>.
Two ways in, one implementation behind them so they cannot drift:
togo-builder app new|list, for a human at a promptcreate_app/list_appsover MCP, so an agent mid-run can add the screen it just decided was needed
The MCP tools live on /mcp/agents, not /mcp/feedback. An app ships an ES module the dashboard imports and executes in an authenticated origin; the feedback token is the one documented as safe to wire into a shared editor, and it must not also be able to put running code in front of an operator. The surface fails closed when auth is unavailable, and no custom app can fail boot.
Semantic recall
Recall was keyword overlap wearing relevance's clothes — HashEmbedder hashes a bag of words, so "the login button is broken" and "authentication fails" embed orthogonally.
Real embeddings (bge-m3, 1024-wide, which migration 0002 already declared) now back it when configured, with reranking on the candidate set where the quality gain lands. Three things make the switch safe:
- The endpoint is probed at boot, not assumed. Configured-but-unreachable used to leave every retain storing a NULL vector with only a warning.
- Vectors are labelled with the model that wrote them (
0018), and recall filters on it. A hash vector and a bge-m3 vector share a column, a width and a distance operator while living in unrelated spaces — a meaningless 0.4 would outrank a real match at 0.5. - Legacy rows are re-embedded in the background. Nothing is dropped; they stay findable by keyword throughout.
HashEmbedder remains the fallback, because this harness has to work on day 0 with nothing else running.
Agent runs happen in tmux
A run was a black box. Runs now spawn inside a deterministic session named builder-issue-<number>-<attempt>, and the issue page shows the attach command.
The name is written to the run row before the process starts, so the mapping survives a crash between the claim and the first byte of output. Every terminal path clears it, because the column means "a session you can attach to right now" — a stale name sends the operator to something tmux has already reaped. Reconcile also reaps the orphaned session, closing a leak of one live shell per crashed run.
UI
The 23 screens rebuilt against the design references — issue board and detail, agent and skill catalogues, dashboard, vault, docs, brain, sources, chat, mcp, terminal — over a shared token and motion layer. EN/AR with RTL throughout.
Fixes
- Fonts never loaded.
@font-facedeclares absolute URLs, and the SPA fallback answered those 404s withindex.html— so the browser got HTML where it expected woff2 and every screen silently fell back to the system stack. - Page headings jumped ~120px between routes, because each screen laid out its own header.
- The brain graph letterboxed in fullscreen instead of filling it, and opened at a scale that had to be fought before it was readable.
tmux new-session -Afailed on the second call from a non-TTY. On the exists branch tmux builds a client, and a client needs a terminal — so from an HTTP handler it died with "open terminal failed: not a terminal". Now check-then-creates viahas-session, which touches no client.
Docs
A rewritten README.md plus 14 pages under docs/: architecture, the two ways to run it, the full environment reference, the HTTP API, the CLI, the brain and its embedders, sources, custom apps, tmux agent runs, and troubleshooting.
Upgrading
Two new migrations, 0017 (tmux session) and 0018 (embedding model), both IF NOT EXISTS and safe to re-apply. Nothing is dropped. Semantic recall is opt-in — set BUILDER_EMBED_URL; unset, the brain behaves exactly as it did.
Changelog
- 01868ed: chore(release): 0.2.0 (@fadymondy)
- bd88f66: feat(apps): custom screens, addable without touching builder's source (@fadymondy)
- d9cb29f: feat(blueprint): mirror the EN/AR pass on issues, sources, brain and chat (@fadymondy)
- 0c11b60: feat(brain): real embeddings and reranking, so recall means something (@fadymondy)
- 3836872: feat(builderd): host several apps in one shell, each reporting as itself (@fadymondy)
- e3817c2: feat(runner): agent runs happen inside tmux, so you can watch them work (@fadymondy)
- 1c6bff2: feat(ui): rebuild the 23 screens against the design references (@fadymondy)
- 09bc597: feat(web): the plugin ships its own dashboard, compiled into the binary (@fadymondy)
Install
npx create-togo-builder@latest my-projectThe postinstall verifies the binary's SHA-256 against checksums.txt
before running it.