# Changelog

## 3.2.0 — 2026-10-19

### Room bridge (HQ project rooms → a locked-down room agent)

People in an HQ project room can ask that project's agent to build something.
Shipping still needs the owner's yes. Requests never reach the session the
owner works in; they go to a **room session** per project, which the daemon
starts on demand (#105, #111, #112, decibel-hq EPIC-0028):

- It runs in its own git worktree, `~/.decibel-rooms/<project>`, on
  `room/<project>`, outside the owner's checkout.
- It loads **only** decibel-tools and claude-peers (`--strict-mcp-config`),
  never the user's other MCP servers. Without this, a room session would have
  had the user's deploy and database MCP servers.
- Claude Code deny rules (which hold in bypass mode too) block merge, push to
  main or master, force push, release, publish, deploys, `curl`/`wget`/`ssh`,
  web fetch, shell wrapping (`bash -c`, `eval`), secret reads, and messaging
  other sessions. A per-worktree pre-push hook refuses main and master. The
  rules live in the system prompt, and the request is fenced as data.
- It gets no daemon credentials in its environment.
- **New `rooms` facade:** `rooms.reply(command_id, text, final)` posts into the
  room, and `rooms.request_ship(command_id, pr)` is the only route to shipping.
  It asks the project's ship approvers in their HQ Inbox.
- The daemon claims queued requests atomically, only for orgs with room
  agents enabled, and marks room sessions in presence.

Verified live: a room request edited the README, opened a PR, did not merge
it, and asked the owner to ship.

Limit: deny rules match command prefixes and can be evaded by a determined
prompt injection. The hard guarantee is branch protection on the repo.

## 3.1.2 — 2026-10-18

- **Hosted HQ can pair with a fresh install** (#108). A new daemon trusted only
  HQ's local dev server; `https://home.theagenthq.app` was added only from a
  config file new users don't have, so pairing from hosted HQ was refused and
  showed up as a CORS failure. It is now a built-in default. An allowed origin
  can only request a pairing; approval is still a local click.

## 3.1.1 — 2026-10-17

- **`decibel-tools --daemon install` works on a fresh macOS user** (#106). It
  now creates `~/.decibel` and `~/.decibel/logs` before loading the launchd
  job, whose log paths launchd opens before the daemon runs. When a directory
  can't be created, it says which one and why, instead of a stack trace or a
  daemon that silently never starts.
- **`/health` reports whether hosted terminals work** (`terminals`). They need
  the optional `node-pty` package. Without it the daemon runs normally, and
  `/health` says so with the fix
  (`npm install -g @decibelsystems/tools --include=optional`). npm 11's warning
  that node-pty's install scripts weren't run is harmless on macOS and Windows:
  its prebuilt binary is used, and the daemon fixes the helper's permissions.

## 3.1.0 — 2026-10-17

3.0 made Decibel one runtime. 3.1 lets that runtime host the work: the daemon
spawns and restores Claude Code sessions, a browser can pair with it, and agents
can ask a person a question and get the answer back. This is the release the HQ
private beta installs.

### Changed — check these if you upgrade

- **An HQ-connected daemon refuses to start without `DECIBEL_ORG_ID`** (#90,
  ADR-0014). With Supabase credentials set and no org, it used to fall back to a
  hardcoded default org. There is no default any more. A daemon with no HQ
  credentials is unaffected.
- **Run tracking is off by default** (#77). `.decibel/runs/` is no longer written
  unless you turn it on.
- **Core tables are service-role only** (#95, #96). `voice_inbox` and the swarm
  tables no longer accept the public anon key, which had let anyone read and
  write them. Writes go through the daemon.
- **Spawned sessions no longer inherit the daemon's environment** (#100). The
  daemon's credentials (`SUPABASE_SERVICE_KEY` and any `*_KEY`, `*_TOKEN`,
  `*_SECRET`) and `NODE_ENV=production` are kept out of the Claude sessions it
  starts, and are removed from its tmux server's environment. `ANTHROPIC_*` is
  kept.

### Hosted sessions and HQ

- **Hosted Claude Code sessions** (#84): the daemon spawns and attaches sessions
  in its own tmux, for HQ's terminal pane. HQ chat and teams (#84, #85).
- **Browser pairing** (#84): a page pairs with a non-extractable key and signs
  every request, and the daemon holds only public keys. No HTTP route can
  approve a pairing. **On macOS the approval is a native dialog** showing the
  origin and code (#103, ADR-0016). Elsewhere it's
  `decibel-tools pair approve <code>`.
- **Fleet restore** (#92, decibel-hq ADR-0018): the daemon records which
  sessions were live per repo, and `GET /sessions/last-fleet` plus
  `POST /sessions/restore` bring last night's fleet back. A launch profile in
  `~/.decibel/launch.json` sets permission mode, dev channels and tmux options.
  `scripts/fleet.py` is a terminal stand-in for HQ's `decibel fleet`.
- **Open in Ghostty** (#94): `POST /sessions/open-terminal` opens live sessions
  as Ghostty tabs or windows, titled by repo through tmux.
- **Scrolling** (#97, #99): scroll frames on the attach socket, then mouse on,
  50k lines of scrollback (`launch.json` `tmux` block) and clickable links.
- **Asks** (#98, decibel-hq ADR-0015): `asks.create` puts a question in a
  person's HQ Inbox (or on a guest's page, drafts only until approved), and the
  daemon brings the answer back into `.decibel/asks/` and tells the asking
  session. New `guest:` identity.
- **Artifacts** (#91): published pages registered in HQ's library.
- **Voice** (#101): `voice.inbox_discard`.
- **ADR status from HQ** (#102): `architect.update_adr` changes an ADR's status
  (proposed, accepted, superseded, deprecated, and the new rejected), matching the id
  exactly.

### Reliability

- The daemon supervises the claude-peers broker (#86). Only the daemon
  advertises in `daemon.meta`, and a dead advertisement is not followed (#80).
- Records are created atomically, so a kill can't leave an empty issue (#88).
  A re-close keeps the first resolution (#79). ADRs get a stable identity (#81).
- The global config directory is not a project (#78). A kill switch gives
  RUN/STOP for unattended work (#76).
- Secret guards, and guardian no longer prints the secrets it finds (#87).
- The torture sandbox no longer reaches live services (#89, #74).
- Dependencies: npm audit fix, including @modelcontextprotocol/sdk 1.32.1 (#93).
- MCP clients are told the real server version. The stdio and bridge adapters
  had `2.0.0` hardcoded in their serverInfo.

## 3.0.0 — 2026-09-06

Decibel Tools stopped being a daemon, an MCP server, a CLI, a store and a VS Code
extension that happen to share a repo. It is **one runtime with several adapters**.
An adapter translates transport and establishes project context; it does not
instantiate its own store, caches, project registry, or write path.

That is the whole of 3.0. Everything below follows from it.

### Breaking

- **`DECIBEL_APPS=1` is gone, and no environment variable replaces it.** The
  apps-tier facades (`senken`, `deck`, `mother`, `terminal`) are no longer compiled
  into the package. They load at boot as extensions, from `extensions.allow` in
  `~/.decibel/config.yaml`, **by absolute path only** — a bare specifier would
  resolve against `node_modules`, which would hand facade selection to whoever can
  write a package. An extension may not shadow a registered facade name.

  Config-file state the owner edits beats environment state a generator has to
  remember: a regenerated launchd plist silently dropped those four facades once,
  and `/health` reported `status: ok` with no indication four were missing.

- **The public package no longer describes private facades.** It previously shipped
  compiled `senken.js` and `deck.js` — a description of a live trading Postgres and
  a wallet-spending tool it could never reach. `S7` now asserts their absence from
  the tarball on every run.

- **An unregistered facade is a hard error, not an empty success.** Fail-closed by
  absence is only safe if absence is loud. A call to something that was never
  registered returns `ok: false` / `TOOL_ERROR`; it does not return zero rows.

- **Issue records are canonical markdown.** All 58 remaining bare-YAML records were
  converted; the store is markdown-only. Timestamp-slug *filenames* are deliberately
  kept (ADR-0010) — 49 provenance events and 2 ADRs reference them, and provenance
  events are immutable audit records. Identity is the `id:` field, which is already
  how records resolve.

- **Adapters do not silently become writers.** There is no in-process fallback for
  mutations. A thin client auto-starts the runtime, retries briefly, then fails with
  an actionable error.

### The surface

| Tier | Facades | Actions | How it loads |
|---|---|---|---|
| core | 26 | 183 | always |
| + pro | 31 | 224 | license-gated at runtime |
| + apps | — | — | extensions, absolute-path allowlist, not in the package |

### Store integrity

- **One writer.** `sentinelIssues.createIssue` delegates to `FsIssueRepository`;
  there is no longer a second implementation emitting a different format into the
  same directory.
- **Id allocation is serialized across processes.** A file lock spans allocation
  *through successful write* — serializing only the id scan leaves the race intact,
  since both processes still compute the same number before either file lands — and
  the write itself is `O_EXCL` as a second, independent defence against callers that
  never take the lock. Applies to issues **and epics**; epics were still racing
  until the S5 sweep found it (ISS-0158).
- **Structured fields are parsed, mutated, serialized and atomically renamed**, never
  edited by regex. Values containing `#`, `:`, `---` or a markdown heading survive
  read-back.
- **Repair, once.** `degraded` went 10 → 0, duplicate ids 4 groups → 0, wrong
  `project` values 18 → 0. All three are now invariants CI can fail on. Most of the
  degraded records carried a completed resolution and were open only because an old
  status write had failed — the correct repair was to close them, not reformat them.
- **Status is no longer stored twice by hand.** Markdown records mirror status into
  the body; 16 had drifted from frontmatter, reading `open` to a person while every
  tool reported them correctly. `close_issue` rewrote the mirror and `updateIssue`
  never did. No test caught it because every test asserted through frontmatter, which
  was never wrong.

### Reads state absence honestly

Thirty-one store-backed reads can now tell an **empty** store apart from an
**unreadable** one, an **unparseable** one, and a **project that does not resolve**.
Previously `if (!parsed) continue` and `catch {}` turned every one of those into
`[]`.

Reads that skip a record now carry `store_status` and `unreadable_count`. Three
paths fail loudly instead, because a partial answer would have been dishonest:
context's whole-file `facts.json` and `events.json`, and `auditPolicies` — where one
bad policy used to abort the audit and report `summary.fail: 0`.

### Runtime and daemon

- `ensureRuntime({projectRoot, protocolVersion})` — one lifecycle primitive shared by
  every caller (MCP, CLI, VS Code, hooks). Arbitration is the socket bind or an
  atomic startup lock, never a PID file.
- Protocol negotiation refuses a version-skewed runtime at handshake rather than
  per-call. `/health` carries runtime version, protocol version, tier,
  `extensions.loaded` and `extensions.rejected` with per-entry reasons.
- **Error isolation**: no facade fault escapes `dispatch()`, including throws from
  parameter coercion and facade resolution (which the transports would otherwise turn
  into an MCP protocol fault rather than a failed tool call), and including dispatch
  event listeners, which run synchronously on the call path.
- **Per-facade circuit breaker.** The signal is *unresponsiveness*, not unhappiness:
  a throw always counts, an `isError` slower than 2s counts, a fast `isError` leaves
  the counters untouched. Five consecutive faults open a facade for 30s, then exactly
  one probe at a time — a recovering database gets one connection attempt, not six.
  Facade and direct-tool calls share one circuit, so `senken.trade_summary` and
  `senken_trade_summary` cannot each burn a budget against the same pool.
- Daemon hardening: timing-safe auth, body limits, rate limiting, log rotation,
  graceful drain, crash-loop protection.

### New

- **guardian** (core) — dependency, secret, HTTP and config scanning with A–F grading.
- **postoffice** (EPIC-0037) — model-agnostic agent-to-agent messaging, all seven
  verbs, two-way threaded exchange.
- **conductor** — run/dryrun/trace/cost/egress/routing.
- **zoom** (EPIC-0036) — meeting-summary ingestion, gated behind `DECIBEL_ZOOM=1`.
  **Not yet run against a live Zoom account.** It ships dark: unreachable unless
  explicitly enabled.

### The release gate

`@decibelsystems/tools@3.0.0` is the first release blocked on the **tool torture
test** (`.decibel/specs/2026-09-02-tool-torture-test.md`) — eight generated sweeps
over the whole surface across four transports, built on one observation:

> Every failure this project has actually shipped returned `ok: true`.

A voice inbox dead for five and a half hours read as `voice 0`. A regenerated plist
dropped four facades while `/health` said `status: ok`. `/batch` answered
`{ok: true}` for a facade that does not exist.

| Sweep | Gate |
|---|---|
| S0 enumeration | no unclaimed actions, no dangling actions, no expired waivers |
| S1 envelope | every action answers in a well-formed envelope |
| S2 absence | every read distinguishes the four situations above |
| S3 round-trip | hostile values survive write → read |
| S4 transports | stdio, thin, http-call and http-batch agree, differences declared |
| S5 concurrency | 50 concurrent creates across 10 processes; crash mid-write |
| S6 tier/extension | the apps boundary holds, and no env var reopens it |
| S7 artifact | the packed tarball, not the repo |

The surface is **generated from the kernel's own registries**, never enumerated by
hand: adding a tool without giving it coverage turns the build red. Every action is
claimed by a sweep or waived in `waivers.yaml` with a reason, an owner, and an
expiry. Waivers expire, and an expired waiver fails the build.

Six of the eighteen findings in S2 were the harness lying — fixtures planted at paths
no reader globs, so tools that handled absence correctly were reported broken. S5's
first crash test reported four successful `SIGKILL`s while all four victims kept
running. Every sweep now carries a calibration probe that asserts it can see a
deliberately broken tool before it is believed about the healthy ones.

881 tests.

---

## 2.x

Not tracked in this file. See the git history and `.decibel/sentinel/` for the
issue-level record.
