You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem — On a base peer-studio session (a remote Studio session opened on the MacBook), the Enable control affordance is a no-op and is mispositioned. Clicking it does nothing: the app posts the amicode.fleet.enableControl command over the app→extension bridge, but that command is not on the bridge allowlist and no extension command is registered for it, so the envelope is dropped before anything runs. The self-owned enable path (evaluateControlBootstrap / enableSelfOwnedControl) has zero live callers — it is unit-tested but never invoked. Separately, the affordance renders as a fixed button in the top-right over the titlebar, not over the composer. Net effect: B2b's headline act (take control of a peer session) cannot actually be performed from the UI despite the state channel and write plane being wired. Approach — Wire the enable act end-to-end and re-home the affordance. (1) Replace the bare command post with a payload-carrying bridge envelope (carrying the target owner machineId + sessionID), mirroring the existing project-selected / bug-report envelope kinds — the bare command lane cannot carry a target. The extension handler shows the VS Code native-modal confirm (ADR 0034 D4) and, on confirm, calls enableSelfOwnedControl to mint + persist the self-owned control grant; the next fleet-projection poll flips amicode_control to interactive and lights the driving banner. (2) Re-home the affordance from the fixed top-right Portal into a composer-anchored overlay: blur the composer and center a "This session lives on <peer> — Enable control to drive it" card with the button; the composer is gated (not typeable) until control is held. Scope — in: the app→extension bridge envelope + allowlist for the enable act; the registered extension handler (native modal → enableSelfOwnedControl → grant persistence); the composer-scrim placement + gating; the projection→interactive flip driving the banner. · out: the shared-peer Request control handshake (that stays inert, backend is #1545); any change to the observation read/write planes (#1542/#1543) or the state-channel projection shape (#1544); real-board device writes. Assumptions — Self-owned peer (JJ owns both machines) → isSelfOwned is true, so eligibility is enable-control (live), not request-control. The control grant is persisted through the existing lifecycle-grant writer; the projection resolver (buildControlResolver) already reads control grants keyed by targetMachineId, so no resolver change is needed once a grant exists.
Acceptance Criteria
Clicking Enable control on a self-owned remote peer session shows a VS Code native-modal confirm; confirming mints and persists an active self-owned control grant for the target peer.
After the grant is minted, the session's projected amicode_control resolves to interactive, the affordance disappears, and the persistent "Driving <peer>" banner appears.
Cancelling the native modal mints no grant and leaves the affordance and gated composer unchanged.
The enable envelope carries the target owner machineId (and sessionID) and is honored only for that peer; an envelope for a non-self-owned peer does not mint a grant (no-privilege-bleed, matching evaluateControlBootstrap).
The affordance renders as an overlay over the composer, not the titlebar: the composer is blurred and its input is not editable while control is not held; the CTA card is centered on it with the peer name.
The composer becomes editable (control held / interactive) or when the session is local — the scrim never appears on a local or already-controlled session.
Extend the existing fleet_control_bootstrap test suite for the handler's decision path (authorize on self-owned+verified, refuse otherwise) — reuse, don't recreate.
Extend the chat-bridge test(s) that pin BRIDGE_ALLOWED_COMMANDS / envelope handling to cover the new enable envelope (allow + payload shape + reject unknown peer).
Extend the session-header app test suite (which already asserts the affordance markers and drivingBannerFromProjection wiring) for the composer-scrim placement + gated-composer state, replacing the top-right-Portal assertions.
Keep the sidebar_fleet_section regression guard green (control affordances never leak into the read-only sidebar).
Key Decisions
Payload envelope, not the command lane.postAmicode is command-string-only; the enable act needs a target. Add a dedicated bridge kind (e.g. fleet-enable-control) carrying { ownerMachineId, sessionID }, handled beside the existing kind handlers — do not overload the generic command relay.
Modal + mint live in the extension host. The native modal and enableSelfOwnedControl run extension-side (the app cannot show a VS Code modal nor persist a grant); the app only posts intent and re-reads the projection.
Grant persistence flips the projection; the UI does not self-declare interactive. The banner/affordance derive from the projected amicode_control, which re-resolves from the persisted grant on the next /amicode/fleet/sessions poll — the single source of truth stays the SoT, not optimistic local state.
Composer gating is the observe-vs-drive signal. Blurred + non-editable composer with the CTA is the deliberate "you are observing, not driving" state; enabling control is the one act that lifts it.
This is the last mile of B2b: #1544 shipped the state channel (projection → chip/affordance) and #1542 the write plane (which only bites once a grant exists), but the enable act that produces the grant was never connected to a command. It matches the honest gap recorded during the campaign ("CONFIRM safety NOT wired live … NO live caller").
Important
Problem — On a base peer-studio session (a remote Studio session opened on the MacBook), the Enable control affordance is a no-op and is mispositioned. Clicking it does nothing: the app posts the
amicode.fleet.enableControlcommand over the app→extension bridge, but that command is not on the bridge allowlist and no extension command is registered for it, so the envelope is dropped before anything runs. The self-owned enable path (evaluateControlBootstrap/enableSelfOwnedControl) has zero live callers — it is unit-tested but never invoked. Separately, the affordance renders as a fixed button in the top-right over the titlebar, not over the composer. Net effect: B2b's headline act (take control of a peer session) cannot actually be performed from the UI despite the state channel and write plane being wired.Approach — Wire the enable act end-to-end and re-home the affordance. (1) Replace the bare command post with a payload-carrying bridge envelope (carrying the target owner machineId + sessionID), mirroring the existing project-selected / bug-report envelope kinds — the bare command lane cannot carry a target. The extension handler shows the VS Code native-modal confirm (ADR 0034 D4) and, on confirm, calls
enableSelfOwnedControlto mint + persist the self-ownedcontrolgrant; the next fleet-projection poll flipsamicode_controltointeractiveand lights the driving banner. (2) Re-home the affordance from the fixed top-right Portal into a composer-anchored overlay: blur the composer and center a "This session lives on<peer>— Enable control to drive it" card with the button; the composer is gated (not typeable) until control is held.Scope — in: the app→extension bridge envelope + allowlist for the enable act; the registered extension handler (native modal →
enableSelfOwnedControl→ grant persistence); the composer-scrim placement + gating; the projection→interactive flip driving the banner. · out: the shared-peer Request control handshake (that stays inert, backend is #1545); any change to the observation read/write planes (#1542/#1543) or the state-channel projection shape (#1544); real-board device writes.Assumptions — Self-owned peer (JJ owns both machines) →
isSelfOwnedis true, so eligibility isenable-control(live), notrequest-control. The control grant is persisted through the existing lifecycle-grant writer; the projection resolver (buildControlResolver) already readscontrolgrants keyed bytargetMachineId, so no resolver change is needed once a grant exists.Acceptance Criteria
controlgrant for the target peer.amicode_controlresolves tointeractive, the affordance disappears, and the persistent "Driving<peer>" banner appears.evaluateControlBootstrap).Testing Decisions
fleet_control_bootstraptest suite for the handler's decision path (authorize on self-owned+verified, refuse otherwise) — reuse, don't recreate.BRIDGE_ALLOWED_COMMANDS/ envelope handling to cover the new enable envelope (allow + payload shape + reject unknown peer).drivingBannerFromProjectionwiring) for the composer-scrim placement + gated-composer state, replacing the top-right-Portal assertions.sidebar_fleet_sectionregression guard green (control affordances never leak into the read-only sidebar).Key Decisions
postAmicodeis command-string-only; the enable act needs a target. Add a dedicated bridgekind(e.g.fleet-enable-control) carrying{ ownerMachineId, sessionID }, handled beside the existingkindhandlers — do not overload the generic command relay.enableSelfOwnedControlrun extension-side (the app cannot show a VS Code modal nor persist a grant); the app only posts intent and re-reads the projection.amicode_control, which re-resolves from the persisted grant on the next/amicode/fleet/sessionspoll — the single source of truth stays the SoT, not optimistic local state.Constraints & Invariants
fleet_headless_rehydrationinvariant).Prior Art
fleet_control_bootstrap(evaluateControlBootstrap,enableSelfOwnedControl) — the dead-but-tested enable modules this issue gives a live caller.remote_session_state(projectSessionControlState,buildControlResolver) — the SoT→projection resolver that flips tointeractiveonce a grant exists.chat_bridge(BRIDGE_ALLOWED_COMMANDS,handleAmicodeBridgeMessage) + the chat panel'sonDidReceiveMessagekindhandlers (app-ready / session-context / watch-files / project-selected) — the envelope precedent to mirror.session-fleet-peers(controlAffordance,drivingBannerFromProjection) + the session header's affordance/banner render — the app surface to re-home.Source
feature/free-tier-fleet.feature/free-tier-fleet(notmain).Notes
This is the last mile of B2b: #1544 shipped the state channel (projection → chip/affordance) and #1542 the write plane (which only bites once a grant exists), but the enable act that produces the grant was never connected to a command. It matches the honest gap recorded during the campaign ("CONFIRM safety NOT wired live … NO live caller").