Reviewer report · Draft PR #3194 · bb/environment-targets-core

Environment providers move workspace and machine lifecycle into plugins.

Eight commits replace bb’s built-in environment provisioning with two experimental provider contracts, then ship four implementations: project checkout, Git worktree, personal workspace, and Modal sandbox. Modal is the only machine provider.

HEAD 7e1ed2fdb3base 06aeaa9949570 files+40,771−16,965protocol 183→187SDK 0.4.48→0.4.55migration 0112→0113

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.

+23,806net lines across the exact base…HEAD diff
8 commitsfive implementation slices, provider removal, path-less backfill, then migration-scope cleanup
2 contracts / 4 pluginsenvironment + machine contracts; three local environment plugins and Modal

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.

b664bc3d4a
API Core
contracts, migration, launch coordinators, routes/CLI, plugin app/host APIs, daemon attach/process primitives, core deletions
+32,524 / −17,093 · 504 files
e5e1abe545
Project Checkout Plugin
reuse/current checkout, branch selection hooks, remote clone on a provider-created machine
+2,552 / −235 · 17 files
5ae0c32c93
Git Worktree Plugin
managed worktree create/rebuild/remove, branch picker and host-data ownership
+2,851 / −233 · 22 files
2c5e81c6f8
Personal Workspace Plugin
projectless per-thread folders and inheritance-compatible paths
+631 / −0 · 11 files
a3203e43c8
Modal Sandbox Plugin
sandbox create, snapshot suspend/resume, provider availability, settings and app picker sugar
+2,938 / −68 · 20 files
8b4d56e38a
Remove the SSH machine plugin
delete the implementation before landing; retain generic no-icon/no-suspend contract coverage
+50 / −36 · 11 files
3cdbc227f5
Backfill provider ids for path-less environments
map every legacy workspace row by kind, even when a failed row has no path
+97 / −5 · 2 files
7e1ed2fdb3
Limit migration 0113 to what main persisted
remove the ephemeral-host and Modal-environment backfill for an intermediate branch state
+3 / −170 · 6 files

Area footprint and suggested reading order

#AreaAddedDeletedNetRead for
1packages/plugin-sdk1,64818+1,630public contracts and experimental app/host surfaces
2packages/db5,731758+4,9730113 schema and compatibility migration
3apps/server13,4166,194+7,222selection, coordinators, replay, cleanup, routes and policy
4apps/host-daemon1971,554−1,357protocol 187; provisioning deletion; attach-only host primitive
5packages/host-workspace1353,865−3,730deleted core provisioning; retained inspection/process tools
6apps/app6,7593,659+3,100provider-driven composer, Info, Machines and plugin category
7docs31355+258CLI/guide/audit discoverability

Provider plugin size

PluginTree lines at 7e1ed2fdb3
source / test
Diff-added vs mainRole
Project checkout1,531 / 7372,337checkout/reuse adapter and branch hooks
Git worktree1,726 / 8061,935managed local workspace reference implementation
Personal workspace188 / 337585projectless reference implementation
Modal sandbox1,485 / 8832,555suspendable 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.provision and environment.destroy; core becomes environment.attach plus inspection.
  • The three-kind workspaceProvisionType selection and the persisted environments.workspace_provision_type/managed columns.
  • The Host.type field, whose only value was persistent; 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.tsx
  • apps/app/src/views/root-compose-branch-selection.test.ts
  • apps/app/src/views/root-compose-branch-selection.ts
  • apps/app/src/views/root-compose-branch-ui.test.ts
  • apps/app/src/views/root-compose-branch-ui.ts
  • apps/server/src/services/environments/environment-cleanup-internal.ts
  • apps/server/test/environments/environment-cleanup.test.ts
  • apps/server/test/public/public-thread-environment-decoupling.test.ts
  • apps/server/test/services/managed-environment-cleanup-recovery.test.ts
  • packages/host-workspace/test/provisioning.test.ts
  • packages/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 surfaceBranch behaviorCompatibility
environments.managed and environments.workspace_provision_typeProvider 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 | destroyingLifecycle phase is separate from availability status; migration maps retiring→ready, destroying→error.Removed from the typed status union.
Host.typeMachine provider identity and lifecycle fields replace it.Removed; at main its schema had only persistent and classified nothing.

Migration 0113

TableAddedDropped / 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.
environmentsprovider 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.
hostsmachine 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_sessionsThe 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.

Desktop 1280×800Mobile 390×844; checkout chips 500×844REPORT3 fixturesChromium / dev-browser 1.0.0-rc.3Dark app theme

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.

SurfaceBranch behaviorDecision / parity
New-thread environment pickerRows 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 composePersonal workspace is implicit and does not add a redundant environment chip.D71
Matches main’s projectless default.
Checkout chipClosed/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 chipsProject, environment, branch and permission remain separate, with provider display names replacing core kinds.D63, D69
Same information density as main.
Reuse rowThe reuse menu lists the same eligible existing environments as main, and picking one behaves as on main.D67
Main reuse semantics preserved.
Provisioning failuresFor 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 threadsFollow-up creation row is replaced with archived status; unarchive emits a lifecycle event.D24
Main archive behavior retained.
Info tabShows provider display, lifecycle, directory/branch/git status; destroyed workspaces remain inspectable as Destroyed/Unknown without dereferencing a path.D63, F7
Sidebar groupingProject worktree glyph/grouping derives from provider identity/traits; projectless children still group by the parent rule.D0, D1, D40
Settings navThere 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-threadsA 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 pageManual 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 machineManual pairing remains. Modal is the only plugin-provided machine creation row.Q6, S10, F19
Plugins directoryThe 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.
  • requires facts 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/destroying is removed; use lifecycle phase.
  • Main’s Host.type, whose only permitted value was persistent, 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 build with 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 is null because 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.
Expected verification noise. On macOS, two Plugin Build tests initially compared /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

  1. Define a stable provider-traits vocabulary instead of continuing to infer behavior from provider IDs.
  2. Add a reusable host-owned environment picker.
  3. Add environment.changed and finish the inputs documentation surface.
  4. Move update_environment_directory ownership to the checkout plugin.
  5. Deprecate the legacy type:"host" thread-create sugar.
  6. Represent manual enrollment as a machine provider.
  7. 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.