1. Overview
At main (06aeaa9949), core server and daemon code provisioned exactly three workspace kinds on user-enrolled hosts: managed worktrees, personal workspaces, and unmanaged checkouts. The branch makes provisioning extensible: the server owns policy and durable orchestration; plugins own environment or machine mechanics; the daemon retains only host-local inspection and process primitives.
What bb had
Core selected exactly three workspace kinds through workspaceProvisionType, used managed as an ownership flag, and dispatched daemon environment.provision/destroy. Hosts were user-enrolled; Host.type could only be persistent and classified nothing. Main had no hostless or ephemeral environments, no provider-created machines, and no Environments settings page.
What changed
Plugins register typed environment and machine providers. The server validates selections, persists launch checkpoints, retries and replays work, and emits progress. The app renders plugin-supplied picker rows and machine lifecycle controls.
Why
A managed Modal machine needs lifecycle semantics that do not fit a local-worktree switch. The new boundary makes provisioning composable without teaching core about each provider, while keeping product policy and recovery centralized.
Sources: exact compare, branch summary, migration 0113, plan CURRENT, and the F17/F19/F20 review outputs. Main-baseline claims were reverified at the rebased base with git show 06aeaa9949:packages/domain/src/environment.ts, git show 06aeaa9949:packages/domain/src/host.ts, git show 06aeaa9949:packages/db/src/schema.ts, git show 06aeaa9949:packages/host-daemon-contract/src/protocol.ts, and git show 06aeaa9949:packages/plugin-sdk/package.json; git ls-tree 06aeaa9949:packages/db/drizzle confirmed that main ends at migration 0112.
2. Reviewer’s map
Read the eight commits in order. API Core establishes the wire, persistence, recovery, and UI host surfaces; the next four commits add providers; the last three remove the SSH machine plugin, repair the path-less migration backfill, and delete an intermediate-state backfill that main never needed.
b664bc3d4acontracts, migration, launch coordinators, routes/CLI, plugin app/host APIs, daemon attach/process primitives, core deletions
e5e1abe545reuse/current checkout, branch selection hooks, remote clone on a provider-created machine
5ae0c32c93managed worktree create/rebuild/remove, branch picker and host-data ownership
2c5e81c6f8projectless per-thread folders and inheritance-compatible paths
a3203e43c8sandbox create, snapshot suspend/resume, provider availability, settings and app picker sugar
8b4d56e38adelete the implementation before landing; retain generic no-icon/no-suspend contract coverage
3cdbc227f5map every legacy workspace row by kind, even when a failed row has no path
7e1ed2fdb3remove the ephemeral-host and Modal-environment backfill for an intermediate branch state
Area footprint and suggested reading order
| # | Area | Added | Deleted | Net | Read for |
|---|---|---|---|---|---|
| 1 | packages/plugin-sdk | 1,648 | 18 | +1,630 | public contracts and experimental app/host surfaces |
| 2 | packages/db | 5,731 | 758 | +4,973 | 0113 schema and compatibility migration |
| 3 | apps/server | 13,416 | 6,194 | +7,222 | selection, coordinators, replay, cleanup, routes and policy |
| 4 | apps/host-daemon | 197 | 1,554 | −1,357 | protocol 187; provisioning deletion; attach-only host primitive |
| 5 | packages/host-workspace | 135 | 3,865 | −3,730 | deleted core provisioning; retained inspection/process tools |
| 6 | apps/app | 6,759 | 3,659 | +3,100 | provider-driven composer, Info, Machines and plugin category |
| 7 | docs | 313 | 55 | +258 | CLI/guide/audit discoverability |
Provider plugin size
| Plugin | Tree lines at 7e1ed2fdb3 source / test | Diff-added vs main | Role |
|---|---|---|---|
| Project checkout | 1,531 / 737 | 2,337 | checkout/reuse adapter and branch hooks |
| Git worktree | 1,726 / 806 | 1,935 | managed local workspace reference implementation |
| Personal workspace | 188 / 337 | 585 | projectless reference implementation |
| Modal sandbox | 1,485 / 883 | 2,555 | suspendable managed machine and private resource handle |
Tree lines count tracked TypeScript/TSX source and test files at the final commit; source excludes test files and Vitest configuration. Diff-added is the rename-aware exact-base diff. Git counts several worktree-plugin files moved from host-workspace as renames, so its diff-added number undercounts the code present at HEAD.
Core behavior removed or displaced from main
- Daemon provisioning RPCs
environment.provisionandenvironment.destroy; core becomesenvironment.attachplus inspection. - The three-kind
workspaceProvisionTypeselection and the persistedenvironments.workspace_provision_type/managedcolumns. - The
Host.typefield, whose only value waspersistent; it was not a machine classifier. - Core daemon/host-workspace provisioning, branch-switch, and worktree-include implementations move into provider plugins.
- Root-composer branch-selection core modules and tests.
- Core cleanup internals/tests superseded by provider teardown.
- Host-workspace provisioning/worktree-include tests superseded by plugins.
- Final diff deletes 11 paths outright; the conceptual removals above also include in-place rewrites and moved policy.
Baseline scope: hostless/ephemeral provider paths, an Environments settings page and fields, and an isolate/inherit sub-thread setting were added and removed in intermediate branch states. Main had none of them, so they are not deletions from main.
All 11 paths deleted in the exact final diff
apps/app/src/views/root-compose-branch-selection.navigation.test.tsxapps/app/src/views/root-compose-branch-selection.test.tsapps/app/src/views/root-compose-branch-selection.tsapps/app/src/views/root-compose-branch-ui.test.tsapps/app/src/views/root-compose-branch-ui.tsapps/server/src/services/environments/environment-cleanup-internal.tsapps/server/test/environments/environment-cleanup.test.tsapps/server/test/public/public-thread-environment-decoupling.test.tsapps/server/test/services/managed-environment-cleanup-recovery.test.tspackages/host-workspace/test/provisioning.test.tspackages/host-workspace/test/worktree-include.test.ts
Counts are recomputed from git diff --numstat 06aeaa9949..7e1ed2fdb3; commit stats from git log --stat. No generated or untracked plan file is counted.
3. API and data model
The contracts deliberately separate durable server orchestration from plugin mechanics. Inputs are JSON-schema validated at the boundary; plugin resource values are private, bounded handles used only for replay and teardown. Removed surfaces below are compared only with main at 06aeaa9949, not with intermediate branch states.
Environment provider contract
interface PluginEnvironmentProviderPolicy {
retireGraceMs: number | null
removeRetryMs: number
transientRetryMs: number
transientRetryLimit: number
pathKeys: "per-thread" | "per-attempt"
createTimeoutMs: number | null
}
type EnvironmentCreated = {
status: "created"
path: string
ownsPath: boolean
mergeBaseBranch?: string
resource?: JsonValue
}
interface PluginEnvironmentProviderDefinition<R, S> {
id: string
displayName: string
icon?: string
requires?: R
inputs?: S
policy?: Partial<PluginEnvironmentProviderPolicy>
availability?: (context) => Promise<ProviderAvailability>
validate?: (context) => Promise<ValidateDecision>
create(context: EnvironmentCreateContext<S>): Promise<EnvironmentCreated | ProviderFailed>
remove(context: EnvironmentRemoveContext): Promise<ProviderRemoved | ProviderFailed>
}Create context: project, host, project checkout, gitRemote, schema-inferred inputs, thread, suggested branch, attempt, pathKey, rebuild, previous environment/resource, progress reporter, signal. Remove accepts nullable environment/host/path plus pathKey, resource, attempt, reporter, signal. Six normalized policy defaults: 5-minute retirement grace, 60-second removal retry, 30-second transient retry, 3 transient retries, per-thread path keys, and no create timeout.
Machine provider contract
type MachinePolicy = {
idleSuspendMs: number | null
retire: { after: "last-thread"; graceMs: number } | { after: "never" }
removeRetryMs: number
}
type MachineCreated<R extends JsonValue> = {
status: "created"
hostId: string
resource: R
}
interface PluginMachineProviderDefinition<R, S> {
id: string
displayName: string
icon?: string
requires?: R
inputs?: S
availability?: (context) => Promise<ProviderAvailability>
validate?: (context) => Promise<ValidateDecision>
environmentRow?: { displayName: string; environmentProviderId: string }
policy: MachinePolicy
create(context: MachineCreateContext<S>): Promise<MachineCreated<R> | ProviderFailed>
suspend?: (context: SuspendContext) => Promise<{ resource: JsonValue }>
resume?: (context: LifecycleContext) => Promise<{ resource: JsonValue }>
remove(context: LifecycleContext): Promise<ProviderRemoved | ProviderFailed>
}Create context: nullable project/gitRemote as constrained by requires, schema-inferred inputs, stable key, attempt, reporter, signal. Suspend receives checkpoint(resource) so it can persist recovery state before destructive work; suspend/resume return the updated private resource. Remove receives host ID/resource/reporter/signal. A machine provider’s create must enrol a real daemon through the join code and return that host’s ID. Core does not fabricate host rows.
Request and selection shapes
type EnvironmentMachineSelection =
| { type: "existing"; hostId: string }
| { type: "new"; machineProviderId: string; inputs: JsonValue | null }
type ProviderEnvironmentArgs = {
type: "provider"
environmentProviderId: string
machine: EnvironmentMachineSelection
inputs: JsonValue | null
}
// Server-boundary compatibility sugars retained for one cycle:
// { type: "managed-worktree" | "personal" | "unmanaged" | "host", ... }
HTTP routes
System environment/machine provider catalogs; environment list/show/delete; host list/show/create/remove; POST /hosts/:id/suspend, /resume, /retry-cleanup; thread create accepts provider selections. Environment status/PR routes now reject destroyed or pathless rows consistently.
CLI
bb environment providers|list|show|delete; bb thread spawn --environment-provider … [--environment-inputs] [--machine|--new-machine … --machine-inputs]; bb thread list --environment; bb machine list|show|remove|providers|suspend|resume|retry-cleanup.
SDK and app
experimental_environments.register/recheck, experimental_machines.register; environment/host SDK methods; environment-filtered thread listing; app slots for input rows; experimental BranchPicker/useBranches/useCheckoutState; sidebar providerId; environmentIntent; unarchived/cancelled events; host process/sanitize primitives.
Removed and compatibility fields
| Old surface | Branch behavior | Compatibility |
|---|---|---|
environments.managed and environments.workspace_provision_type | Provider identity and provider_owns_path replace the persisted three-kind and ownership fields. | The deprecated managed, workspaceProvisionType, and sidebar display-kind response fields are still present in every response. They are derived from provider-backed state and will be removed in a later release; isWorktree remains a workspace fact. |
EnvironmentStatus.retiring | destroying | Lifecycle phase is separate from availability status; migration maps retiring→ready, destroying→error. | Removed from the typed status union. |
Host.type | Machine provider identity and lifecycle fields replace it. | Removed; at main its schema had only persistent and classified nothing. |
Migration 0113
| Table | Added | Dropped / migrated |
|---|---|---|
environment_launches (new) | thread PK; provider/attempt/phase/timestamps/failure/transient count; path key; host/path/ownership/merge base/resource; step and pending log; replaced env/env ID; selection/request; cancel flag; phase index. | Durable launch journal replaces in-memory/core provisioning recovery. |
machine_launches (new) | stable key PK; provider/project/inputs/attempt/phase/times/failure/retries; host/resource; step/log/cancel flag; phase index. | Durable machine-create replay. |
environments | provider ID, owns path, selection, instance key, retire time, teardown attempt/status/message, private resource, index. | managed, destroy attempt, retire request, workspace provision type; main’s managed-worktree, personal, and unmanaged-checkout rows are backfilled to providers. |
hosts | machine provider ID, resource, selection, phase, suspended/retire times, teardown fields. | type; every main host was persistent. F20 removed the Modal/ephemeral backfill for an intermediate branch state, so 0113 now migrates only states main persisted. |
host_daemon_sessions | — | The redundant single-value host_type storage column is dropped with Host.type. |
Primary sources: environment-provider.ts, machine-provider.ts, 0113 migration, API audit docket.
4. Before / after screenshots
The matrix is driven by ?device=desktop|mobile&state=compact|expanded&surface=…. “Before” represents the rebased main baseline at 06aeaa9949; “After” represents the final 7e1ed2fdb3 UI. The screenshot set was captured during the pre-rebase b194187a7c review run; the rebase changed the pins and baseline, not the pictured provider surfaces. Click any image for its original capture. Provider-only surfaces use an explicit no-counterpart card.
5. User-facing changes
Each row ties the visible behavior to the plan decision or final review round that owns it. “Parity” means main’s user intent is preserved even when implementation moved into a plugin.
| Surface | Branch behavior | Decision / parity |
|---|---|---|
| New-thread environment picker | Rows are provider registrations. Modal is the only machine-provider sugar row; it selects a new Modal machine plus Project checkout. Project checkout and Worktree remain direct rows. | D63, D65, D68, S10, F19 Local checkout/worktree intent remains. |
| Projectless compose | Personal workspace is implicit and does not add a redundant environment chip. | D71 Matches main’s projectless default. |
| Checkout chip | Closed/open/new-branch/search/detached states are supplied through experimental branch hooks and a shared picker. | D69, F5–F7 Restores main’s Enter-to-select, labels and blockers. |
| Composer chips | Project, environment, branch and permission remain separate, with provider display names replacing core kinds. | D63, D69 Same information density as main. |
| Reuse row | The reuse menu lists the same eligible existing environments as main, and picking one behaves as on main. | D67 Main reuse semantics preserved. |
| Provisioning failures | For a thread whose provisioning failed before an environment existed, the environment row under the prompt box is absent, as on main. Sending during provisioning is unchanged. The Info panel reports “Not created · provisioning failed.” | D64, S7e, F17 The after capture uses one fresh failed attempt. |
| Archived threads | Follow-up creation row is replaced with archived status; unarchive emits a lifecycle event. | D24 Main archive behavior retained. |
| Info tab | Shows provider display, lifecycle, directory/branch/git status; destroyed workspaces remain inspectable as Destroyed/Unknown without dereferencing a path. | D63, F7 |
| Sidebar grouping | Project worktree glyph/grouping derives from provider identity/traits; projectless children still group by the parent rule. | D0, D1, D40 |
| Settings nav | There is still no Environments settings page; provider credentials live under plugin settings. Machines stays first-class, and General now labels the shared field “New branch prefix.” | D70, D71, F17 The branch-added page was cut before the final diff. |
| Sub-threads | A project child gets a fresh Git worktree; a personal child shares the parent’s workspace. | D70, D71 Matches main; the branch-added isolate/inherit setting was cut. |
| Machines page | Manual and Modal machines share the page. Online/offline stays as on main; there is no Active badge. A provider logo and badge appear only when the provider declares an icon. Suspended/Retiring/Cleanup failed lifecycle badges and their controls are core-rendered. | D84, S10, F17 |
| Add machine | Manual pairing remains. Modal is the only plugin-provided machine creation row. | Q6, S10, F19 |
| Plugins directory | The Environments category contains exactly Modal sandbox, Personal workspace, Project checkout, and Worktree. | Q12, F19 |
6. Bugs found and fixed during review
This is the reviewer-facing chronology from the standing reviewer and plan rounds. Every failure below describes an intermediate branch state that was fixed or removed before 7e1ed2fdb3; none is presented as behavior from main. Each line states the failure mode, not merely the patch. Final standing-review result: CLEAN.
S7e lifecycle hardening
- Cancellation reused attempt IDs, so stale completion could be adopted by a retry.
- Cancelled Modal creation could leak a sandbox.
- Interrupted host jobs could wait forever after their connection disappeared.
- Cleanup sweeps could call a provider remove concurrently.
- A Modal rebuild after teardown could lose the prior private handle.
- Schema defaults could make optional provider inputs appear required.
S8 standing review
- API deletion cleared the path before provider cleanup could use it.
- Cleanup for a cancelled attempt could delete the retry’s workspace.
- Retirement could remove an environment while its thread was still stopping.
- Project deletion could stick when its source already returned 404.
- One rejected cancellation promise could abort the whole sweep.
- Environment removals needed one provider-owned, replayable contract.
S9 / F1–F3
- The cancellation wrapper could settle before the real create stopped.
- Teardown released its path reservation before removal actually finished.
- Transient retry lost per-attempt cleanup identity.
- Validated inputs and configured defaults were each transformed twice.
- The host-RPC cancellation adapter could settle too early.
- An intermediate Add Project implementation selected branch-created ephemeral-only inventory.
F4
- Modal replay could duplicate both host and source.
- Worktree rebuild could select the wrong branch.
- Interrupted setup could be mistaken for completed setup.
requiresfacts were skipped when no validate hook existed.- Modal appeared twice in the picker.
F5–F7 UI parity
- F5 rejected a valid detached HEAD/tag checkout.
- F6 used the wrong branch in the default label.
- F6 lost Enter-to-select in checkout search.
- F6 left dead checkout UI in core.
- F7 cut the branch-added Environments settings and isolate/inherit controls, then restored error/reuse/checkout parity with main.
F8 machine coordinator
- An intermediate schema parser rejected a protocol-186 daemon before mismatch handling could initiate its update.
- Modal checkpoint recovery could crash.
- A new-work race could suspend its machine.
- Failed recovery could clear cancellation intent.
- A destroyed host launch could wait forever.
- Teardown could not resume a suspended machine.
- One machine failure could block the sweep.
F9 lifecycle replay
- A lifecycle RPC could deadlock while suspending its own machine.
- Suspend could overwrite a concurrent removal.
- Checkpoint failure could make a machine permanently unavailable.
- Cleanup unnecessarily resumed machines whose environment needed no remote remove.
- An old Modal snapshot could be discarded on replay.
F10 / S11
- Expanded machine policy, status UI, CLI/routes, progress and restart coverage landed.
- No new standing-review defect remained after the lifecycle matrix.
F11–F16 removed-provider review
- The proposed second machine provider exposed checkpoint, cleanup ownership, readiness, restore-concurrency, cancellation, and removal races.
- Each defect was fixed and regression-tested during those rounds.
- The implementation was later removed before landing because it was too new and generated most of the review churn.
F17 report review
- Machine-provider icons became optional, and the Machines page now shows a provider logo/badge only when declared.
- The Modal logo was replaced with a padded square mark.
- The Active badge was removed; online/offline remains the connectivity signal.
- General settings now says “New branch prefix.”
- Per-attempt provisioning-failure rendering was confirmed to match main.
F18 landing decision
- A final removal review found another host-identity ownership gap in the proposed provider.
- The implementation was removed before landing instead of extending the branch again: it was too new and accounted for most review churn.
F19 finalization
- Catalog, guide, API-map, build and test references were reduced to four environment plugins; Modal is the only machine provider.
- Generic fixtures retained no-icon and no-suspend contract coverage.
- Migration 0113 now backfills provider IDs for legacy rows even when their path is null.
- The fresh real-database-copy dry run passed every invariant.
F20 migration scope
- The reviewer found that 0113 still carried an ephemeral-host and Modal-environment backfill for an intermediate branch state that main never persisted.
- The final commit removed that block and its fixture, limiting the migration to main’s real persistence shape.
- The real-database-copy migration dry run was rerun at final HEAD and passed 23/23 checks.
Live QA matrices
Backend / repository
- Typecheck + lint: 94/94 tasks.
- App: 483 files; 3,937 passed + 3 skipped.
- Server: 228 files; 2,295 passed + 1 skipped.
- CLI: 54 files / 550 tests.
- Integration: 26 files / 54 tests.
git diff --check: no output.
Contracts and packages
- Plugin API map: 10 files / 74 tests.
- Plugin Build: 10 files; 138 passed + 1 skipped.
- Plugin SDK: 22 files / 245 tests.
- Database: 31 files / 444 tests.
- Modal plugin: 3 files / 33 tests.
- Branch tooling packaged all four provider plugins.
Live final-state QA
- Modal created a real daemon-backed machine, reached idle, showed its fixed mark without an Active badge, suspended, and was removed.
- Machines showed the pre-existing hand-enrolled machine beside Modal; Add machine offered Modal as its only plugin provider.
- The project picker showed Modal, Project checkout, and Worktree; projectless compose stayed implicit.
- A fresh one-attempt failure reproduced the absent environment row and “Not created” Info state.
Migration 0113 dry run on a real database copy
The harness copied the real database, containing 507 environments and 1,626 threads. Its first pass found one path-less unmanaged row without a provider ID. F19 fixed that backfill. F20 then removed the ephemeral-host and Modal-environment backfill for an intermediate branch state that main never persisted. A fresh-copy rerun at 7e1ed2fdb3 passed 23/23 migration checks: unmanaged rows mapped 8/8, managed-worktree rows 488/488, personal rows 11/11, missing provider IDs 0, failed checks 0, anomalies 0, and SQLite integrity was ok. The temporary database, WAL, and SHM copies were deleted.
Evidence: standing reviewer output; visual-review rounds through F20; final verification; F17, F19, F20, and migration-dry-run thread outputs; plan Rounds log.
7. Breaking changes and known issues
These are review constraints, intentional compatibility limits, and known parity behaviors—not untriaged findings.
Compatibility and breaking surfaces
- The deprecated
managed,workspaceProvisionType, and sidebar display-kind fields remain in every response for now. They are derived from the new provider-backed state and will be removed in a later release. EnvironmentStatus.retiring/destroyingis removed; use lifecycle phase.- Main’s
Host.type, whose only permitted value waspersistent, is removed; provider identity and lifecycle now carry real distinctions. - The branch’s daemon wire changes raise protocol 183→187. That mismatch makes protocol-186-and-earlier daemons self-update.
bb plugin buildwith an installed bb older than this branch cannot bundle the checkout/worktree app bundles because its export list predates the two new hooks. A bb built from this branch can.- Experimental public plugin members stay prefixed and are listed in
docs/api_to_audit.md.
Lifecycle policy
- Provider removal retries remain uncapped, matching main’s cleanup behavior.
- There is deliberately no universal
createTimeoutMs; the default isnullbecause a provider may own a longer operation. - Non-secret selections are persisted; credentials stay in plugin settings. Private provider resources are capped at 16 KiB.
- A machine provider’s create must enrol a real daemon through the join code and return that host’s ID. Core does not fabricate host rows.
- Sub-thread defaults match main: project children get fresh worktrees, while personal children share the parent’s workspace.
Network and UX constraints
- A Modal target needs a server URL it can reach: bb Cloud, Remote access, or a temporary tunnel.
- Modal appears as setup-required when credentials are absent; the token is never part of selection JSON or screenshots.
- Main and the final branch have no Environments settings page; provider-specific configuration lives with each plugin.
- Destroyed environments keep their durable row for audit/Info but no longer expose a usable path.
Migration and rollout
- Migration 0113 is one-way and maps the three environment kinds and persistent hosts that main stored onto provider-backed state.
- Path-less failed environments still receive a provider ID; inputs omit the missing path.
- F20 removed the ephemeral-host and Modal-environment compatibility block because it targeted an intermediate branch state, not main.
- The final real-database-copy rerun left source row counts unchanged and passed 23/23 migration checks plus SQLite integrity.
- Modal remains opt-in through plugin configuration.
/tmp with its resolved /private/tmp path. The final Turbo run passed with TMPDIR=/private/tmp. Load-only integration timeouts also passed when rerun in isolation.8. Deferred follow-ups
Plan sections C/D were intentionally kept out of this eight-commit branch.
C — next contract/product work
- Define a stable provider-traits vocabulary instead of continuing to infer behavior from provider IDs.
- Add a reusable host-owned environment picker.
- Add
environment.changedand finish the inputs documentation surface. - Move
update_environment_directoryownership to the checkout plugin. - Deprecate the legacy
type:"host"thread-create sugar. - Represent manual enrollment as a machine provider.
- Add a generic composer “New provider machine” row beyond provider-specific sugar.
D — later exploration
- Express managed Modal-machine traits in the provider vocabulary without reviving main’s single-value
Host.type. - Add remote-branch sources to the shared branch picker.
- Expose supported Info items/slots below the composer.
- Make per-harness plugin installation cheaper in the integration suite.
Source: untracked plans/environment-providers.md, sections C/D and Decisions log. The plan was read only and remains untracked.