Release readiness · rev. 2 · evidence-led

Shipping Hearth
and Ember

A second pass, rebuilt around what is actually in the repositories rather than what a release usually needs. Every finding below was measured: line counts, package manifests, request behaviour, the scripts as shipped. The code is healthy. The packaging is not.

Baseline
hearth 0.29.1 · ember 0.49.0
Tests
233 + 524, all green
Findings
6 blocking · 9 significant
Verdict
Two weeks of plumbing from shippable
The harness

pi-durable

Durable task engine: conversations, generations, tools, the inbox. Owns the survival guarantees. Pinned, not forked — and its API changes without notice.

The leash

Hearth

6.7k lines. Adds crews, schedules, provider credentials, an HTTP surface. One process, one SQLite file, one bearer token.

The window

Ember

33k lines of GUI. Replaceable by design — the whole point is that someone else can point their own client at the same Hearth.

01

The shape of it

Before the problems, the dimensions — because they explain why the problems are where they are.

6.7k
Hearth source lines, against 6.3k of tests — a near 1:1 ratio
33k
Ember source lines, of which App.tsx alone is 3,847
0
CI pipelines, installers, or release artefacts across both repositories
1
TODO/FIXME across all of Hearth's source

Hearth is disciplined: 25 source files, one clearly-biggest module (server.ts at 1,100 lines), and almost no deferred-work markers in the code. Its tests are as large as the implementation, which is the ratio you want when the thing being tested is a durable state machine that must not lose a turn.

Ember is the opposite shape. Five files exceed 1,500 lines, and App.tsx at 3,847 is where most of the application lives. That is a real contributor barrier — not because long files are inherently wrong, but because a newcomer cannot find the seam they want to work on.

What this predicts

Everything blocking a release is in the shell — packaging, naming, paths, scripts, documentation — not the core. That is good news: it means the hard technical work is largely done, and the remaining work is unglamorous and mechanical.

02

Six blockers

Each is verifiable in a single command, and each alone makes a public release a mistake.

B-01

Hearth has no licence

Ember ships an MIT licence with a revenue-based rider. Hearth — the piece people would actually fork and build against — has no LICENSE file and no licence field. Absent one, default copyright applies: nobody may legally copy or redistribute it.

$ ls /workspace/hearth/LICENSE* ls: cannot access: No such file or directory
B-02

The Windows installer defaults to your home directory

Both PowerShell scripts ship with C:\Users\Noemi as a default parameter value. A stranger who runs the documented command gets a scheduled task pointing at a repository that does not exist, and a data directory in someone else's name.

scripts/install-windows.ps1:12 [string]$Repo = "C:\Users\Noemi\workspace\hearth", [string]$Home = "C:\Users\Noemi\.hearth", scripts/hearth-watchdog.ps1:9 [string]$Home = "C:\Users\Noemi\.hearth",

The fix is small — derive from $HOME/$env:USERPROFILE, resolve the repo from $PSScriptRoot — but until it is done the Windows path is unusable by anyone else, and Windows is half the audience.

B-03

Neither package can be published, and both names are taken

Both manifests are private: true with no bin, no files, no repository and no author. Hearth has no CLI entry point at all, so there is nothing to put on a path. And the unscoped names are already occupied:

hearth → "Stupid simple json test data stream generator" (1.0.2) ember → "A framework for creating ambitious web applications" (1.1.1)

This needs a naming decision before anything else: scoped packages (@hearth/…), or different names entirely. Everything downstream — the install command, the docs, the Homebrew formula — depends on it.

B-04

The README leads with your infrastructure

The first thing a stranger reads in Hearth's README is the container path on your Mac, your tailnet address, and the names of your three machines. Seventy-four personal identifiers across three documents.

README.md:3 TAILNET.md:52 scripts/README.md:19 … reachable from the Mac at http://100.70.127.78:3002 … C:\Users\Noemi\.hearth on Windows … Flagpole on the Mac, Moat on the Windows PC, Tower in pc-container

TAILNET.md is a genuinely good document about running Hearth on several machines. It is also entirely about yours. It should become an appendix or a blog post, and the README should open with what Hearth is and how to run it in one command.

B-05

The HTTP server has no request-size limit

Hearth parses JSON bodies directly off the request. There is no Content-Length check and no streaming cap, so a single oversized POST can exhaust memory in a process that is also holding the SQLite handle for every conversation.

grep 'content-length|maxBody|bodySize' src/*.ts → no matches src/server.ts:116 const value = await request.json(); src/server.ts:566 const body = (await request.json().catch(() => null))

A token gates it, which bounds the risk to a holder of the token. For a self-hosted single-user tool that is arguably acceptable — but it must be a stated decision, not an omission.

B-06

A bearer token sits in a git remote URL

The GitHub access token is embedded in .git/config. It is not in the tree and has not leaked — but it is one pasted git remote -v from leaking, and it teaches every contributor the wrong habit. Rotate it and move to a credential helper.

origin https://x-access-token:gho_z1Xp…@github.com/noemit/hearth.git
03

Nine significant issues

Not disqualifying on their own. Collectively they are the difference between "works" and "supported".

RefIssueEvidence and consequenceSeverity
S-01 Hearth binds 0.0.0.0 by default A process that runs shell commands listens on every interface, not loopback. Defensible for tailnet access; it must be the documented default, or 127.0.0.1 should be, with binding outward an explicit opt-in. high
S-02 The token is accepted as a query parameter Convenient for WebSocket clients that cannot set headers. It also puts the token in browser history, proxy logs and Referer headers. Keep it, document it, and prefer the header everywhere else. high
S-03 No CORS, so no browser-based client Fine for Electron, which proxies through the main process. It directly contradicts the "build your own GUI" pitch for anyone writing a web client — they will discover it as a wall, not a documented constraint. high
S-04 11 end-to-end specs that nothing runs Playwright suites cover crews, queues, attachments, images and freshness. They only run when a human remembers. A regression in any of those is invisible until someone clicks it. high
S-05 Hearth has no lint step check is typecheck && test. Ember runs ESLint; Hearth, the process holding the database, does not. The cheapest way to catch an unused import that hides a missing call. medium
S-06 18 Markdown files in Ember's root, most stale UI_UX_AUDIT.md states "Target: Ember 0.10.0 at 9fa00fc" — the repo is at 0.49. Nine *_PLAN.md files describe work that shipped or was abandoned. Contributors cannot tell which are live. medium
S-07 Hearth's README claims 224 tests; there are 233 A small lie of the most corrosive kind: it makes every other number in the document suspect. Numbers in docs should be generated or omitted. medium
S-08 Diagnostics logs provider error text verbatim Up to 500 characters of whatever the provider returned, to the console. A provider that echoes part of a key in an error — which happens — writes it to a log file that outlives the session. medium
S-09 buddy-arena/ ships inside the repo A development toy with its own server, sibling to src. Harmless, but it is the kind of thing that makes a reviewer wonder what else is in here. low
04

Already strong

Worth stating plainly, because these are the things most projects get wrong and this one did not.

Electron security is correct

contextIsolation: true, nodeIntegration: false, and a genuinely tight CSP (script-src 'self', object-src 'none', base-uri 'none', frame-src 'none'). The preload exposes a narrow, typed surface rather than a general IPC pipe. This is better than a large fraction of shipped Electron applications, and it should be said out loud in the README.

Credential handling is careful

The remote-access password uses scryptSync with a random salt and timingSafeEqual. Hearth's bearer check hashes both sides to a fixed length before comparing, so it does not leak the token's length. Provider credentials never round-trip — GET /providers reports only the credential type.

Backups are production-grade

A nightly VACUUM INTO — a consistent snapshot of a live WAL database, not a file copy — with thirty kept and old ones pruned. That is the correct primitive, and most tools this size do not have it at all.

The copy rule is a real differentiator

AGENTS.md states that all copy is marketing, especially error messages, and requires failures to name the actor and offer a next step. That is unusually good product thinking. It should be a public contributor guideline — it signals care that most agent tooling does not have.

05

The security story

This is a program that runs commands and holds keys. The job is not to remove that power but to bound it visibly, and to say so before the features.

The framing

"This program can run commands on your machine" belongs in the first paragraph of the README, before the feature list. Anyone who keeps reading has consented to the actual thing. Anything softer is a misleading sense of safety, and a misleading sense of safety is worse than a stated absence of one.

What still has to be written

DocumentContentEffort
SECURITY.mdHow to report a vulnerability, what response is promised, and a private channel. Expect scrutiny the moment a stranger runs this.small
Threat modelTwo pages: assets (keys, shell, filesystem), the trust boundary (the bearer token), and what is explicitly out of scope. Local-first tools are judged by whether they admit their limits.small
auth.json postureProvider keys are plaintext on disk. Either encrypt at rest against an OS keychain, or state plainly that the file is 0600 and disk encryption is the user's responsibility.medium
Tool allow-listAn operator should be able to say "no shell" without forking. The registry makes this nearly free; it needs a documented surface.medium
Binding & token noteDocument 0.0.0.0 and the query-parameter token, or change them (S-01, S-02). Silence here is the finding.small
Dependency auditBoth pi packages are pinned exactly, which is right. Add an audit step so a CVE in a transitive package is noticed rather than discovered.small
06

The contract is the product

If the pitch is "replaceable GUI", then the wire format is what you are actually shipping. Today it exists only as Ember's piApi.ts.

Hearth's HTTP surface is already clean: conversations, entries with optional view documents, providers and sign-in, the inbox queue, approvals, questions, schedules, directory browsing — all bearer-authenticated, all JSON. A second client is possible today. It is not practical, because the only specification is 827 lines of TypeScript written for a different purpose.

What a second client author needs, in order

  1. An OpenAPI description generated from the routes and checked in CI so it cannot drift. This is the single highest-leverage artefact: it produces SDKs, enables exploration, and makes a bug report precise.
  2. A stability promise. Which routes are stable, which are experimental, what the version prefix means. Upstream pi-durable changes without notice; the HTTP surface should not inherit that.
  3. The entries?docs=1 concept, documented as primary. It is not an optimisation — it is what lets a tail read carry the in-flight generation, which is what makes live reasoning possible. A client that misses it will look broken in exactly the way Ember did.
  4. An event-stream description. What may be relied on versus what is a hint to be treated as an optimisation.
  5. One worked example written by someone who is not the author. A forty-line script that sends a message and polls. Showing is cheaper than specifying, and it is the only real test of documentation.
S-03 blocks this

A browser-based client cannot call Hearth cross-origin because there are no CORS headers. That may be the right security posture for a local server — but it means "build your own GUI" is currently true only for native clients, and the documentation must say so or the API must support an explicit opt-in origin.

07

Phases

Ordered by dependency, not by appeal. Phase 1 is a day of work and unblocks everything else.

01a day

Make it legal and impersonal

  • Decide the Hearth licence (Apache-2.0 recommended — it is the piece people build against, and a rider on the server deters exactly those integrators).
  • Strip personal paths from both PowerShell scripts; derive from $HOME and $PSScriptRoot.
  • Rotate the GitHub token; move to a credential helper.
  • Rewrite Hearth's README to open with what it is, not where yours runs. Move TAILNET.md to an appendix.
  • Fix the test-count claim, or remove the number.
022–3 days

Decide the name, then make it installable

  • Pick scoped package names — hearth and ember are both occupied.
  • Add bin, files, repository, author, and drop private.
  • Give Hearth a real CLI entry point: hearth, hearth provider login, hearth token.
  • Compile to a single-file binary with Bun so nobody installs a runtime.
  • Docker image, then a Homebrew tap.
032 days

Make it safe to change

  • CI for both repositories: typecheck, lint, test, build. Add the lint step Hearth is missing.
  • Run the eleven Playwright specs in CI on at least one platform.
  • A large-transcript fixture asserting that opening a session issues a tail request and not a full one — the bug that hung chats would have been caught by one assertion.
  • Formatting and a commit hook so contributions are not whitespace wars.
041 week

Make it auditable

  • Publish SECURITY.md and the threat model.
  • Resolve B-05 (body limit), and decide rather than omit S-01, S-02, S-03, S-08.
  • Decide the auth.json posture: keychain, or documented 0600 plus a plain statement.
  • Ship a documented tool allow-list so "no shell" is a setting, not a fork.
  • Redact provider error text before it reaches a log.
051 week

Make it buildable against

  • Generate and publish the OpenAPI description; check it in CI.
  • State the stability promise and the event-stream guarantees.
  • Publish the "build your own GUI" tutorial with a working minimal client, written by someone else if possible.
  • Archive the stale plan documents into a docs/history/ folder or delete them.
06ongoing

Make it findable and survivable

  • Trademark the name before it is popular — not to stop forks, but so a fork cannot pass itself off as official. The licence should say the code is free and the name is not.
  • Decide CLA versus DCO before the first outside pull request.
  • Packaging: Electron signing and notarisation. Without it, macOS and Windows refuse to launch the app by default, and that is a recurring cost, not a one-off.
  • State the support boundary: which OSes, which providers, and that self-hosting means you run it.
08

Release gates

A checklist meant to be answered yes or no. Anything ambiguous is a no.

01
Every repository carries a licence, summarised in plain languageIncluding a stated position on forking and on the name.
02
A stranger installs it in under five minutes on macOS and WindowsFrom a documented command, without cloning a repository or installing a runtime first.
03
No personal path, home directory or tailnet address appears in a shipped script or READMEVerified by grepping the repository for the author's name.
04
CI is green on a clean clone of both repositoriesTypecheck, lint, unit tests, e2e, build. No environmental exemptions.
05
No credential appears in any committed file, URL, or log outputVerified by searching git history and by running a failing turn and reading the console.
06
The HTTP surface rejects an oversized requestAnd the binding address and token-in-query decision are both documented.
07
SECURITY.md exists with a real reporting channelAnd the threat model is published, including what is out of scope.
08
A second client can be written from the docs aloneOpenAPI plus one worked example, ideally by someone who is not the author.
09
A large conversation opens without hangingPinned by a fixture, not by a fix and a hope.
10
The README's first paragraph says this program can run commands on your machineConsent before features.

Deliberately out of scope for 1.0

DeferredWhy that is the right call
Ember managing Hearth's processKeeps the client replaceable and the supervisor story honest. Revisit once the wire contract is public.
Auto-updateRetrofitting is painful, but shipping it before signing exists is worse.
Splitting App.tsxWorth doing, not worth blocking on. The contributor barrier is real but not a release criterion.
Plugin or extension marketplaceThe registry is extension-shaped and will invite this. Say no publicly, early, or it arrives as a thousand issues.
Multi-user or hosted HearthEvery trust assumption here is single-user local. Hosting it is a different product with a different threat model.
Mobile clientThe HTTP surface makes it possible, and that is the point — someone else can build it.