Skip to content

Fix provider process exit lifecycle races - #1402

Merged
ymichael merged 1 commit into
mainfrom
bb/split-provider-shutdown-race-fixes-into-pr-thr_xehe355z82
Aug 12, 2026
Merged

ymichael merged 1 commit into
mainfrom
bb/split-provider-shutdown-race-fixes-into-pr-thr_xehe355z82

Conversation

@ymichael

@ymichael ymichael commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Extracted from #1385 as a focused split: only the provider process shutdown/replacement race fixes, plus their regression tests. No Codex adapter, provisioning, schema, or docs changes ride along.

The races

An exited provider process could corrupt the state of the session that replaced it. Three distinct races made that possible.

1. Exit vs. close drain. The manager finalized a provider on the child exit event. Node can emit exit before the final stdout/stderr bytes have been read from the pipes, so pending requests were rejected and exit diagnostics reported without the provider's last output — losing exactly the error text that explains why it died. Finalization now waits for close (stdio fully drained), bounded by a one-second grace deadline so a descendant that inherited a pipe cannot block finalization forever.

2. Replacement synchronization. A caller could observe an exited provider and start a replacement while the old process was still draining, briefly yielding two "current" providers for one process key. Provider entries now carry an explicit exitFinalized promise. The replacement, shutdown, and concurrent-start paths await it and then re-check whether another caller already created the replacement, so only one current provider exists.

3. Stale stream output. A descendant holding an inherited pipe could emit protocol messages after the replacement became current, injecting stale output into a fresh session. When the grace deadline expires the old readline interface is closed and both provider pipes destroyed; the stdout/stderr callbacks additionally verify their process is still current before forwarding anything.

Two distinct issues behind the stderr test

These were initially conflated and are worth separating:

  • Fixture bug (test-only). bounds provider stderr while data arrives without a newline fails deterministically on current main. The fake provider did process.stderr.write(100KB) then process.exit(42). process.exit() discards Node's async-buffered pipe output, so the child attempted ~100,011 bytes but the parent only ever saw 65,536 — at both exit and close. No parent-side drain can recover bytes the child never wrote. Fixed by setting process.exitCode = 42 and letting the process exit naturally.
  • Production bug (issue 1 above). Genuinely separate. Verified by experiment: clean-main runtime + fixed fixture passes 3/3, so that test does not exercise the drain race. The production race is covered instead by drains provider stderr before reporting process exit, where a descendant writes to the inherited pipe 50 ms after the provider exits — an ordering that guarantees exit precedes the final data and that only the close/grace fix handles.

Tests

Six regression tests in runtime.process-lifecycle.test.ts: final output after exit, concurrent replacement/start synchronization, descendant-held pipes, stale descendant output after replacement, stream destruction, and bounded cleanup.

Each was mutation-verified — every fix was individually reverted to confirm the corresponding test actually fails, rather than passing incidentally:

Reverted fix Failing test(s)
stream destruction cuts off inherited provider output…
close/grace drain drains provider stderr…, cuts off inherited…
ensureProvider synchronization waits for an exited provider…, starts a single replacement…, cuts off inherited…
shutdown skip for exited process does not wait for an already-exited provider…

This caught two initially-weak assertions, which were then strengthened: the stream-destruction check passed incidentally because the descendant had already exited and let the pipes EOF naturally, and the shutdown test had no assertion at all and relied on vitest's ambient timeout.

Scope

Product reliability change. No database schema, migration, or Drizzle snapshot change. No host-daemon contract or protocol-version change — verified by inspection: RuntimeProviderProcess never leaves @bb/agent-runtime, the new exitFinalized field is a Promise (inherently non-serializable), and the only production consumers of the adjusted listRunningProviders() are two host-local length === 0 bookkeeping checks in runtime-manager.ts that are never serialized. HOST_DAEMON_PROTOCOL_VERSION is therefore deliberately unchanged.

Validation

  • turbo run typecheck lint test --filter=@bb/agent-runtime --filter=@bb/host-daemon — green (916 + 543 tests)
  • turbo run build for both packages — green
  • runtime.process-lifecycle.test.ts run 20× consecutively — 20/20, no flake
  • Rebased onto latest main (c288a199a) and re-validated

AGENT GENERATED: by Claude Opus 5

@ymichael
ymichael force-pushed the bb/split-provider-shutdown-race-fixes-into-pr-thr_xehe355z82 branch from 1847d81 to 0e3e196 Compare August 12, 2026 21:06
Three related races let an exited provider process corrupt the state of
the session that replaced it.

Exit vs. close drain: the manager finalized a provider on the child
`exit` event, which Node can emit before the final stdout/stderr bytes
have been read. Pending requests were rejected and exit diagnostics
reported without the provider's last output, losing the error text that
explains why it died. Finalization now waits for `close` (stdio fully
drained), bounded by a one-second grace deadline so a descendant that
inherited a pipe cannot block finalization forever.

Replacement synchronization: a caller could observe an exited provider
and start a replacement while the old process was still draining.
Provider entries now carry an explicit `exitFinalized` promise; the
replacement, shutdown, and concurrent-start paths await it and then
re-check whether another caller already created the replacement, so only
one current provider exists per process key.

Stale stream output: when the grace deadline expires, the old readline
interface is closed and both provider pipes destroyed, and the stdout and
stderr callbacks verify their process is still current. A descendant
holding an inherited pipe can no longer emit output into the replacement
session.

Separately, fix a test fixture that wrote 100KB to stderr and then called
`process.exit(42)`. Node discards async-buffered pipe output on
`process.exit`, so the child attempted ~100,011 bytes while the parent
only ever saw 65,536 at both `exit` and `close`, making "bounds provider
stderr while data arrives without a newline" fail deterministically. This
is a fixture bug, not the drain race above: clean-main runtime with the
corrected fixture passes. The fixture now sets `process.exitCode` and
exits naturally, and the production drain race is covered instead by
"drains provider stderr before reporting process exit".

Host-local provider process lifecycle only: no wire payload, schema, or
host-daemon contract change, so HOST_DAEMON_PROTOCOL_VERSION is
deliberately unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ymichael
ymichael force-pushed the bb/split-provider-shutdown-race-fixes-into-pr-thr_xehe355z82 branch from 0e3e196 to 08156e4 Compare August 12, 2026 21:09
ymichael added a commit that referenced this pull request Aug 12, 2026
## What this fixes

Test-determinism and QA/runbook maintenance only, extracted from #1385.

| Issue | Simple explanation | Fix |
|---|---|---|
| Docs watcher test was timing-dependent | The test wrote a real file
and then raced native FSEvents plus a 250ms debounce, so it failed
whenever the `watch-vaults` service had not attached yet or the host was
busy. | Give the Docs plugin a watcher injection seam that defaults to
native `fs.watch`, and have the test drive a fake watcher directly. It
now asserts the watched root, the published `vault-changed` signal, and
watcher cleanup on abort — with no filesystem-event timing. |
| Database-maintenance test depended on file order | The `@bb/server`
vitest project runs with `isolate: false`, so files share the
module-level `lastDatabaseMaintenanceCheckAt`. `periodic-sweeps.test.ts`
parks it at wall time, while this file swept from a hardcoded
`1_000_000_000_000` (2001) — every sweep then looked "too soon". | Start
the synthetic clock a day ahead of wall time so it always leads any real
timestamp another file records. |
| Daemon restart helper only worked once | `buildDaemonRestartCommand`
baked in the PID captured at build time, so a second run left the first
replacement daemon alive and raced a second one onto the same port and
data dir. | Resolve the PID from the daemon PID file at run time
(build-time PID as first-run fallback), and refuse to run when the
resolved value is not a positive integer so a corrupt PID file cannot
become a stray `kill`. |
| Archive runbook expected immediate destruction | A last-live archived
managed worktree stays revivable for the five-minute undo grace period
(`MANAGED_ENVIRONMENT_RETIRE_GRACE_MS`), so the old expectation was
wrong. | Assert `retiring` plus a preserved worktree first, then
permanently delete the archived thread and wait for destruction. |
| Runbook tracked the wrong restarted daemon | `DAEMON_PID=$!` after
`eval "$RESTART_DAEMON_COMMAND"` captures the shell's job, not the
detached daemon. | Read the authoritative PID file
(`.paths.daemonRestartPidPath`) exported by the standalone harness after
each restart. |
| Permission QA used non-public modes and unreliable probes | `readonly`
and `workspace-write` are not accepted by the CLI (public modes are
`accept-edits`, `auto`, `full`), and Codex sandboxes commonly permit the
OS temp dir, so `/tmp` probes were not reliably outside the workspace. |
Cover only the public modes and place the approval/denial probes under
`$HOME`. A new contract test rejects stale mode names and pins the probe
targets. |
| Provisioning runbook promised restart recovery | The old "Server
restart during environment provisioning" scenario killed the server
mid-RPC and implied lost-result recovery and persisted attempt identity.
| Removed entirely. Replaced with the supported contract: a
`.bb-env-setup.sh` hook fails exactly once, the thread and environment
settle in `error` with `thread_provisioning_failed`, and one explicit
`bb thread tell ... --mode auto` starts a fresh provisioning attempt
that reaches `ready` before the turn runs. A contract test keeps the
restart scenario from returning. |

## Scope

No product behavior, database schema, migration, Drizzle snapshot,
provisioning implementation, host-daemon wire contract, or
protocol-version change.

The full diff is 8 files. Only two are non-test sources:

- `plugins/docs/server.ts` — adds the watcher injection seam; the
default parameter is the existing native `fs.watch` implementation, so
production behavior is unchanged.
- `tests/qa/src/shared.ts` — QA-harness helper in the `tests/qa`
package.

**Deliberately omitted:** the provider-interaction integration commit
from #1385 (`0fc13d45e`). It deletes roughly 750 lines of live-provider
scenarios covering `auto` permission mode plus the
`auto-ask`/`auto-deny` harness presets. Its premise does hold on current
main — `auto` pairs with `approvalReviewer: "automatic"`, which the
Codex adapter maps to `auto_review` rather than routing to
`onInteractiveRequest` — so it is not strictly coupled to the excluded
Codex runtime commits. It is omitted because it is a coverage deletion
rather than a flaky-test or QA fix, and it cannot be validated here:
`src/integration*.test.ts` is excluded from the default `test` task and
only runs via `test:integration` against real providers. Removing
unverifiable integration coverage does not belong in a no-product-change
PR; it is left as a follow-up.

## Validation

Run on this branch rebased onto current `main`:

- `turbo run typecheck test --filter=bb-plugin-simple-notes
--filter=@bb/qa --force` — 5/5 tasks; Docs 59 tests, QA 25 tests.
- `turbo run typecheck test --filter=@bb/server --force` — 161 files,
1450 tests passed.
- Database-maintenance flake reproduced before the fix with
`--no-file-parallelism --sequence.shuffle.files --sequence.seed=3` (4
failures), and passing on that same ordering after.
- The standalone-restart regression assertions were confirmed to fail
against the previous `buildDaemonRestartCommand`.
- `turbo run lint` on the touched packages — no lint tasks are defined
for them.
- Prettier clean; final diff audited for
schema/migration/contract/provisioning paths (none).

Runbook claims were checked against `main` rather than assumed:
`thread_provisioning_failed`, `error` in `threadStatusValues`, `bb
thread wait --status`, `bb thread delete --yes`, `bb thread tell --mode
auto`, `PROJECT_ROOT` exported by the QA harness, and
`MANAGED_ENVIRONMENT_RETIRE_GRACE_MS = 5 * 60_000`.

### Pre-existing macOS-only failure, not from this PR

`packages/agent-runtime/src/runtime.process-lifecycle.test.ts > bounds
provider stderr while data arrives without a newline` fails on clean
`origin/main` on macOS. Its fake child writes 100KB to stderr and
immediately calls `process.exit(42)`. Pipe writes are asynchronous on
macOS, so the child exits without flushing bytes that never reach the
parent; this is distinct from the real parent-side `exit`-before-`close`
drain bug. CI's `Tests (packages)` shard runs on Ubuntu, where the
fixture passes.

#1402 fixes both issues separately: the fixture uses `process.exitCode =
42` so Node flushes naturally, while the runtime waits for stream close
before finalizing provider exit. Those provider-runtime changes are
explicitly out of scope here, and this branch touches no `agent-runtime`
files.

> AGENT GENERATED: by Claude Opus 5 (1M context)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ymichael
ymichael merged commit b5d89f3 into main Aug 12, 2026
10 checks passed
@ymichael
ymichael deleted the bb/split-provider-shutdown-race-fixes-into-pr-thr_xehe355z82 branch August 12, 2026 21:29
ymichael added a commit that referenced this pull request Aug 12, 2026
Two Codex runtime reliability fixes, split out of #1385 so they can be
reviewed on their own.

## Bug 1: command output lost when Codex reorders events

Codex app-server normally reports a shell command as `function_call` →
`function_call_output` (the full stdout) → `item/completed`. Sometimes
the `item/completed` event arrives **before** the raw record carrying
the output.

The adapter only repaired a completed item using output it had already
captured. In the reordered case there was nothing captured yet, so it
published the provider's truncated `aggregatedOutput` and the real
output — which arrived a moment later — was dropped. Users saw a
finished command with missing or partial output.

**Fix:** when a completed command item arrives while its shell call is
still awaiting raw output, hold it instead of publishing it. Repair and
emit it as soon as the raw record lands. If the raw record never
arrives, flush the held item at turn completion, so output is at worst
delayed rather than lost. Unparseable raw output clears the pending
shell call so the completed item is released unchanged.

## Bug 2: generated-title rename fails on a brand-new session

A new Codex rollout file can exist briefly before its first record is
written. A generated-title rename landing in that window fails with
`rollout at <path> is empty`, and the thread keeps its placeholder title
even though the same call would succeed moments later.

**Fix:** retry only that exact transient error, and only for Codex,
backing off 50 ms then 200 ms before a final attempt. Every other rename
failure, and every other provider, still fails immediately.

The backoff is only ever paid when the rename has *already* failed — the
happy path returns on the first attempt — and the full 250 ms only when
it fails for good, where the alternative was failing instantly and
leaving the thread on a placeholder title.

## Protocol version

Bug 1 changes what the host daemon sends the server, so
`HOST_DAEMON_PROTOCOL_VERSION` goes 109 → **110** with its contract test
and comment updated. Older daemons can publish a truncated completed
item, so enrolled machines must update.

Note for reviewers: #1408 bumped the constant to 109 and updated the
assertion, but left the test name and comment describing 108. Rebasing
onto it meant rewriting those exact lines, so this PR lands a correct
110 lineage and clears that stale wording as a side effect.

These are product reliability changes only — **no database schema,
migration, or Drizzle snapshot changes**, and no other behavior from
#1385.

## Validation

- `turbo run typecheck --filter=@bb/agent-runtime
--filter=@bb/host-daemon-contract` — pass
- `turbo run typecheck --filter=@bb/host-daemon --filter=@bb/server`
(downstream protocol consumers) — pass
- `turbo run test --filter=@bb/host-daemon-contract` — 49/49 pass
- Rebased onto `3bd8e79a0` and fully re-validated
- `turbo run test --filter=@bb/agent-runtime` — **921 pass, 0 failures**
- Prettier clean on all changed files

Three regression tests were each verified to **fail against current
`main` and pass with the fix**:
- adapter: repairs a completed item when the raw output arrives late
- adapter: flushes the held item at turn end when raw output never
arrives
- runtime: retries the rename through one empty-rollout error and
applies the title

A fourth test pins the exhaustion path: a rollout that stays empty makes
exactly 3 attempts and then propagates the error, so the retry can
neither loop nor swallow a genuine failure.

## Previously-noted test failure is resolved upstream

Earlier revisions of this description flagged
`runtime.process-lifecycle.test.ts > bounds provider stderr while data
arrives without a newline` as failing locally on macOS. #1402 landed
that fix on `main` (the fixture used `process.exit()`, which discards
Node's async-buffered pipe output on macOS). After rebasing onto it,
**the full suite is green: 921/921 with zero failures.**

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ymichael added a commit that referenced this pull request Aug 17, 2026
The only provider without an existing bridge, built canonical-only (no
legacy dialect; the legacy in-process adapter stays untouched and live).
Per decision #5 the bridge owns its app-server children: one per bb
thread with #1402 supervision (finalize on close with bounded grace,
pipe destruction, per-callback currency checks, allowlisted child env,
SIGTERM->SIGKILL escalation, kill-all on stdin close). Unexpected child
death settles open turns as failed and the next turn restores from the
rollout with a session/replaced notification.

Translation shared, not duplicated: the adapter's ~1,200 lines of
stateful closures extracted byte-identically into codex/translator.ts +
session-params.ts (adapter 2,255 -> 348 lines); event-translation,
schemas, interactive-requests, permission-mapping imported by both
dialects. Bridge-minted structural ids (bt<entropy>-<serial>-<codexId>)
keep parentToolCallId consistency with no maps and reverse-map for
steer/interrupt/fork including legacy-persisted raw codex ids.
Archived-session resumes reply SESSION_NOT_RESTORABLE with the original
text preserved for the unarchive-retry path. Handshake: sessionRestore,
archiveSync, nameSync, goalState, manualCompaction all true; fork
checkpoint; runtime approval policy.

Conformance 11/11 against real supervised fake-app-server children.
Suites 973 default + 253 isolated, zero legacy assertion changes; the
daemon build produces bb-codex-bridge.mjs, wired into the bundle
manifest, launcher required-artifacts, and the tarball smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ymichael added a commit that referenced this pull request Aug 17, 2026
…ocol) (#1640)

# Agent providers as a first-class plugin surface

Implements
[plans/agent-provider-plugin-surface.md](plans/agent-provider-plugin-surface.md)
**end to end — phases 1–6 plus graduation**. Providers declare their
metadata through the plugin API and run as plugin-registered **provider
bridges** speaking one bb-owned, provider-agnostic JSON-RPC protocol.
The canonical path is no longer gated: the experiment, the legacy
adapters, and the core provider catalog are all deleted, so the bridge
protocol is the only way a provider reaches bb.

132 commits, 622 files, **+41,035 / −25,028** (`git diff --stat
origin/main..HEAD`, rename-aware — most of the insertions are provider
sources *moving* into `plugins/provider-*`).

## What's here, by phase (commits are ordered to match)

1. **Protocol + kit** — `@bb/provider-bridge-protocol`: versioned
handshake with session-behavior capabilities, canonical
requests/notifications (including end-to-end `skills/configure`), error
codes, an event-grammar doc, and a transport-agnostic conformance kit
(12 rules, incl. full streaming-delta coverage and zero-work-prompt
settlement).
2. **Four conformant bridges** — acp, pi, claude-code rewritten onto the
canonical dialect (translation extracted byte-identically into shared
modules both paths import); codex is the one new bridge, owning
per-thread `codex app-server` children with the #1402 supervision rules.
One generic `BridgeProviderAdapter` replaces per-provider adapter glue.
Turn-start watchdog (`provider_turn_start_timeout`).
3. **Server registry + SDK** — `ProviderRegistryService`
(catalog-equality pinned), `bb.agents.experimental_registerProvider`,
and `app.slots.experimental_providerIcon` (plugins ship theme-aware
React brand marks; `logoUrl` files remain for static color logos). Both
have `docs/api_to_audit.md` entries.
4. **First-party plugins** —
`plugins/provider-{codex,claude-code,pi,acp}` take over their core-seed
entries in place (position preserved, restored on disable, takeover
merge preserves flagship behaviors) and register their brand marks as
app components. The app no longer depends on `@bb/agent-providers`.
5. **Third-party artifact delivery** — `bb.providerBridge` manifest key
builds a self-contained bridge bundle; server stores/serves it
content-addressed; daemon caches by verified hash; a verified
`bridgeLaunch` (carrying the declaration's validated execution
capabilities) is its own routing authority.
`HOST_DAEMON_PROTOCOL_VERSION` 123 → 124 (the PR's only bump; main took
123 for artifact engine ranges). `examples/plugins/echo-provider` proves
the path end to end.

## Graduation (waves 1–5)

Each wave ended with a full board (typecheck + all suites) before the
next started, and every deletion first scanned the dying adapter's tests
for uncovered shared-module invariants and **moved** them.

1. **The pinned gap, then acp + pi** — the codex zero-work-prompt
settlement fix landed first (dispatch-ownership seam, no fabricated
turns), unpinning the last non-green conformance rule; acp's and pi's
legacy adapters were deleted.
2. **claude-code, then codex** — the last two legacy adapters deleted.
**No legacy adapter remains anywhere.**
3. **Flag and core-catalog retirement** — the `providerBridge`
experiment, the whole provider-bridge-policy endpoint, and the daemon's
prefix capture are gone; the registry's core seed is deleted and
`@bb/agent-providers` no longer exists. The runtime's codex special
cases were audited one by one and consciously kept (one stale comment
fixed). v124 was unshipped, so no further bump was needed.
4. **Phase-6 consolidation sweep** — one commit per item, each
red-verified: ACP manual compaction as a per-agent declaration; the
skill provider id opened and the six per-provider skill scopes collapsed
to `provider-user`/`provider-project`; ask-user-question reading
`supportsNativeUserQuestion`; edit-message eligibility reading the
declared `supportsNativeSessionRewind`; plan mode gating on the declared
`plan` composer action (plus deleting the thread-view display-name
switch); and the `thread/openWork` notification that finally gives
`hasOpenThreadWork` a canonical implementation and stops the reaper
killing codex native subagents.
5. **First-party artifact migration** — two moves. First the **bridge
kit**: a plugin-shipped bridge cannot import `@bb/agent-runtime`, and
nearly every module under `agent-runtime/src/shared/` turned out to have
only provider-side consumers, so they moved wholesale (with their tests)
to `@bb/provider-bridge-protocol/bridge-kit` — JSON-RPC plumbing, the
stdio harness, tool-call/interaction codecs, id scoping, visibility
metadata, translation helpers, and the runtime↔bridge structural types;
bridge test infrastructure became
`@bb/provider-bridge-protocol/testing`. Then the bridges themselves:
**codex** (868 KB), **acp** (919 KB) and **claude-code** (2.44 MB) now
ship as self-contained, content-addressed plugin artifacts on the same
route as `echo-provider`, with every core carve-out removed end to end
(daemon bundle target, bb-app `files` entries, launcher assertions,
registry branches, catalog capability/session-restore baselines) and the
tarball smoke driving the *packed* artifacts.

**Pi is the one documented exception, and the evidence is in the plan**:
a fully-inlined 14.8 MB artifact was built and driven through the real
fixtures plus this machine's live pi config. It fails for three reasons,
none of them module identity (an identity-divergent control run passed
everything): pi-coding-agent's extension loader resolves itself by
`__dirname` with no fallback, `import.meta.resolve` for the pi packages
has no `node_modules` ancestor in the daemon cache, and pi-ai's OAuth
loader is deliberately bundler-hostile. The only shape proven to work is
a *directory* artifact (bridge + a real npm tree), which is future
design work. Pi's bridge therefore stays daemon-bundled — the only
bundled bridge left — while its daemon coupling was still removed
(`createConfiguredPiSettingsManager` inlined, the `@bb/agent-runtime`
re-export deleted).

After wave 5 every provider-specific line lives in `plugins/provider-*`;
`@bb/agent-runtime` keeps only the protocol, the generic adapter, and
supervision. The deliberate exceptions still id-switched in core are
listed in the plan (skill-root union normalizers, `isAcpProviderId`, the
`acp-cursor` launch table, codex error-text classification).

## Verification

- **Final validation gate — live QA, all pass.** Every provider through
its graduated route with real CLIs: spawn, multiple turns,
steer/stop/resume, fork, archived resume, models, staged bb skills
reaching the CLIs, and accept-edits writes landing in thread storage.
Plus the paths graduation specifically put at risk: plugin
**disable/re-enable**, a **plugin update with a new artifact hash
mid-session**, and a **fresh install against an empty data dir**.
Process-tree and orphan sweeps clean; zero error events; CI green.
- **Multi-agent adversarial review of the graduation diff**: **49 raw
findings → 8 confirmed** (every finding survived only if a dedicated
skeptic failed to refute it), and **all eight are fixed with
red-verified tests**. The sharpest class was the one that only exists
after graduation: `thread.archive` and `thread.unarchive` carried no
`bridgeLaunch`, so every graduated provider threw `Unsupported provider`
— unarchive always (it runs on a fresh provider-maintenance runtime),
archive whenever the thread's process was not already live — and the
server's warn-log swallowed it. The launch spec is now threaded exactly
like `thread.start` (wire field → server attach → daemon artifact
resolve → runtime process key + adapter), and the codex account-restart
re-resume carries it on `ThreadRuntimeConfig` too. The other six: codex
never retracting `thread/openWork` on child exit (thread never
idle-reaped); dynamic ACP agents offered while the ACP plugin is
disabled; the boot race where the listener serves before plugins load
(now a bounded registry settlement gate awaited by thread create,
command building, provider listing and model loads); non-reactive
execution-options reads in ThreadDetailView; and custom ACP agents
labelled "ACP provider" in the skills library.
- **Earlier live QA, three full passes** (pre-rebase, post-rebase,
default-on gate): all four providers through the canonical bridges,
verified by process trees and event streams, including a
daemon-restart-under-stalled-server policy check.
- **Historical-bug audit**: ~30 past provider fixes classified; every
invariant is bridge-tested, shared-by-construction, or has a dedicated
bridge-path suite (codex translator, ACP event-translation/interactions,
pi `none`→`off`, claude rate-limit classification), each verified red
before landing green.
- **Dual-path calibration replay** for all four providers while both
paths existed: normalized event streams diffed by LCS, **no unexplained
parity divergence on any provider** — only deliberate protocol
differences. Post-graduation the suites remain as bridge-only
self-consistency checks.
- **Conformance is all green for all five bridges** — the codex
zero-work-prompt pin was flipped, not deleted.
- Full-tree typecheck and tests green; CI green.

## Follow-ups (tracked, not in this PR)

- **Pi externals design** — the directory-shaped artifact (bridge `.mjs`
plus a pruned npm tree) that would let pi leave the daemon bundle; the
empirical findings are recorded in the plan.
- **Auto-mode workspace gap** — surfaced during the gate's live QA,
independent of the provider surface.
- **Claude dropped-message-after-stop** — a reproducible upstream-SDK
behavior found during QA, written up as its own report rather than
patched here.
- **Deferred phase-6 items**, in two groups: (a) the three sessionless
daemon→bridge reads — `skills/scanRoots`,
`provider/health|install|update`, `provider/usage` — which share one
transport and should land as their own wave; and (b) the four surfaces
blocked on new plugin API rather than on consolidation — provider-scoped
settings toggles, onboarding/settings entries, the provider-retry banner
label, and provider icon colors. Both groups want a plugin-facing
provider directory, which does not exist on either SDK surface today.
- Smaller: env-var live-change semantics, fork-UI reactivity minors, ACP
write-approval presentation.

> AGENT GENERATED: by Claude Fable 5

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
yoanyombapro1234 added a commit to PlaybookMediaLLC/bb that referenced this pull request Aug 22, 2026
* Survive a broken pipe to the watcher child (#1506)

Fixes #1505.

## Problem

`ChildChannel.send` guarded the IPC write with `child.connected` alone:

```ts
send(message: ParentToChildMessage) {
  if (child.connected) {
    child.send(message);
  }
},
```

That flag stays `true` while the channel is tearing down, so the send
can still fail with `EPIPE`. The proxy pings the child from a bare
`setInterval`, so there is nothing above it to catch — the throw escapes
as an `uncaughtException` and kills the host daemon, taking every thread
on the host with it (`Thread interrupted because the host daemon
disconnected`).

The race opens widest when the daemon is already unhealthy: a stalled
event loop delays the ping timer past the child's unprocessed `exit`
event, so the timer fires at a child that is already gone. On my host
that meant heartbeat gaps of up to 403s, then `write EPIPE`, then 5 dead
threads. Full evidence in #1505.

## Fix

A failed send means "this child is gone" — which is precisely the
condition `createParcelWatcherProxy` already recovers from by killing,
respawning, and replaying every subscription. So route it there instead
of throwing:

- `try/catch` around `child.send` **and** a send callback, covering the
synchronous throw and the async write error.
- `child.on("error", ...)` — an unhandled `'error'` event on a
ChildProcess is the same fatal outcome by another route, and there was
no listener at all.
- SIGKILL before reporting the child gone, so a process that outlived
its pipe doesn't leak the inotify fds and threads its watches allocated.
- `onExit` listeners moved behind a `gone` latch, so a pipe failure
followed by the real `exit` doesn't drive two respawns.

`createChildChannel(child)` is split out from `createForkChannel()`
purely as a test seam — a real EPIPE only reproduces under a race.

## Tests

`packages/host-watcher/test/fork-channel.test.ts`, 5 cases: synchronous
EPIPE, async send failure, `'error'` event,
exit-reported-once-and-no-further-sends, and the healthy path.

These are real regression tests, not just coverage — reverting the guard
makes 4 of the 5 fail with exactly the EPIPE escape:

```
FAIL test/fork-channel.test.ts > swallows a synchronous EPIPE and reports the child as exited
FAIL test/fork-channel.test.ts > treats an asynchronous send failure the same way
FAIL test/fork-channel.test.ts > handles a child 'error' event rather than leaving it unhandled
FAIL test/fork-channel.test.ts > reports exit exactly once and stops sending after a failure
```

With the fix, all 44 `@bb/host-watcher` tests pass; `turbo run typecheck
--filter=@bb/host-watcher` and prettier are clean.

## Scope

This is the crash only. The event-loop stalls that widen the race are a
separate problem — after this change the watcher recycles cleanly
instead of taking the daemon down with it. No server↔daemon wire change,
so no `HOST_DAEMON_PROTOCOL_VERSION` bump.

> AGENT GENERATED: by Claude Opus 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>

* Rename @bb/plugin-sdk to @get-bb/plugin-sdk (#1574)

Layer 1 of 4 in the plugin-SDK npm publishing stack (#1578). Targets
`main`.

Renames the plugin SDK package to `@get-bb/plugin-sdk` so it can publish
publicly under the org scope, and adds `publishConfig.access: "public"`.
Pure rename plus that one field: all imports and workspace deps (app,
server, CLI, templates, 19 in-repo plugins/examples), the bundled-types
banner (regenerated by the build, not hand-edited), scaffold tsconfig
path maps, docs, and skills.

Deliberately unchanged: `engines.bbPluginSdk`, `sdkMajor`/`sdkVersion`,
`PLUGIN_SDK_VERSION`, the `bundled-types/bb-plugin-sdk*.d.ts` file
names, and every other `@bb/*` package name. The plugin-build app shim
keeps `"@bb/plugin-sdk/app"` as a legacy alias so pre-rename plugin
sources still build until the migration window closes.

Known and accepted: the export map's `source` conditions point at
`src/`, which the tarball excludes. They stay because `apps/app`'s vite
config resolves `conditions: ["source"]` for workspace dev; standard
resolvers ignore unknown conditions, so external consumers are
unaffected unless they opt into a custom `source` condition.

Verified: project-wide stale-name grep (one deliberate legacy alias
remains), typecheck across 7 packages, SDK 82 + templates 36 tests
including the external scaffold test that npm-packs the tarball and
installs it into a fresh scaffold. Reviewed by GPT-5.6-Sol; both
findings addressed.

> AGENT GENERATED: by Claude Fable 5 (implementation: Claude Opus 5;
review: GPT-5.6-Sol)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Publish @get-bb/plugin-sdk with the release train (#1575)

Layer 2 of 4 in the plugin-SDK npm publishing stack (#1578). Requires
#1574 (the rename) below it.

Closes #1134.

Adds a `publish-plugin-sdk` job to `publish-bb-app.yml`: build the SDK,
then publish only when the current version is absent from npm
(idempotent skip otherwise), via OIDC trusted publishing in the
`npm-release` environment. The job has no dependents, so a failure
cannot block the bb-app or desktop publishes. It honors
`RELEASE_DRY_RUN`, and always publishes to `latest` — the SDK has no
channel concept.

Adds the pin-to-host CI guard: `check-npm-version-guard.mjs` fails a PR
when the committed `bundled-types/` differ from the published content of
the current `PLUGIN_SDK_VERSION`, so npm can never silently lag the API.
Package/version-not-found passes (pre-bootstrap state); registry outages
exit 2 and surface as CI warnings, not failures. Decision logic is a
pure function with a unit-tested decision matrix.

Drive-by hardening from review: the main-only guards (both the new SDK
job and the pre-existing bb-app job) now compare `github.ref ==
'refs/heads/main'` — the old `ref_name` check accepted a tag named
`main`.

One-time bootstrap after this lands (documented in
`docs/bb-release-process.md`): manual first `npm publish`, then add the
trusted publisher entry on npmjs.com pointing at this workflow file and
the `npm-release` environment.

> AGENT GENERATED: by Claude Fable 5 (implementation: Claude Opus 5;
review: GPT-5.6-Sol)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Scaffold plugins against the published SDK instead of vendored types (#1576)

Layer 3 of 4 in the plugin-SDK npm publishing stack (#1578). Requires
#1575 (the publish pipeline) below it — a fresh scaffold pins a version
that must exist on npm, so this must not merge before the bootstrap
publish has run.

`bb plugin new` stops writing `types/` and the tsconfig path maps. The
generated package.json instead carries an exact-pinned
`@get-bb/plugin-sdk` devDependency equal to the host's
`PLUGIN_SDK_VERSION`, alongside the existing `engines.bbPluginSdk`
floor. After scaffolding, the CLI probes the registry and warns — never
fatally — when the host's SDK version is a positive registry miss (with
an `npm pack` + `file:` workaround); an unreachable registry only
reports the pin as unverified.

Legacy plugins are untouched: `resolvePluginSdkLayout` classifies a
plugin as vendored when either declaration file exists under `types/` or
a tsconfig path map (old or new package name; JSONC tolerated via
raw-text fallback) is present — ambiguity resolves to vendored, because
refreshing too much is recoverable and silently stopping a refresh is
not. `bb plugin build`/`dev`/`types` behave byte-for-byte as before on
vendored layouts and never write `types/` into package-layout plugins.

Verified: the external scaffold test now installs the packed tarball as
the scaffold's own dependency and typechecks with `skipLibCheck: false`
and no path maps, proving resolution from `node_modules`. Reviewed by
GPT-5.6-Sol; all three findings addressed (detector robustness, README
overclaim, probe wording), plus a fourth found in orchestration
(old-name path maps).

> AGENT GENERATED: by Claude Fable 5 (implementation: Claude Opus 5;
review: GPT-5.6-Sol)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Migrate vendored plugins to the published SDK on request (#1577)

Layer 4 of 4 in the plugin-SDK npm publishing stack (#1578). Requires
#1576 (the scaffold switch) below it.

Adds `bb plugin migrate [path] [--yes]`: a consent-gated, idempotent,
one-shot conversion of a legacy plugin to the package layout — exact
devDependency pin, engines floor raised but never lowered, old-name and
new-name path maps removed (other paths like `@/*` kept), vendored
declaration files deleted, `types/` removed only when empty. The full
plan prints before any write; non-TTY without `--yes` prints the plan
and exits nonzero having changed nothing; symlinks and out-of-tree paths
abort before the first write.

`bb plugin types` now manages the pin on package-layout plugins: the
default invocation repins to the running host's SDK version (reporting
old → new, reminding to `npm install`), while `--check` stays read-only
with a nonzero exit on mismatch. Vendored behavior is unchanged, and
`build`/`dev` print a one-line migrate hint. The guide, `bb-cli`, and
`bb-plugin-authoring` surfaces document the new command, including an
explicit instruction that agents never migrate a plugin unasked.

Verified: 16 new tests (transform matrix, idempotency, symlink refusal,
non-TTY refusal, repin/--check), plus an end-to-end run of the real
built CLI against a hand-built legacy plugin in /tmp.

> AGENT GENERATED: by Claude Fable 5 (implementation: Claude Opus 5;
review: GPT-5.6-Sol, 9 findings fixed or documented)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Bump the plugin SDK to 0.4.4 and add a one-command bump script (#1588)

Fixes the red CI on `main`.

## The failure

The `checks` job failed at **Check plugin SDK npm version guard**:

```
@get-bb/plugin-sdk@0.4.3 is already published, but the package this
checkout would publish differs from it:
  - README.md
```

#1576 edited `packages/plugin-sdk/README.md`, which ships inside the npm
tarball (`files` lists it). #1575 had already published 0.4.3. The
publish job is publish-if-missing, so the corrected README could never
reach npm consumers. The guard was right to fail.

## The fix

Bump `PLUGIN_SDK_VERSION` and the `plugin-sdk` manifest to 0.4.4. That
is the whole fix. Since #1569 made `engines.bbPluginSdk` a floor rather
than a ceiling, the `>=0.4.3` pins across `examples/plugins/*`, the
first-party `plugins/*`, and the scaffold keep resolving and do not
move.

## Why a script

The fix is two files that must stay in sync, hand-edited.
`scripts/bump-plugin-sdk.mjs` makes it one command:

```bash
node scripts/bump-plugin-sdk.mjs --patch
```

It writes both files atomically, refuses to run when they already
disagree, and anchors its rewrite to the `PLUGIN_SDK_VERSION` export so
the version numbers in the surrounding comment are not rewritten. The
guard failure message now names the command, matching how
`check-version-lockstep.mjs` points at `scripts/bump-version.mjs`.

Semver parsing, comparison, and bump derivation move to
`scripts/lib/semver.mjs`, shared with the existing `bump-version.mjs`
rather than copied.

## Verification

- 5 new tests: both files move together, comment prose survives,
pre-existing drift is refused, a non-advancing version is refused, and
the manifest rolls back when the second rename fails.
- The 7 existing `bump-version` tests pass unchanged, covering the
semver extraction.
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` → `PASS
— 0.4.4 is not on npm yet`.
- `build typecheck lint` green for `@bb/domain`, `@get-bb/plugin-sdk`,
`@bb/scripts`; tests green for those plus `@bb/templates`.

## Note

The version numbers left in `docs/configuration.md` and the plugins
guide are illustrative examples of the floor rule (`^0.4.1` still works
after the SDK moves to `0.4.3` or later), not claims about the current
version, so they stay put. Chasing them on every patch bump would be
churn the script cannot maintain.

> AGENT GENERATED: by Claude Opus 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix pending interaction banner overflow (#1586)

## Summary

- Keep pending interaction banners within the available composer width.
- Keep long commands horizontally scrollable.
- Wrap long paths and action details inside the approval card.

## Root cause

The banner and command container kept their intrinsic minimum width.
Long command and detail text could therefore expand the parent grid item
beyond the viewport.

## User impact

Approval cards now remain inside the thread view when commands, paths,
or action details are long.

## Validation

- `pnpm exec turbo run typecheck --filter=@bb/app`
- `git diff --check`

> AGENT GENERATED: by GPT-5

* Move split dimming control to Appearance settings (#1605)

## Summary

- move the split dimming preference from pane headers to Settings >
Appearance
- label the preference as Fade inactive splits and place it at the
bottom of the list
- keep the existing local storage behavior and add a focused settings
test

## Tests

- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/settings/SplitDimmingSetting.test.tsx
src/views/thread-detail/SplitThreadArea.test.tsx
src/views/thread-detail/ThreadDetailHeader.test.tsx`
- `pnpm exec turbo run lint --filter=@bb/app`
- Prettier check for all affected files

> AGENT GENERATED: by GPT-5

* Release agent runtimes on thread stop (#1584)

## Summary

- Make `threads.stop` release active and idle agent runtimes.
- Make daemon stop idempotent when no runtime remains.
- Wait for runtime release through the public route and plugin SDK.
- Document prompt cleanup for hidden plugin workers.
- Increase the host daemon protocol version to 120.

## Why

Plugins can create many hidden worker threads. Idle workers kept
provider processes until delayed archive cleanup, which could exhaust
host memory.

`threads.stop` now owns runtime release. It preserves thread state and
history, so callers can stop without archive and resume later.

Closes #1573.

## Validation

- Relevant Turbo type checks passed.
- Focused server tests passed: 2 files and 3 tests.
- Host daemon, SDK, CLI, plugin SDK, contract, and template suites
passed.
- The full server suite passed 1,499 tests.
- One unrelated file-mode test failed because the local umask produced
0664 instead of 0644.
- Live plugin CLI tests passed with real Codex and Claude Code workers.
- A stop-only Claude test removed the process, kept `archivedAt` null,
and resumed the same thread.

> AGENT GENERATED: by GPT-5 Codex


---

## Follow-up: separate a release from an interrupt

SlopCop found three defects in the awaited stop path. All three were
real, and
this branch now fixes them.

- An idle release reused the interrupt settlement. It appended a false
`system/thread/interrupted` event and interrupted the pending
interactions of
  a thread that nobody interrupted. A probe test reproduced the event.
- The daemon waited for an active turn that an idle thread never has, so
every
  release burned the full five-second timeout.
- A turn that completed between the caller's read and the stop
transaction made
  `stop.requested` a no-op, so the route returned before it released the
  still-loaded runtime.

`thread.stop` now carries a required `intent`. An `interrupt` keeps the
active-turn wait and the interrupt settlement. A `release` skips both,
so an
idle thread keeps its status, its timeline, and its pending
interactions. The
lost race falls through to a release instead of returning.

Two further issues found while fixing those:

- The awaited stop used `LIVE_DAEMON_COMMAND_TIMEOUT_MS`, which is 24
hours. A
wedged daemon held the HTTP request open. It now uses a one-minute
bound.
- The route returned 500 when the host was offline, where the old
fire-and-forget stop always returned `ok`. An unreachable host holds no
  runtime to release, so the release reports success.

The awaited stop also runs under the in-flight guard, so a duplicate
concurrent
stop no longer sends a second RPC.

Increase the host daemon protocol version to 121.

The branch is rebased onto `main`, which already carries the plugin SDK
0.4.4
bump from #1588. That version is not on npm yet, so the changed bundled
types
ship inside it and need no further bump.

### Validation

- Repo-wide `turbo run typecheck` passed: 58 tasks.
- Host daemon suite passed: 555 tests. Contract suite passed: 50 tests.
- CLI, SDK, plugin SDK, and templates suites passed.
- Server suite passed 1,501 tests. The one failure is the same unrelated
  file-mode test; it passes under `umask 022`.
- New tests cover all three findings: a release appends no interruption,
an
  interrupt still settles the thread, and the lost race still releases.
- A new daemon test asserts a release skips the active-turn wait.

### Second review round

A second SlopCop round on the fix found three more defects. All three
were
real, and all three are fixed.

- A release could lose a race with a turn that started after the server
read
the thread. The daemon would stop accepted work and leave the server
holding
  an active thread with no runtime. A release now skips a busy runtime.
- A concurrent stop returned before the first release finished. Awaited
stops
  now share one RPC and one result through a per-thread deduper.
- A failed release reported success. A release now propagates its
failure. An
interrupt still swallows one, because `stopping` is durable and the
backstops
  settle it. An unreachable host stays a success.

Four more tests cover these: the release-versus-start race, the shared
concurrent result, the propagated failure, and the disconnected host.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Plugin collection manifests and nested git installs (#1579)

Bottom layer of the plugin marketplace stack (1/4). Implements #1097.

## Summary
- Adds the repository collection manifest `.bb/plugins.json`: a strict,
versioned index (`schemaVersion: 1`) naming nested plugins by relative
path, with duplicate-name, traversal, root-source, and
symlink-containment validation. The JSON schema is published at
`apps/web/public/schemas/plugins.schema.json`.
- Adds nested installs: `bb plugin install git:<url>[@ref]
--subdirectory <path>` as the primitive and `--plugin <name>` to resolve
a manifest entry; both also work for `path:` sources. An unselected
install into a manifest-bearing repo with no root plugin fails listing
the available entries.
- Persists the subdirectory end to end (`sourceGitSubdirectory` was
already plumbed through updates/rollback; installs now fill it), and
makes plugins from one repo@commit share a single cached checkout —
promotion moves only the selected subdirectory, with crash recovery, GC
protection for live siblings, and refreshed ancestor hashes.
- Ships the marketplace design doc (`docs/plugin-marketplace-plan.md`)
that the rest of the stack implements.
- Surfaces updated in the same change: CLI flags/help, install route
`selection` contract, SDK, `bb guide`, bb-cli and bb-plugin-authoring
skills.

## Validation
Turbo typecheck workspace-wide; server/app/cli/db suites green on this
branch standalone (one known machine-local umask failure in
`internal-skill-trees.test.ts`). New tests cover schema validation,
symlinked manifests/entries, shared-checkout promotion with sibling
integrity, nested update checks, and rollback.

Reviewed by a single adversarial pass plus a whole-stack final review;
findings were fixed in-branch.

> AGENT GENERATED: by Claude Fable 5

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* BB Official marketplace catalog with last-known-good refresh (#1580)

Layer 2/4 of the plugin marketplace stack. Prerequisite: #1579
(collection manifests and nested installs).

## Summary
- Adds the marketplace manifest schema (strict, versioned): entries
carry id, display metadata, icon (host name or URL), tags, required
author, optional narrowing-only `engines`, and an npm (range XOR
dist-tag) or git (url/subdir/ref) source. Published at
`apps/web/public/schemas/marketplace.schema.json`.
- Generalizes provenance: catalog rows carry a marketplace name
(`bb-official` reserved; existing rows backfilled via generated Drizzle
migration 0094), flowing through registration, snapshots, and rollback
with legacy-shape compatibility.
- Adds catalog persistence and a last-known-good refresh loop:
conditional HTTP (ETag/Last-Modified) on startup and every 6h with an
injected fetch; failures, invalid payloads, and identity drift retain
the last valid catalog; a refresh writes discovery metadata only and
never runs plugin code. Catalog + icons publish in one transaction.
- Server-side icon pipeline: fetch at refresh, validate (compact-SVG
sanitizer, PNG/WebP magic bytes, 256 KB cap), cache, and serve from BB's
origin with nosniff/CSP — the app never hot-links external icon URLs.
- Replaces the static `GIT_OFFICIAL_PLUGINS` with a schema-validated
bundled seed snapshot; catalog installs route through the normal
pipeline with subdirectory selection and the engines-widening check
restored from the retired marketplace design.
- Adds the `bb-web` `/marketplace/v1/*` route reading an optional
`MARKETPLACE` R2 binding (ETag pass-through, cache headers, 404 when
absent), plus the `BB_MARKETPLACE_URL` config knob.

## Validation
Standalone green: workspace typecheck;
server/app/cli/db/config/templates/web suites (known umask failure
aside). Tests cover widening, refresh LKG semantics, icon rejection,
migration provenance states, legacy snapshot restore, and install-dialog
source identity.

> AGENT GENERATED: by Claude Fable 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Resolve git plugin sources from semver tags (#1581)

Layer 3/4 of the plugin marketplace stack. Prerequisite: #1580 (official
marketplace catalog).

## Summary
- Git plugin sources can now specify a semver range resolved against
tags: `git ls-remote` listing with annotated-tag peeling,
`[tagPrefix]vX.Y.Z` filtering, highest-satisfying selection with
npm-parity prerelease rules. `tagPrefix` supports monorepo per-plugin
tagging.
- Records the resolved tag and its commit, and refuses any later
resolution where a recorded tag points elsewhere — a moved tag is a
security error naming both commits, applied to range tracking and
exact-tag pins alike (legacy rows without a recorded commit are handled
without false positives). Missing recorded tags stop updates.
- Direct-install syntax: `git:<url>@^1.2.0` resolves over tags when no
ref of that literal name exists; a collision fails loudly with
`@ref:<name>` / `@semver:[<prefix>:]<range>` escape hatches. Bare
version tokens (`v1`, `v1.2.3`) stay literal tags. `--tag-prefix` added.
- Persistence via generated Drizzle migration 0095 models the selector
as a union (ref pair XOR range trio) with corrupt-row rejection;
marketplace git entries gain the `range`/`tagPrefix` variant with
JSON-schema parity, generating explicit `@semver:` specs so a
coincidental branch name can never shadow a listing's intent.
- Fixes two latent bugs found en route: a crash on `*` ranges in
prerelease detection (affected `npm:pkg@*`), and silent 8 KB output
truncation that could resolve a range against an incomplete tag listing
(now a byte-bounded declared cap that fails past the limit).

## Validation
Standalone green: workspace typecheck; server/cli/db/integration suites
(known umask failure aside). Tests cover tag filtering/ordering,
prerelease in/out, moved-tag refusal for both selector kinds, legacy
exact-tag movement, ambiguity errors, and corrupt selector rows.

> AGENT GENERATED: by Claude Fable 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Third-party plugin marketplaces (#1582)

Top layer (4/4) of the plugin marketplace stack. Prerequisite: #1581
(git tag semver).

## Summary
- Adds the marketplace registry: `bb marketplace add | list | refresh |
remove` over new validated routes, accepting an https manifest URL,
`git:<url>[@ref]`, or `path:<dir>`. The manifest's own name is its
identity; collisions and the reserved `bb-official` are refused;
identity drift on refresh is rejected. Adding installs nothing.
- Install routing: `bb plugin install <entry>@<marketplace>`; a bare id
resolves across marketplaces (one match installs, several fail listing
qualified choices), with every existing source form untouched.
- Trust UX: a new install-plan endpoint reports the true resolved source
(npm package/range/registry, or git url/ref/range/subdir plus the exact
tag and commit) before anything runs; the CLI prompt and app dialog
render it, and the server verifies the approved facts — including the
git commit — at install time.
- Server-side fetch policy for third-party content: HTTPS on 443 only,
public DNS results, no credentials, no redirects; applied to manifests
and icons. Local marketplaces use real-path containment; git
marketplaces stage into a throwaway checkout that is cleaned up on the
next operation after a crash.
- Removal disposition: one transaction converts installed plugins to
`direct` provenance retaining full source intent and exact resolution —
update checks keep working afterward — then deletes catalog rows and
icons. Re-adding the same marketplace works.
- Browse groups by marketplace (official first) with tag-derived
sections; cards show authors and, for third-party entries, their
marketplace. Settings gains a Plugin marketplaces section mirroring the
CLI. SDK (`plugins.marketplaces.*`, `catalog.installPlan`), guide, and
skills updated in the same change.

## Validation
Standalone green: workspace typecheck and lint;
server/app/cli/db/integration suites (known umask failure aside).
End-to-end tests cover the add → install-with-plan → range update →
moved-tag refusal → removal → still-updates-as-direct lifecycle, icon
isolation, SSRF policy, and re-add.

No server↔daemon wire shape changes anywhere in the stack; protocol
version 119 is unchanged deliberately.

> AGENT GENERATED: by Claude Fable 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Unify new-thread composition and secondary-panel layout (#1559)

## Summary

- add one shared new-thread composer that owns picker configuration,
drafts, attachments, project switching, and request construction for
both the root page and plugin SDK adapter
- add one shared secondary-panel layout that owns desktop sizing,
compact drawer realization, native browser readiness, and hosted-pane
registration for root compose and thread detail
- leave page-specific welcome, fork, machine setup, metadata, timeline,
and macOS chrome behavior in thin wrappers
- centralize behavior-heavy tests and remove the resolved
duplicated-assembly audit item

## Testing

- pnpm exec turbo run typecheck --filter=@bb/app
- pnpm exec turbo run test --filter=@bb/app --force (341 files, 2,704
tests)
- pnpm exec turbo run build --filter=@bb/app

> AGENT GENERATED: by GPT-5 Codex

* fix(terminal): answer fish device queries before attach

Handle DA1 queries at the host PTY boundary so shells do not time out while the browser terminal is detached. Remove handled queries from replay output to prevent duplicate responses after reconnects.

Amp-Thread-ID: https://ampcode.com/threads/T-019ffee9-be44-75c0-bd8a-1c653b61f62a
Co-authored-by: Amp <amp@ampcode.com>

* fix(terminal): bound automatic DA1 replies to one write

Answer at most 8 DA1 queries for each PTY output chunk with a single write.
A flooded chunk could otherwise fill the unbounded node-pty write queue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(acp): support generic session forking

Negotiate the unstable ACP fork capability, route session/fork through the existing thread fork path, and persist the returned child session identity.

Amp-Thread-ID: https://ampcode.com/threads/T-019ffeea-31bc-70de-833b-bf6045f8578d
Co-authored-by: Amp <amp@ampcode.com>

* feat(acp): guard generic session forks

Reject a cross-provider fork at the server boundary, a checkpoint fork that
ACP session/fork cannot honor, and a fork result that reuses an active
session ID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add provider session release experiment

* Guard the provider session release sweep

Report Codex native subagents as open thread work, isolate a failed release
from later candidates, refresh ACP restore support when a settings change
replaces the session, and state that the experiment extends existing Codex
release rather than gating it.

Protocol version 122 now also covers the terminal device-attribute handler and
the ACP fork route that ship in the same stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Allow thread execution overrides for all providers (#1610)

## Summary

- remove the stale Claude-only gate for sticky thread model and
reasoning overrides
- validate every override against the thread provider's active model
catalog, while retaining the requirement to start a new thread when
changing providers
- add route coverage for Codex, Claude Code, Pi, and ACP and document
how sticky overrides differ from per-turn follow-up overrides

## Context

The original implementation explicitly gated Codex out for v1. That
policy became stale: follow-up requests already send changed model and
reasoning settings through the same next-turn/runtime reconfiguration
paths, and the Codex, Pi, and ACP resume adapters all apply those
settings today.

Deleting the now-always-supported capability avoids rejecting future and
dynamically configured ACP providers through a second provider
allowlist.

## Testing

- `pnpm exec turbo run test --filter=@bb/server --
test/public/public-threads.execution-override.test.ts
test/services/threads/thread-execution-override.test.ts`
- `pnpm exec turbo run test --filter=@bb/agent-providers
--filter=@bb/templates`
- `pnpm exec turbo run typecheck --filter=@bb/server
--filter=@bb/agent-providers --filter=@bb/templates`

> AGENT GENERATED: by GPT-5

* Fix long streamed messages across timeline completion (#1568)

## Summary

- preserve the complete text of delta-only assistant, plan, and
reasoning items when an active turn crosses the timeline event budget
- supersede in-flight timeline reads and cancel paced trailing refetches
when `turn/completed` arrives, preventing an active-turn projection from
landing after the canonical completed projection
- snap the authoritative active-to-completed timeline height replacement
while preserving normal streaming height transitions and observer state
- add provider-shaped regression coverage for the sequence-window
closure and client completion handoff

## Problem

Pi, Claude, and ACP can begin assistant text with
`item/agentMessage/delta` and never emit `item/started`. When an
unfinished turn crossed the event budget, whole-item closure only
recognized buffered text through a backfilled `item/started` row. A
window cut inside a delta-only message therefore projected only the
suffix inside the budget instead of carrying its earlier deltas forward.

Completion then replaced that suffix with the canonical full
`item/completed` text. The normal streaming invalidation path
deliberately avoids cancelling active reads and schedules a paced
trailing refetch, so an older suffix projection could also land around
the terminal boundary before the trailing fetch restored the completed
shape. Animating the whole timeline height made these replacements look
like the response was streaming in reverse.

## Fix

Whole-item closure now identifies unfinished buffered text from either
its lifecycle row or its in-window delta event type, then uses the
existing prefix query to reconstruct the complete item. This is
provider-independent and also handles already-stored delta-only threads.

The client keeps its non-cancelling behavior for ordinary stream events.
Only the authoritative `turn/completed` boundary cancels obsolete work
and immediately requests the terminal shape. The completed-turn topology
replacement snaps through the existing zero-duration height path without
reinstalling the `ResizeObserver`, so settle and resize state remain
intact.

## Testing

- full `@bb/app` suite — 341 files and 2,707 tests passed
- full `@bb/server` suite — 166 files and 1,506 tests passed
- focused regressions after the final rebase — 46 app tests and 22
server pagination tests passed
- `pnpm exec turbo run typecheck --filter=@bb/server`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `git diff --check`

> AGENT GENERATED: by GPT-5.6

* Restore bounded rich untracked status and diffs (#1607)

## Summary

- enrich ordinary untracked workspace status with exact line counts when
the snapshot fits both a 50-file and 8 MiB budget
- keep watcher invalidation cheap and content-free while retaining rich
file paths and statuses for larger snapshots
- return an exact bounded leading slice for diffs above 500 files, with
an explicit truncated signal instead of rejecting the diff
- preserve full-diff rename classification while bounding both
name-status and numstat streams
- keep tracked and untracked stats independent, enrich eligible files
around nested repositories, and cap optional status enrichment with a
10-second deadline
- surface exact line counts for the shown truncated slice in the app and
preserve truncation in the CLI
- bump the host daemon protocol to 123 and the plugin SDK to 0.4.6 after
rebasing over the versions already claimed on main

## Why

2292db1e0 correctly bounded process count, output buffers, and untracked
content work, but it removed useful line stats from every untracked
status and made all diffs above the ceiling unavailable. This keeps
those resource guarantees while restoring exact presentation for
ordinary cases and useful partial results for oversized diffs.

## Verification

- affected typechecks across app, CLI, host daemon, server, contracts,
host workspace, domain, plugin SDK, and templates
- app: 2,722 tests passed
- host daemon: 562 tests passed
- host workspace: 213 tests passed
- CLI: 449 tests passed
- plugin SDK: 86 tests passed
- domain: 141 tests passed
- templates and targeted host-daemon contract suites passed
- git diff --check

> AGENT GENERATED: by GPT-5.6

* Fix file preview line scrolling (#1611)

## Summary

- find Pierre source lines inside the renderer's open shadow root
- rerun target-line scrolling when the worker-backed code view mounts or
swaps to highlighted output
- center the target through the panel's vertical scroll offset without
touching Pierre's horizontal code scroller
- scope target lookup and cleanup to the active file preview
- add regression coverage for delayed worker readiness, shadow-root line
markup, and horizontal-scroll preservation

## Testing

- `pnpm exec turbo run test --filter=@bb/app --force -- --run
src/components/secondary-panel/FilePreview.test.tsx` (14/14)
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run lint --filter=@bb/app` (passes with existing
warnings)
- SawyerHood/dev-browser against the real dev app: clicked an assistant
`file://.../FilePreview.tsx#L1100` link with Pierre's inner code
scroller preset to `scrollLeft=240`; confirmed line 1100 was selected
and fully visible after vertical scrolling to `scrollTop=19417`, while
horizontal scroll remained exactly `240`

> AGENT GENERATED: by GPT-5.6 Codex

* Avoid unsolicited local network prompt in bb connect (#1608)

Delay local helper discovery until the user explicitly enables local editor integration, improve state-specific guidance, and document remote-browser setup and troubleshooting.

* Add marketplace plugin submission skill

* Address marketplace submission review

* Remove plugin submission CLI guidance

* Remove plugin SDK requirements from marketplace listings

A marketplace listing declared `engines.bb` and `engines.bbPluginSdk`, and
bb judged an entry's compatibility from those ranges before it fetched
anything. A listing's copy of a range is a second source of truth: it goes
stale the moment the plugin publishes a new version, and a stale range hides
a plugin that this bb can in fact run.

Drop `engines` from the marketplace schema, the published JSON schema, and
the bundled seed. The store now offers every listed entry, and the install
pipeline reads `engines.bb` and `engines.bbPluginSdk` from the fetched
plugin's own package.json and refuses there. That check already ran on every
managed git and npm install; catalog installs use the same paths.

This also fixes an empty first-run catalog: both seed entries declared
`bbPluginSdk: "^0.5.0"` while PLUGIN_SDK_VERSION is 0.4.5, so both evaluated
as incompatible and the Browse tab filtered them out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Enforce engine ranges on cached-artifact registrations

`registerInstalled` skipped `validateInstallDir` whenever a caller passed
`validated: true`, and every cache-hit path does. The engine and SDK checks
live inside that call, so `refuseEngineMismatch: true` was silently ignored
on exactly those paths: a cached artifact bb accepted before a version change
could register again even when this build cannot run it.

Move the check to `registerInstalled` itself, the one chokepoint every
managed registration passes through, so the plugin's package.json is read on
every path rather than only on a fresh materialization.

Found by SlopCop on #1617.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): warn when installing a path plugin from a managed workspace (#1565)

A path-sourced plugin installed from inside a bb-managed workspace
(<dataDir>/personal-workspaces/env_*/ or <dataDir>/worktrees/) is silently
deleted when that environment is destroyed (e.g. when the owning thread is
archived). The env teardown path in packages/host-workspace/src/provision.ts
recursively removes the workspace directory with no awareness of registered
plugins whose source_path lives beneath it, leaving the plugin permanently
"missing (reinstall)" with its source gone.

This is the cheapest of the three fixes proposed in #1565: emit a logger.warn
at install time so users know the source is ephemeral and can reinstall from a
stable path. It reuses the existing isBbManagedWorkspacePath predicate already
used for thread path-claims, and does not change install behavior.

Tested: new case in plugin-service.test.ts asserts the warning fires for an
install under <dataDir>/personal-workspaces/env_*.

Refs #1565

* Fix iOS terminal touch focus

* Harden terminal touch gesture detection

* Clamp child thread permission mode to the parent's mode

* Revert the Intel Mac desktop build (#1627)

## Summary

PR #1137 moved the macOS desktop job to a `macos-15-intel` runner and
made one Electron Builder invocation package both arm64 and x64. Desktop
release time went from about 12 minutes to about 40 minutes. Every step
paid the slower Intel runner, and notarization ran twice.

We do not want Intel Mac artifacts, so this reverts to arm64-only.

- macOS jobs in `build-desktop.yml` and `publish-bb-app.yml` return to
`macos-15` and verify arm64.
- DMG and ZIP targets return to `["arm64"]`.
- `desktop:build` and `package` pass `--arm64` again.

The Linux job stays as it is. It runs in parallel and does not extend
the critical path.

## Beyond a plain revert

A plain `git revert` was not sufficient, because #1501 added the Linux
job after #1137 landed.

1. The revert conflicted in both workflows and in
`apps/desktop/package.json`. The auto-merge also renamed the nightly
macOS **upload** artifact without renaming its matching **download**,
which would have broken the nightly publish job. All four
upload/download artifact-name pairs now agree.
2. `README.md` said Intel Mac and Linux users should run `npx`. PR #1501
never updated it, so the revert restored a sentence that is now wrong
about Linux. It now names macOS arm64 and marks the Linux AppImage
alpha. `apps/desktop/README.md` claimed "macOS-only", which contradicted
its own Linux section; the arm64 claim is now scoped to the macOS build.

## Test coverage

`electron-builder-config.test.ts` asserted the Linux arch but not the
macOS arch, so no test caught this regression. This PR adds the macOS
arm64-only assertion to the schema and a test that names the
release-time cost.

## Validation

```
pnpm exec turbo run test typecheck --filter=@bb/desktop --filter=bb-app --force
```

- Typecheck passes for `@bb/desktop` and `bb-app`.
- 191 desktop tests pass, including 22 in
`electron-builder-config.test.ts`.
- Four desktop test files fail on this Linux machine with "Electron
failed to install correctly": `desktop-window-factory`,
`existing-server-dialog`, `window-state`, `preload-build`. I confirmed
via `git stash` that the identical four fail on the unmodified base, so
this is a missing Electron binary in the local worktree, not a
regression.
- Both workflow files parse as YAML. `actionlint` is not installed
locally, so CI is the first real lint.

## Risks

The macOS artifact name changes from `bb-desktop-macos-arm64-x64` back
to `bb-desktop-macos-arm64`. Upload and download are updated together in
the same workflow run, so there is no cross-run dependency. Anyone on an
Intel Mac who installed the x64 build from a release this week will stop
receiving updates and should switch to `npx`.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Badge plugins by publisher and rename the curated marketplace to bb-community (#1628)

## What

The marketplace BB curates now lists as **BB Community**, and the store
says who published each plugin instead of calling everything "BB
Official".

The companion change is in `get-bb/marketplace`
([e4b9894](https://github.com/get-bb/marketplace/commit/e4b9894),
already on `main`), which renames the manifest and credits Bersabel
Tadesse for Prompt Improver and Thread Hover Cards.

## Publisher, not "official"

The server sends a `publisherLabel` and every surface renders that one
value:

| Plugin | Label |
| --- | --- |
| Bundled with the app | `BB Official` |
| Installed from the curated marketplace | `BB Community` |
| Listed by a third-party marketplace | that marketplace's display name
|
| Installed from a source by the user | none — no pill |

It shows beside the plugin name in Browse, on the detail page **before**
installation, in the installed list, and on the installed detail page.

The installed **Type filter** follows the same field, so its facets are
the publishers that actually installed a plugin plus `User`. Adding a
marketplace adds its facet with no list to update, and a selection is
intersected with the facets on offer so uninstalling the last plugin of
one cannot strand an empty list with no menu row left to clear.

Browse groups by publisher rather than by marketplace. Grouping by
marketplace filed all 13 bundled plugins under the curated marketplace's
heading and told the user BB Community wrote plugins that ship in the
build.

## `bb-official` → `bb-community`

The reserved name is the key BB registers the marketplace under, and it
must equal the `name` in the published manifest. **No released BB
registers this marketplace** — the feature commit is an ancestor of
neither `desktop-v0.37.0`, `desktop-latest`, nor `desktop-nightly` — so
the rename breaks no installed client. Renaming after release would have
permanently broken catalog refresh everywhere.

Development databases do have the row, because BB registers it on
startup. Migration `0098` moves the marketplace row, its cached icons,
and every catalog install together; a partial move would leave installs
naming a dead key and the store would list every entry twice. I hit
exactly that locally before writing it.

The `OFFICIAL_*` constants held the string `"bb-community"`, so they are
now `CURATED_*` and `official-marketplace.ts` is
`curated-marketplace.ts`. The `official` boolean keeps its name — it
means the reserved marketplace BB reviews, which is still true of this
one.

## Install confirmation

It called every catalog entry official, including community and
third-party listings. It now names the publisher, and a bundled plugin
drops the claim rather than repeating it. The full-trust note now names
what the trust covers — the computer, not just local bb data — and a
long git ref scrolls on one line instead of wrapping across three and
shoving the buttons down.

## Tests

- `turbo run typecheck` — 58/58 pass
- `@bb/app` 347 files, `@bb/cli` 49, `@bb/db` 29, `@bb/server-contract`
6 — all pass
- `@bb/server` 174/175 — `test/internal/internal-skill-trees.test.ts`
fails, verified failing identically on a clean `main` checkout by
stashing; pre-existing and unrelated
- New: publisher-label resolution (`marketplace-publishers.test.ts`),
migration 0098, the vanished-facet case in `PluginsOverview.test.tsx`
(confirmed non-vacuous — it fails without the fix)
- Manually verified in the dev app via headless browser: Browse
grouping, both install dialogs, and the scrolling source line

## Risks

- Requires the registry rename to be live, which it is. A BB built from
this branch talking to a manifest still named `bb-official` would refuse
the refresh on the name check, and vice versa.
- `bb marketplace list` widened its Name column from 22 to 24;
`bb-community (official)` is 23 characters and was being truncated.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Prepare bb-app 0.38.0 (#1631)

## Summary

- bump bb-app and the desktop app to 0.38.0
- add release notes for the Extensions Page, plugin marketplaces,
community plugins, and Linux Alpha
- add the August 15 website changelog metadata

## Validation

- `node .github/workflows/check-version-lockstep.mjs`
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs`
- `umask 022 && pnpm exec turbo run typecheck test --filter=@bb/config
--filter=@bb/server --filter=bb-app --filter=@bb/web`
- `umask 022 && pnpm exec turbo run smoke:tarball --filter=bb-app
--force`
- `git diff --check`

## Release note

`@get-bb/plugin-sdk@0.4.6` is not on npm yet. The release workflow will
publish it with bb-app 0.38.0.

> AGENT GENERATED: by Codex GPT-5

* Fix line-linked HTML preview default (#1635)

## Summary

- default line-linked HTML files to rendered Preview
- preserve Raw access and source targeting for ordinary code files
- add regression coverage for HTML previews carrying a line range

## Testing

- `pnpm exec turbo run test --filter=@bb/app --force -- --run
src/components/secondary-panel/FilePreview.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `git diff --check`

> AGENT GENERATED: by GPT-5.6 Codex

* Fix live HTML file preview updates (#1630)

## Summary

- reload the sandboxed HTML iframe when the fetched source revision
changes
- keep the iframe stable across unrelated renders and Preview/Raw
visibility toggles
- add regression coverage for both reload and stability behavior

## Root cause

Realtime invalidation refetched the HTML source, but the iframe raw URL
stayed stable, leaving the existing rendered document mounted.

## Tests

- `pnpm exec turbo run test --filter=@bb/app --
src/components/secondary-panel/FilePreview.test.tsx` (15 tests)
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run lint --filter=@bb/app` (0 errors; existing
warnings only)

> AGENT GENERATED: by GPT-5 Codex

* Publish the nightly channel after a stable release (#1651)

## Problem

A nightly version is `<next patch>-nightly.<run id>.<attempt>`, derived
from the version on `main`. When we release `0.38.0`, the newest nightly
is `0.37.1-nightly.*`, which is lower than `latest`. The nightly npm
dist-tag and the `bb Nightly` desktop app therefore stay behind the
stable release until the 3am cron.

## Change

A non-dry `npm_tag=latest` run now refreshes the nightly channel from
the same release commit.

- New `publish-nightly` job. It runs only for a non-dry `latest`
dispatch, after `publish` succeeds. It derives the next nightly version
from the release commit, publishes it under the `nightly` dist-tag, and
verifies the registry. It runs `smoke:tarball` first, because `bb-app`
has no `prepack` hook and its `files` list ships built output, so a
fresh runner must build before it packs. It skips typecheck and unit
tests, because `publish` ran them on this same commit and only the
version string differs.
- The nightly desktop build jobs take the version from either npm job:
`needs.publish-nightly.outputs.version ||
needs.publish.outputs.version`. Their `if` uses `!cancelled()`, because
`publish-nightly` is skipped on the scheduled and manual nightly paths
and a skipped need would otherwise skip them.
- Both npm publishes that move the `nightly` dist-tag share one
job-level concurrency group, `publish-nightly-npm`, so the cron and a
release run cannot move that tag at the same time. A stable release
publishes `latest`, so it takes a per-run group and never queues behind
the cron.
- `nightly-desktop-publish` gets its own concurrency group. The
workflow-level group splits `nightly` from `ref`, so a release run and
the cron could reach the moving `desktop-nightly` release at the same
time. That job deletes every asset before upload, so the race could
leave one platform's binaries missing.
- `docs/bb-release-process.md` and `apps/desktop/README.md` describe the
new step.

The nightly jobs run after the stable publish succeeds, so a nightly
failure cannot affect the release that already shipped.

## Tests

- Parsed the workflow YAML and confirmed the job graph, the `if`
expressions, and the `needs` lists.
- No unit tests: the change is GitHub Actions job wiring.
`prepare-nightly-version.mjs` and `bump-version.mjs` are unchanged.

## Risks

- A `latest` run grows from about 7 minutes to about 30 minutes, because
it now also builds the signed macOS and Linux nightly desktop apps.
Measured from recent runs: `Publish bb-app` 7m, macOS desktop 14m, Linux
desktop 4m, desktop publish under 1m. The stable npm publish still
completes about 7 minutes in, so users do not get the release later.
- The `npm-release` environment gate applies to `publish-nightly` too,
so an approval-gated setup asks a second time.
- The full path only runs on a real release dispatch, so the first live
exercise is the next stable release.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fit the user question form to the visible footer space on mobile (#1672)

## Summary
- The AskUserQuestion form capped its height with
`max-h-[calc(100dvh-6rem)]`. That value ignores the header, safe-area
insets, sibling footer content (goal card, child banners), and the
software keyboard. When the sticky footer grew taller than the timeline
scroll port, the browser clipped the top of the footer. On mobile the
tab strip and the top of the question moved off screen and no scroll
could reveal them.
- Add `useStickyFooterAvailableHeight`: it measures the bottom-anchored
scroll port, subtracts the height of the other footer content, and
returns the space the form may use. A `ResizeObserver` keeps it current
when the keyboard opens or the footer stack changes.
- The form applies that value as its `maxHeight` with a 192px floor.
Outside a scroll body (stories, tests) it keeps the old `dvh` fallback.
- Mark the sticky footer wrapper in `BottomAnchoredScrollBody` with
`data-scroll-footer` so the hook can find it.

## Testing
- `pnpm exec turbo run typecheck lint --filter=@bb/app`
- Vitest for `user-questions`, `bottom-anchored-scroll-body`,
`pending-interactions`, `promptbox/FollowUpPromptBox`, and
`views/thread-detail`
- Manual QA with dev-browser at 393px width with touch emulation on a
real Claude Code thread with 4 long questions: the footer height now
equals the scroll port height with extra footer content and at a 450px
tall viewport; touch swipes scroll the option list; tabs and Cancel/Next
stay on screen.
- Verified on a phone through bb connect.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Fix the render loop when a New plugin example seeds the root composer (#1694)

## Summary

- Fixes #1685: Installed plugins → New plugin arrow → an
Examples/Capabilities entry crashed the app with `Maximum update depth
exceeded`.
- Cause: `RootComposeView` applies a `replaceInitialPrompt` seed from
location state in an effect that depended on the `promptDraft` object.
The draft write re-renders the view synchronously with a new
`promptDraft` identity before the router's `startTransition` navigation
clears the state, so the effect re-applied the seed each render and
starved the transition until React aborted. The plain button used
`restoreIfEmpty` (a no-op after the first write), which is why only the
seeded entries looped.
- Fix: depend on the stable `setDraft` / `restoreIfEmpty` setters in
both location-state effects.
- Test: a `RootComposeView` router test with a replacing `initialPrompt`
state; it crashes the worker with the update-depth error before the fix
and passes after.

## Verification

- Reproduced in the dev app with a headless browser: Installed → New
plugin options → Kanban board crashed before the fix and loads the
composer without errors after it.
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run test --filter=@bb/app -- src/views
src/components/plugin/PluginNewThreadComposer.test.tsx
src/components/promptbox/NewThreadPromptBox.test.tsx` (37 files, 347
tests pass)

Note: the issue also mentions the `.nvmrc` / pnpm Node requirement
mismatch; this PR does not change that.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Accept cross-realm Response objects from plugin HTTP routes (#1693)

## Summary

Plugin HTTP route handlers can run in a different realm, so a valid
\`Response\` from a handler failed the \`instanceof Response\` check in
\`invokeHttpRoute\`. Every plugin HTTP route then returned a 500 with
\`http route handler must return a Response\`.

This change:

- Accepts a structurally valid Response (\`status\`, \`headers\`,
\`arrayBuffer\`, \`clone\`) from any realm.
- Re-wraps a foreign Response into a this-realm \`Response\` at the
invoke boundary, so Hono always consumes a native object.
- Keeps the pointed \`http route handler must return a Response\` error
for a malformed return.

## Tests

- New tests in \`plugin-wire.test.ts\`: a foreign structural Response is
adopted with status, headers, and body intact; a malformed return still
yields the pointed 500.
- The new test fails without the fix and passes with it.
- \`pnpm exec turbo run typecheck --filter=@bb/server\` passes.

Fixes #1661

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Automation execution: settled failures with backoff and auto-pause, single-flight runs, process-group termination, startup recovery (#1675)

Fixes #1674

Three commits, one per section of the issue:

1. Script timeout and output-limit termination now signals the script's
whole detached process group on POSIX (SIGTERM, then SIGKILL a second
later); Windows keeps direct-child termination.
2. A failed dispatch settles the run instead of restoring an overdue
`next_run_at`: recurring automations retry after 30s, then 60s, and the
third consecutive failure pauses the automation; one running execution
per automation is enforced with a partial unique index (scheduled claims
no-op while one is active, Run now returns the existing running record),
and the migration settles older duplicate running rows before creating
the index instead of failing to apply.
3. On startup, before the first sweep, running rows nothing owns any
more are settled: script runs and threadless agent runs as skipped
(interrupted); agent runs follow their thread.

Plugin code only (`plugins/automations`); no wire changes, so no
`HOST_DAEMON_PROTOCOL_VERSION` bump. Tests: single-flight at the DB
boundary, backoff and auto-pause, the manual-pause guard, counter reset
on success/skip/resume, descendant termination, the migration over
duplicate running rows, startup reconciliation (unit and plugin
harness).

> AGENT GENERATED: by Claude Fable 5

* Add host plugin foundation and migrate Keep Awake (#1686)

## Summary

- add a singular optional host entry to plugin manifests, built as a
self-contained artifact and started lazily in an isolated Node process
on the targeted host daemon
- add typed host RPC, cancellation and worker lifecycle, host-to-server
signals, plugin-scoped data/temp paths, and daemon-native filesystem
watching with coalescing and backpressure
- add daemon-owned idle eviction: calls and watches retain
automatically, while plugins can explicitly retain independent
background work with `experimental_retainWorker()`
- add build, development, test-harness, bundled-type, protocol, and
authoring-guide support for host entries
- move the macOS Keep Awake behavior and settings out of core into the
only builtin host plugin in this change; Keep Awake holds a worker lease
only while enabled
- split host-daemon contract subpaths so daemon-only command schemas do
not enter the app boot bundle

## Design boundaries

- the caller selects the host at RPC call time; feature-specific cwd or
environment identifiers remain ordinary typed payload fields
- the daemon owns generic execution, artifact supervision, native
watching, lifecycle, idle eviction, and transport primitives
- an unretained worker is gracefully stopped after five idle minutes and
starts again on the next call; there is no global worker-count limit,
manifest lifetime mode, or plugin-selected timeout
- this does not introduce provider plugins, provider sessions,
environment locking/coordination, a permission system, or additional
example plugins
- host plugin APIs remain experimental and are listed in
`docs/api_to_audit.md`
- bumps the host daemon protocol version to 127 and the internal
daemon/worker protocol to 2

## Validation

- complete PR CI is green: checks, app, packages, server, integration,
and Linux/macOS package smoke
- full `turbo run build typecheck lint` — 73/73 tasks passed
- exact CI packages test shard — 53/53 tasks passed
- app — 348 files and 2,768 tests passed
- host daemon — 46 files and 569 tests passed
- plugin SDK — 11 files and 93 tests passed
- Keep Awake — 3 files and 11 tests passed
- plugin build — 6 files and 18 tests passed, 1 skipped
- app boot payload — 1,637.3 KB raw / 432.8 KB brotli, with no budget
increase
- npm SDK version guard — `@get-bb/plugin-sdk@0.4.7` passes as
unpublished
- `git diff --check`

> AGENT GENERATED: by GPT-5

* Fix mobile file preview switch overflow (#1659)

## Summary

- size coarse-pointer Preview/Raw tabs so the complete bordered switch
fits the 36px file header
- share the corrected sizing across markdown, CSV, and HTML preview
toggles
- document the relationship between the inner tab height and the switch
border/padding

## Verification

- Dev Browser at a 390x844 viewport with coarse-pointer emulation:
switch reduced from 42px to 36px, with measured overflow reduced from
6px to 0
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run test --filter=@bb/app --force` (347 files, 2,765
tests)

> AGENT GENERATED: by GPT-5.6

* .github/workflows: Migrate workflows to Blacksmith runners (#1713)

This PR has been automatically generated by a team member in your GitHub
organization using Blacksmith's [Migration
Wizard](https://docs.blacksmith.sh/introduction/quickstart). This PR
changes the following:

1. Your selected workflows will now run on Blacksmith's 2x faster
hardware (e.g., `runs-on: blacksmith-4vcpu-ubuntu-2204`). Learn more
about the [different instances available to choose
from](https://docs.blacksmith.sh/blacksmith-runners/overview#runner-tags).
2. Your jobs running on Blacksmith will now have all official GitHub and
popular third-party cache actions automatically interact with our 4x
faster, colocated cache. Learn more about [Blacksmith's actions
cache](https://docs.blacksmith.sh/blacksmith-caching/dependencies-actions).
3. Your GitHub Actions will now actually be observable. Learn more about
Blacksmith's
[logging](https://docs.blacksmith.sh/blacksmith-observability/logs) and
other
[observability](https://docs.blacksmith.sh/blacksmith-observability/dashboard)
features.
4. Your Docker builds will now automatically share their Docker layer
cache, resulting in up to 40x faster builds. Learn more about
[Blacksmith's Docker layer
caching](https://docs.blacksmith.sh/blacksmith-caching/docker-builds).

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/get-bb/codesmith/bb/pr/1713"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is enabled.</sup>

<!-- codesmith:autofix:enabled -->
<!-- /codesmith:footer -->

Co-authored-by: blacksmith-sh[bot] <157653362+blacksmith-sh[bot]@users.noreply.github.com>

* Agent providers as a first-class plugin surface (provider bridge protocol) (#1640)

# Agent providers as a first-class plugin surface

Implements
[plans/agent-provider-plugin-surface.md](plans/agent-provider-plugin-surface.md)
**end to end — phases 1–6 plus graduation**. Providers declare their
metadata through the plugin API and run as plugin-registered **provider
bridges** speaking one bb-owned, provider-agnostic JSON-RPC protocol.
The canonical path is no longer gated: the experiment, the legacy
adapters, and the core provider catalog are all deleted, so the bridge
protocol is the only way a provider reaches bb.

132 commits, 622 files, **+41,035 / −25,028** (`git diff --stat
origin/main..HEAD`, rename-aware — most of the insertions are provider
sources *moving* into `plugins/provider-*`).

## What's here, by phase (commits are ordered to match)

1. **Protocol + kit** — `@bb/provider-bridge-protocol`: versioned
handshake with session-behavior capabilities, canonical
requests/notifications (including end-to-end `skills/configure`), error
codes, an event-grammar doc, and a transport-agnostic conformance kit
(12 rules, incl. full streaming-delta coverage and zero-work-prompt
settlement).
2. **Four conformant bridges** — acp, pi, claude-code rewritten onto the
canonical dialect (translation extracted byte-identically into shared
modules both paths import); codex is the one new bridge, owning
per-thread `codex app-server` children with the #1402 supervision rules.
One generic `BridgeProviderAdapter` replaces per-provider adapter glue.
Turn-start watchdog (`provider_turn_start_timeout`).
3. **Server registry + SDK** — `ProviderRegistryService`
(catalog-equality pinned), `bb.agents.experimental_registerProvider`,
and `app.slots.experimental_providerIcon` (plugins ship theme-aware
React brand marks; `logoUrl` files remain for static color logos). Both
have `docs/api_to_audit.md` entries.
4. **First-party plugins** —
`plugins/provider-{codex,claude-code,pi,acp}` take over their core-seed
entries in place (position preserved, restored on disable, takeover
merge preserves flagship behaviors) and register their brand marks as
app components. The app no longer depends on `@bb/agent-providers`.
5. **Third-party artifact delivery** — `bb.providerBridge` manifest key
builds a self-contained bridge bundle; server stores/serves it
content-addressed; daemon caches by verified hash; a verified
`bridgeLaunch` (carrying the declaration's validated execution
capabilities) is its own routing authority.
`HOST_DAEMON_PROTOCOL_VERSION` 123 → 124 (the PR's only bump; main took
123 for artifact engine ranges). `examples/plugins/echo-provider` proves
the path end to end.

## Graduation (waves 1–5)

Each wave ended with a full board (typecheck + all suites) before the
next started, and every deletion first scanned the dying adapter's tests
for uncovered shared-module invariants and **moved** them.

1. **The pinned gap, then acp + pi** — the codex zero-work-prompt
settlement fix landed first (dispatch-ownership seam, no fabricated
turns), unpinning the last non-green conformance rule; acp's and pi's
legacy adapters were deleted.
2. **claude-code, then codex** — the last two legacy adapters deleted.
**No legacy adapter remains anywhere.**
3. **F…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant