1. Overview
Baseline is main at 06aeaa9949: three core-owned kinds, no environment-provider contract. This is the environment-provider half of the earlier PR #3194.
Before · core switches
Project checkout, managed Git worktree, and personal directory were selected and provisioned through core server/daemon branches. Adding another workspace implementation meant extending those switches and cleanup paths.
After · one environment contract
Project checkout, Worktree, and Personal workspace are bundled, auto-installed plugins. Third parties can use the same experimental registration, availability, validation, input, create, and remove surfaces.
Boundary · machines come later
Every environment runs on an existing enrolled machine. No machine-provider API, machine launch table, hostless execution, new-machine controls, Modal, or SSH plugin ships in this PR. Main’s Host.type and host-session fields remain.
2. Reviewer’s map
Exact diff: git diff --shortstat origin/main..origin/bb/env-providers-1-environments in the PR worktree. Eleven commits on the requested main baseline; counts include source, tests, documentation, generated migration snapshot, and lockfile.
387c4510b5Contract, durable orchestration, migration, daemon boundary, SDK/CLI, UI and discoverability.
b07a8a5bf7Checkout validation, shared branch controls, and user-owned path retention.
e38c460759Isolated worktrees, provider preparation, recovery, and branch inputs; lifecycle scripts now run in core.
3ecc3228fcOne owned directory per projectless root and parent-environment reuse for children.
Reading order
packages/plugin-sdk/src/environment-provider.ts→ public operations and cancellation/idempotency contract.packages/domain/src/environment.tsandpackages/db/drizzle/0113_environment_providers.sql→ published model, intent conversion, backfill and status changes.apps/server/src/services/environments/provider-orchestration.ts→ launch/retry/cancel/remove ownership; thenapps/server/src/services/threads/thread-environment-placement.tsand thread-environment-providers/default-policy.plugins/environment-project-checkout/src,plugins/environment-git-worktree/src,plugins/environment-personal-workspace/src→ concrete resource operations;packages/environment-provider-host/src→ host primitives.apps/app/src/components/pickers/EnvironmentPicker.tsx→ eligibility and inputs; then composer, follow-up, Info and sidebar. Finish with SDK/CLI contracts and their regression tests.
Per-area lines of code
The regenerated 0113_snapshot.json alone adds 4,159 lines. Cross-package renames are charged to the destination area with Git’s rename-aware added/deleted counts; all rows sum to +33,293 / −16,614.
| Area (includes tests/docs) | Files | Added | Deleted | Net |
|---|---|---|---|---|
packages/db | 24 | +5,438 | −659 | +4,779 |
apps/server | 144 | +10,544 | −5,939 | +4,605 |
plugins/environment-project-checkout | 14 | +2,343 | −0 | +2,343 |
apps/app | 105 | +5,518 | −3,682 | +1,836 |
packages/environment-provider-host | 8 | +1,221 | −0 | +1,221 |
packages/plugin-sdk | 21 | +1,133 | −18 | +1,115 |
plugins/environment-git-worktree | 17 | +1,574 | −646 | +928 |
apps/cli | 11 | +799 | −75 | +724 |
plugins/environment-personal-workspace | 11 | +585 | −0 | +585 |
tests | 17 | +851 | −514 | +337 |
packages/server-contract | 7 | +232 | −13 | +219 |
docs | 6 | +276 | −62 | +214 |
packages/core-ui | 3 | +317 | −174 | +143 |
packages/sdk | 5 | +144 | −4 | +140 |
packages/client-core | 4 | +153 | −28 | +125 |
Root configuration | 4 | +164 | −65 | +99 |
packages/plugin-api-map | 2 | +100 | −4 | +96 |
packages/templates | 4 | +79 | −14 | +65 |
packages/thread-view | 3 | +70 | −16 | +54 |
packages/scripts | 4 | +70 | −17 | +53 |
packages/plugin-build | 3 | +44 | −3 | +41 |
plugins/bb-official.json | 1 | +12 | −0 | +12 |
apps/demo-server | 1 | +14 | −3 | +11 |
packages/test-helpers | 1 | +14 | −4 | +10 |
plugins/tasks | 1 | +6 | −0 | +6 |
apps/mobile | 2 | +5 | −0 | +5 |
packages/shared-ui | 1 | +4 | −0 | +4 |
scripts | 1 | +6 | −2 | +4 |
packages/bb-app | 1 | +1 | −1 | +0 |
packages/plugin-registry | 1 | +1 | −1 | +0 |
plugins/concurrency-limit | 1 | +1 | −1 | +0 |
plugins/keep-awake | 1 | +1 | −1 | +0 |
plugins/docs | 1 | +0 | −1 | −1 |
packages/host-watcher | 1 | +11 | −20 | −9 |
packages/host-daemon-contract | 5 | +164 | −205 | −41 |
packages/domain | 11 | +97 | −245 | −148 |
apps/host-daemon | 29 | +1,166 | −1,509 | −343 |
packages/host-workspace | 10 | +135 | −2,688 | −2,553 |
Deleted core paths and responsibilities
| Deleted path (link to main) | Replacement |
|---|---|
apps/server/src/services/environments/environment-cleanup-internal.ts | Legacy core cleanup dispatcher deleted; provider orchestration owns teardown. |
apps/app/src/views/root-compose-branch-selection.ts | Core branch routing helper deleted. |
apps/app/src/views/root-compose-branch-ui.ts | Core branch UI policy deleted; checkout plugin owns its inputs. |
The daemon’s environment.provision/destroy operations and host-workspace provisioning responsibilities are removed or reduced inside surviving files. environment.attach inspects an already resolved path. The old cleanup/recovery, decoupling, branch-routing, and host-workspace provisioning tests are replaced by provider-oriented coverage; this is not deletion without replacement.
3. API and data model
Environment contract only. Registration is experimental; the type-only environment-provider entry point exports unprefixed types.
Operations and selection
bb.experimental_environments.register({
id: "git-worktree",
displayName: "Worktree",
requires: { gitCheckout: true },
inputs: inputSchema,
availability,
validate,
policy: { pathKeys: "per-thread", retireGraceMs: 300000 },
create,
remove
});
create({ inputs, pathKey, attempt, previous, report, signal, ...facts })
→ { status: "created", path, ownsPath, mergeBaseBranch?, resource? }
| { status: "failed", failure: "transient" | "terminal", message }
remove({ environment, hostId, path, pathKey, resource, report, signal, attempt })
→ { status: "removed" } | { status: "failed", message }
{ type: "provider", environmentProviderId: "git-worktree",
machine: { type: "existing", hostId },
inputs: { branch: { kind: "default" } } }This is a contract sketch: named callbacks and inputSchema stand for plugin implementations. Create is long-running, abortable and idempotent by pathKey; after restart core retries unfinished creation with the same key. Once successful creation is persisted, recovery uses that result and reconciles the original hook operation without repeating create or setup. Cancellation aborts creation and removes by key. Progress uses report.step/log.
Core versus provider
- Core: eligibility, boundary parsing, availability caching/recheck, placement, path claims, durable attempts, adoption, retry, cancellation, retirement, teardown and project deletion.
- Provider: make/remove the resource, branch policy, input schema/controls, validation/refusal messages and progress.
- No SDK controller, polling/job protocol, provider
cancelhook, or returned host ID. The enrolled host is known before create. - Defaults are filled at registration: removal retry 60 s, transient retry 30 s / three retries, per-thread path keys, no universal create timeout. Worktree/personal retirement follows main’s five-minute grace; checkout keeps its path.
- The nested
machine: { type: "existing", hostId }shape is deliberately forward-compatible with PR 2. Onlyexistingis accepted here;new, empty/missing hosts and flat host IDs are rejected in provider selections.
Hooks in core
For any provider that successfully returns ownsPath: true, core runs the repo’s .bb-env-setup.sh in the created workspace before the thread starts, and .bb-env-teardown.sh before provider removal, including cleanup after setup fails. Ownership defaults to false; a claimed path or failed create alone never authorizes hooks. Attached paths skip both scripts, and missing scripts are skipped. Provider preparation finishes first, including Worktree’s .worktreeinclude copies.
The server orchestrates both hooks through the host daemon on the environment’s machine, with separate 15-minute timeouts. Steps and output appear in the existing provisioning transcript. Setup failure fails the launch; teardown script failure is reported but does not by itself prevent removal. An uncertain running hook does prevent cleanup: the workspace and claim remain reserved until the daemon confirms process-group termination, or confirms the operation never started.
Persisted hook identity lets restart recovery reconcile the original daemon operation instead of running setup twice. Durable daemon operation records and cancellation tombstones handle dropped requests, daemon restarts and delayed dispatch: a cancelled request cannot start later after cleanup. Worktree no longer invokes either hook itself, and the unused SDK exports experimental_killProcessGroup and experimental_supportsProcessGroups are removed.
Published API and composer surfaces
bb.experimental_environments.register/recheck;sdk.environments.listProviders/list/get/delete;threads.list({ environmentId }).bb environment providers|list|show|delete;bb thread spawn --environment-provider --environment-inputs;bb thread list --environment. Inputs without a provider are rejected; unknown provider IDs fail at creation with available choices.app.slots.experimental_environmentProviderInputsreturns ready value or blocked reason. Sharedexperimental_useBranches,experimental_useCheckoutState, and seven-propexperimental_BranchPickersupport plugin-owned branch controls.- Dispatch hooks expose
environmentIntentas environment/provider; events includethread.unarchivedandmessage.cancelled. Legacy path and create-request shorthand is normalized at the server boundary.
Migration 0113 and model
| Surface | Change |
|---|---|
| environments | Provider ID, ownership, selection, instance/path key, resource, retirement and teardown state replace managed/provision-type/cleanup columns. All three legacy kinds are backfilled, including NULL paths. The nullable server-local canonical_path key persists the launch claim on attachment and is used for every attached-path comparison; provider display paths remain intact. Legacy keys resolve independently on the host, with failed rows retried later. |
| environment_launches | The single unshipped migration 0113 creates claim_path directly in this table (no separate 0114 or launch backfill). Ownership defaults to false until successful creation records an owned path. Core persists the canonical host/path claim, attempt, phase, request/selection, path key, progress and log; restart recovery does not rely on an in-memory provider controller. |
| environment_hook_operations | Migration 0113 also creates the hook table: hook kind, original daemon operation ID, timestamps and error, keyed by launch attempt or retirement instance. The hook table and false ownership default are folded into 0113; no PR-1 migration 0114 remains. |
| Environment and thread list | Provider identity/selection/lifecycle are public; legacy managed, provision type and display-kind fields are derived compatibility projections. isWorktree comes from Git inspection, including sidebar grouping. |
| Pending intents and statuses | Pending requests become provider intents with nested existing-machine selections. Legacy retiring/destroying values are remapped; lifecycle carries teardown progress. |
| Host boundary | Protocol 183 → 191; environment.attach and environment.attach.cancel replace daemon provisioning/destruction. Hook rounds advance 188 → 191 for run/progress/cancel, persisted reconciliation and confirmed termination or never-started cancellation outcomes. Hosts and host_daemon_sessions schema remain exactly main’s shape. |
The latest migration dry run migrated a backup obtained through a read-only transaction; the source database was never opened for writing. 30/30 invariants passed with zero anomalies, including data preservation, provider and intent conversion, unchanged host schemas, false ownership default, the empty hook table, migration 0113 only, foreign keys, domain parsing and integrity. The temporary copy was deleted.
Provider inputs are persisted and readable by plugins, including after destruction; use non-secret inputs. Credentials belong in plugin settings. Private resource state stays outside public responses.
Primary code: packages/plugin-sdk/src/environment-provider.ts · packages/sdk/src/areas/environments.ts · packages/db/drizzle/0113_environment_providers.sql
4. Before / after screenshots
Supplied captures reused, not recaptured. PR 1 screenshots were captured at 3ecc3228fc, before the seven review rounds; screenshot labels retain that SHA. Rounds 5–7 change hook execution and recovery; the pictured UI is unchanged. Desktop main is 1280×800; PR 1 is mostly 1280×1000, with earlier composers at 1280×800. Compact Chromium is 390×844; iOS Safari has separate captures. This compares behavior/layout, not pixels. Compact/expanded changes report card size and notes, not application state.
Black rectangles remove identifying text; machine labels are replaced with HOST. Public fixture titles and functional labels remain. Click an image for its full-resolution PNG.
5. User-facing changes and decisions
Later decisions override intermediate #3194 proposals. The report uses the PR 1 body and captured surfaces to scope the plan’s CURRENT and Decisions log.
| Surface | Final behavior | Decision / history |
|---|---|---|
| Provider picker | Project checkout, Worktree, then additional providers by display name. Personal workspace is projectless-only and implicit when it is the sole local choice. | 39, 63, 68; PR 1 fixed order |
| Unavailable vs ineligible | Requirements filter incompatible choices; scoped availability/input blockers explain offered but unusable choices. Scoped listings now omit structurally ineligible providers rather than showing greyed rows. The older non-Git screenshot predates this fix. | 39, 63; PR 1 review round 1 |
| Checkout inputs | Current/New branch/Checkout and base-branch selection live in the checkout plugin. Dirty/busy mutations are refused; detached HEAD remains valid. | 55, 59–61, 69; F5/F6 |
| Follow-up and Info | Keep project/branch identity and the same-environment action. Projectless summary shows HOST even with one machine; project rows show the machine when several exist. Info names the provider. | 40; Q6; follow-up row repair |
| Failure / archive / destruction | Pre-row create failure: no misleading composer environment row; Info says Not created · provisioning failed; details stay in transcript. Archived checkout suppresses live Git status. Destroyed state hides the dead directory. | 64; F7; archived Git-status repair |
| Reuse and sidebar | Reuse shows identity and associated threads, without provider text. Sidebar groups by actual isWorktree; checkout threads stay direct project children. | 67 supersedes Q7; main parity |
| Settings and catalog | New branch prefix stays under General. No Environments page, order setting, defaults setting or sub-thread policy. Environments catalog contains the three bundled plugins. | 68, 70/71; Q12; F17 label |
| Machines | Enrolled-machine list and manual Add a machine remain unchanged; no machine-provider rows, lifecycle controls or badges. | PR 1 extraction boundary |
6. Bugs found and fixed during review
Applicable history from #3194, not newly observed defects in main. These are intermediate failures fixed or superseded by the extracted implementation. Machine-only Modal/SSH/suspend/checkpoint rounds are excluded. The fresh PR 1 review finished CLEAN at b738452cc6, with no remaining actionable findings. The final reviewer passed 40 server, 12 daemon, 52 migration and 3 recovery tests, including independent tombstone/recovery probes; one transient EPERM passed on retry.
Lifecycle hardening · S7/S8
- Reused cancellation attempt IDs allowed stale completion to be adopted by a retry; core preserves attempt identity.
- API deletion cleared paths before cleanup; teardown now retains the data and claim it needs until removal settles.
- Cleanup sweeps could remove concurrently, and one rejected cancellation could stop the sweep; teardown is serialized and failures isolated.
- Retirement could delete a workspace while an archived thread was still stopping; cleanup waits for the appropriate lifecycle state.
Restart and replay · S9/F1–F4
- Interrupted jobs could hang or be mistaken for completed setup. Idempotent create/remove replaces the job/poll controller and replays using stable keys.
- Cancellation and host-RPC wrappers could settle before the underlying work stopped; teardown retains ownership through settlement.
- Retry cleanup could delete a newer workspace, and worktree rebuild could choose a stale branch; per-attempt cleanup identity and updated selections prevent that.
- Inputs/defaults could be transformed twice, and requirements could be skipped without validate; parse once and enforce eligibility independently.
Checkout and retry · S7/F5/F6
- Idle threads on another project sharing a checkout were omitted from busy-branch protection; idle joins starting/active.
- A failure before any environment row prevented the next send/retry; provider intent now permits recovery.
- Path shorthand aimed at an owned worktree failed late with SDK prose; creation refuses with a reuse-oriented message.
- Valid detached HEAD was rejected, default base labels were wrong, Enter-to-select was lost, and dead checkout UI remained in core; parity fixes and plugin-owned controls address these.
UI parity and migration · F2/F7/F17/F19
- The ordinary single-machine follow-up row disappeared; it now retains identity and reuse action after provisioning.
- Archived checkout exposed live Git state; the archive guard is restored. Earlier error-row/lifecycle/reuse-provider text was deliberately cut to match the final decisions.
- General’s label becomes New branch prefix; provider ordering/default settings were removed.
- NULL-path legacy environments missed provider backfill. Migration 0113 maps every legacy kind, with a fresh-copy dry run and dedicated NULL-path/pending-intent fixtures.
Bugs found and fixed · seven PR-1 review rounds
- Round 1 · Concurrent checkout launches could mutate the same branch before attachment; durable host/path claims now precede mutation, directory switching reuses same-project environments, independent cleanup sweeps avoid stalled resources blocking others, scoped requires filtering omits ineligible rows, and unshipped machine-provider documentation claims are removed.
- Round 2 · Reuse, directory switching or restored dispatch could bypass claims, aliases could evade exclusion, and failed-launch cleanup could delete a newer workspace; canonical claims now guard every admission and remain held through failed-launch cleanup.
- Round 3 · Attachment could lose canonical identity and allow alias-based branch changes beneath a live thread; the environment row now persists the canonical key for every attached-path comparison, and claim_path is folded into the single migration 0113.
- Round 4 · One inaccessible or symlink-loop legacy path could block healthy listings and launches; backfill now isolates each row, retries unresolved keys and conservatively refuses unresolved equal/prefix conflicts.
- Round 5 · Hooks tied to Worktree could be skipped by other owned-path providers; core now runs setup after successful owned creation and teardown before removal.
- Round 6 · Failed creation could run teardown in the original checkout, a lost setup RPC could let cleanup remove a workspace under a running script, and restart recovery could run setup twice; false ownership defaults, persisted hook identity and daemon-confirmed termination close those gaps.
- Round 7 · Unknown operation IDs could block cleanup forever and delayed requests could run after cancellation; durable daemon records and cancellation tombstones resolve both, while folding the hook schema into 0113 avoids colliding with PR 2’s migration slot.
Verification supplied with rounds 5–7
The final worker report records 95/95 typecheck/lint tasks, 3,967 app tests, 2,320 server tests, 57 integration tests, 445 DB tests and 575 daemon tests passing; the remaining-package sweep passed 85/85 tasks with capped workers. Live checks covered owned-workspace setup/removal, checkout retention, a server restart during setup, dropped dispatch with claim retention and release, rejection of delayed dispatch, and daemon-crash process-group recovery. These are supplied verification results, not suites rerun for this report update.
Verification supplied with PR 1 · original capture revision
Static checks and suites
- Typecheck/lint: 95/95 tasks.
- App: 484 files / 3,965 passed; server: 231 passed files / 2,296 tests.
- CLI 548; DB 445; host daemon 562; plugins checkout/worktree/personal 28/37/10.
- Integration Turbo task 27 files / 76 tests; separate direct integration run 26 files / 54 tests. These are separate reported commands, not additive coverage.
Live behavior
- 35/35 live CLI checks across three providers: new, follow-up, fork, reuse, archive/unarchive, delete and child threads.
- Defaults, path shorthand, unknown provider, dirty/busy branch refusal, existing/new branch switch, and two projects sharing one checkout.
- Server killed during create, then recovery to completion. Teardown removes owned paths and retains checkout.
- Compact drawer root remained neither inert nor aria-hidden; iOS 26.5 Safari selection and reopen passed. No animation-performance claim.
Migration dry run
- Read-only backup of real DB; only the copy migrated, then deleted.
- 23/23 invariants; zero anomalies; integrity_check=ok; valid foreign keys.
- 484 legacy environments → 467 worktrees / 9 personal / 8 checkouts; all entity counts preserved.
- Real snapshot had no pending intents. Dedicated tests cover pending conversion and all three NULL-path kinds.
TMPDIR=/private/tmp and --env-mode=loose; registry regeneration repaired stale generated checks. Earlier migration/fixture mistakes and two platform-sensitive tests were fixed. The supplied report records no outstanding test failures; these suites were not rerun for this publication-only change.History: #3194 review rounds, plan Decisions/Rounds logs, and supplied pr1-report.md and surface-report.md. Raw private evidence is not published.
7. Breaking changes and known issues
Compatibility limits and rollout constraints for this eleven-commit PR.
Compatibility
- Deprecated
managed,workspaceProvisionType, environment/sidebar display-kind and agent-configuration compatibility fields remain derived responses. Migrate to provider identity/ownership and actual Git facts. EnvironmentStatusremovesretiringanddestroying; consumers must use lifecycle. SDK patch version is 0.4.49.- Daemon protocol 183 → 191 forces version-mismatch update; rounds 5–7 advance 188 → 191 for core hooks, restart reconciliation and durable cancellation confirmation. Unlike the broader #3194, PR 1 retains
Host.type, enrollment/session fields and installer behavior. - Migration 0113 changes environment tables and intents, adds the hook-operation table and defaults ownership to false; regenerated host and host-session schemas equal main.
Tooling and presentation
- An installed bb older than this branch may fail
bb plugin buildfor checkout/worktree app bundles because its export list lacks the new branch hooks. Use a bb built from this branch; repository package tests are green. - Compact Info ellipsizes the leading “Not created” label but retains provisioning failed; desktop verifies the full text.
- Unavailable thread-storage feedback in the failed fixture is expected because no workspace exists.
- Screenshot fixtures differ in dimensions, machine count and exact state; no performance or pixel-parity claim follows from these images.
Lifecycle constraints
- Removal retry is uncapped, as on main;
createTimeoutMsdefaults to null. - Destroyed rows remain auditable without a usable directory. User-owned checkout deletion retains the directory.
- Selections/inputs are persisted and visible to plugins; credentials stay in settings.
- No hostless environments or new-machine selection are accepted in this PR.
Publication validation
HTML structure, JavaScript, image references, report controls and responsive layout are checked locally. Every published image is viewed and OCR-scanned before and after opaque redaction. Only this HTML and PNGs are committed. The documented repository publish script was absent from the clone and searched local files, so local validation and a private backup replace that step; no issue-summary/index changes are required for this PR page.
8. Deferred follow-ups
The split plan is future scope, not a claim that those implementations ship in #3227.
PR 2 · machines
- Machine-provider contract, launches/lifecycle and machine controls; migration 0114 adds host fields and removes Host.type.
- Server access (Connect/direct), enrollment/bootstrap and thin execution helpers.
- Opt-in catalog providers planned for Modal, DigitalOcean, E2B and SSH; no machine plugins auto-installed.
- New-machine selection extends the existing nested machine shape used by PR 1.
PR 3 · rift
- Catalog environment provider using rift create/remove from the checkout; not auto-installed.
- Default/named branch and copy-all/filtered inputs; setup-required until the rift CLI exists.
- Presentation trait groupsThreads; rift postcreate hooks before bb setup.
C · ecosystem API
- C1: semantic ownership, persistence, isolation and capability traits.
- C2: reusable host-owned environment picker for plugin launchers.
- C3: environment.changed event. Inputs persistence/no-credentials documentation is already included here.
- C4: move update_environment_directory to checkout after attach/move SDK primitives exist.
- C5: deprecate legacy type: host create sugar.