Files
stack/docs/plans/2026-08-09-webui-fleet-claude-bridge.md
T
velmaandmos-dt-0 24bbd40dc7
ci/woodpecker/push/publish Pipeline failed
docs: WebUI fleet Claude bridge — Task 0 decision plan (#1131)
Docs-only plan PR. FRED_APPROVED_REF=0629361ca39a4dd7fb3e575d11c64bea9e545dae (review 147). Merged by fred (orchestrator) via API: pr-merge.sh policy predates the next lane (main-only hardcode) — wrapper fix tracked separately.

Co-authored-by: Velma <[email protected]>
2026-08-09 10:28:41 +00:00

115 KiB
Raw Blame History

WebUI Fleet Claude Bridge Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Conditional instruction: The line above is dormant until Fred approves every Section 2 item, all [FRED-GATE] markers are replaced by recorded decisions, and IMPLEMENTATION_BASE_SHA is recorded. Until then: review and planning only.

Goal: Prove one authenticated browser conversation backed by one native Claude Code OAuth session launched through the authoritative Mosaic launcher on one enrolled Agent Host.

Architecture: Keep provider OAuth and process ownership on an outbound-connected Agent Host. The Gateway authorizes and durably binds the browser conversation to a normalized runtime provider, while a host-side adapter invokes the installed mosaic claude command and converts Claude stream-json frames into redacted, ordered Mosaic events. Reuse the existing Web chat event contract and durable interaction session instead of creating a second prompt-composition or transcript path.

Tech Stack: TypeScript, Node.js 22+, pnpm/Turborepo, NestJS 11 + Socket.IO, Next.js 16 + React 19, Vitest, Playwright, native Claude Code stream-json, existing @mosaicstack/{types,agent,mosaic} packages.


STATUS: DRAFT FOR FRED STRUCTURE REVIEW — DO NOT IMPLEMENT YET.

Fred must approve the launcher/seat-home and one-host enrollment contract below before code. This plan deliberately does not read the lease broker, alter the local roster schema, start Gateway/Web around the database hold, or touch the legacy deployed WebUI.

Review status: Fred authorized Task 0's initial docs-only decision PR on the next lane in comms/20260809T094952Z__from-fred__ec0e85.md. That authorization does not extend to Task 1. This revision becomes an executable coding handoff only after the PR records Fred's complete answers, removes every [FRED-GATE], and names an IMPLEMENTATION_BASE_SHA containing the W-F-owned launcher. That implementation base is currently BLOCKED-ON-W-F.

Required execution load order, skills, and conventions

Before implementation, load completely and in this exact order:

  1. ~/.config/mosaic/SOUL.md
  2. ~/.config/mosaic/STANDARDS.md
  3. ~/.config/mosaic/AGENTS.md
  4. ~/.config/mosaic/guides/E2E-DELIVERY.md
  5. repository AGENTS.md
  6. ~/.config/mosaic/runtime/claude/RUNTIME.md

Then load executing-plans, test-driven-development, nestjs-best-practices, next-best-practices, vercel-react-best-practices, webapp-testing, lint, requesting-code-review, and verification-before-completion. Follow ESM .js imports, explicit Nest @Inject() decorators, dedicated *.dto.ts boundaries, typed Socket.IO events, strict TypeScript, and headless-only browser automation.

Tasks 18 use unit tests, fake transports, and already-approved test fixtures only. They must not start Gateway/Web, connect PostgreSQL, apply DDL, or invoke a migration. The first real Gateway/Web startup is Task 9 and remains blocked until Fred/Scooby certify that path.

1. Reconciled baseline and hard gates

Evidence pins, not yet the implementation pin

Surface Verified state on 2026-08-09
Stack main b0f7d26dd9c14d91eaaefc35d6c9fd6618a0bd92
Stack next 4df478cdd150fdf8d52ea109f02ade5d85017acd
Branch relation Diverged: main has 11 unique commits; next has 13
Required next fix #689 / 56787fab: disable Redis consumers on local tier
Web chat Authenticated Socket.IO chat backed by an in-process Pi SDK session
Runtime provider registry Hermes only
Provider lifecycle list/tree/stream/send/attach/detach/terminate; no create/start
Framework structure Three-root seat-home target is approved but not yet built
Live WebUI Legacy v0.0.20, reference-only

Neither current branch contains the complete required state. No implementation commit may be based on either evidence SHA. Before Task 1, Fred must identify one immutable reconciled commit that contains or cleanly contracts with:

  1. current main Gateway/Web/runtime-provider behavior;
  2. #689 or an equivalent certified local-tier queue fix;
  3. the Node 22/resolved-path launcher requirement;
  4. the Fred-approved generated seat-home launch seam;
  5. a certified Gateway/Web startup path that does not bypass the KBN/database hold.

Record that SHA as IMPLEMENTATION_BASE_SHA in this plan and the task scratchpad. If no such SHA exists, implementation remains blocked; do not cherry-pick an ad hoc private baseline.

Verified no-migration storage facts and limitation

At the evidence pin, packages/db/src/schema.ts provides:

  • interaction_sessions.id as the stable primary key with required owner, tenant, provider, and runtime-session fields;
  • interaction_outbox with kind, content, digest, correlation/channel IDs, and pending | processing | delivered status;
  • a unique (session_id, idempotency_key) outbox index;
  • no create-operation, policy, enrollment, terminal-state, or approval-acceptance columns.

apps/gateway/src/agent/durable-session.repository.ts currently updates provider/runtime identity on same-scope create(), so baseline behavior is not safe for M1. Section 4.3 defines an explicit pending-reference/outbox encoding and CAS repository port without DDL. [FRED-GATE] Task 0 must certify that this encoding and transaction are acceptable. If it cannot, Stop Condition 9 applies; workers may not invent a migration.

Ownership boundaries

Fred/framework owns and must freeze:

  • ~/.config/mosaic system root, ~/.mosaic user root, and untouched stock runtime homes;
  • generated seat home and CLAUDE_CONFIG_DIR behavior;
  • roster/profile/seat identity and MOSAIC_AGENT_NAME binding;
  • absolute resolved Node/Mosaic/runtime paths;
  • the machine-facing launch boundary that produces a native process with the correct generated home;
  • any future read-only lease/VERIFIED status adapter.

Velma owns after Fred approves the contract:

  • one-host Gateway connection and authorization;
  • normalized runtime create/send/stream/stop routing;
  • host-side Claude stream adapter around the authoritative launcher;
  • Web host readiness/session launch/chat rendering;
  • tests, redaction, reconnect behavior, and documentation.

Velma must not implement:

  • direct broker socket, daemon state, or broker file reads;
  • independent roster or seat-home semantics;
  • framework prompt composition;
  • Gateway-owned provider OAuth;
  • tmux pane scraping;
  • Matrix/federation, generalized enrollment, broad agent management, or other runtimes in M1.

2. Decisions proposed for Fred approval

Fred should mark each item APPROVE or return one replacement contract before implementation. Each unresolved item is a [FRED-GATE]; workers must not choose for themselves.

  1. One normalized operation [FRED-GATE]: add session.create / createSession; “start” is the HTTP/CLI lifecycle verb, not a second provider operation.
  2. W-F-bound launch descriptor [FRED-GATE]: Section 4.1 proposes ResolvedSeatLaunchDescriptorV1 as a candidate consumer contract to Fred's W-F harness-homes/launcher work. Its exact field list, artifact pinning, commitment mechanism, custody, and generation checks are not settled requirements; Fred may accept, replace, simplify, or defer them at D2-binding time. The invariant proposed by this plan is narrower: Agent Host consumes the W-F-produced resolved contract and neither searches ambient PATH nor trusts ambient HOME, MOSAIC_HOME, CLAUDE_CONFIG_DIR, or runtime paths.
  3. Server-resolved target [FRED-GATE]: the browser request carries only the durable conversation ID and an idempotency key. It cannot submit host URLs/IDs, shell commands, argv, workspace paths, runtime credentials, native session IDs, or policy fields.
  4. One pinned launch policy [FRED-GATE]: M1 resolves one host, roster seat/persona, opaque workspace ID, runtime claude, and policy ID read-only-chat. Fred provisionally bound the W-C read-only tool-label carve-out to exactly {Read, Grep, Glob}; no other tool label may be inferred. Policy remains non-YOLO, with no arbitrary extra args or mutating-tool approval response. If Claude emits approval.required, Web shows the state and allows Stop; it does not approve the provider action in M1.
  5. Outbound transport [FRED-GATE]: dedicated authenticated Socket.IO namespace /agent-host over WSS. Matrix remains a follow-on provider transport.
  6. Bounded enrollment manifest [FRED-GATE]: one operator-owned AgentHostEnrollmentManifestV1 binds credential ID, generation, host ID, owner ID, tenant ID, launch-policy digest, issued/expiry timestamps, and revoked state. Gateway revalidates it before every command and accepted event. Productized pairing/UI remains M2.
  7. Credential custody [FRED-GATE]: Fred supplies exact private Agent Host enrollment credential/manifest paths under the user root plus framework-manifest ownership. This credential is entirely separate from Claude OAuth. Claude OAuth stays in the native runtime location selected only by the descriptor's generated CLAUDE_CONFIG_DIR. Enrollment token is separate from non-secret config, regular, no-follow, owner-only, read once per validated generation, and never argv/log/event data.
  8. Connection replacement [FRED-GATE]: Gateway assigns a random connection epoch. A higher valid enrollment generation replaces and disconnects the old connection; equal/lower duplicate generations are refused. Late events from an old epoch are rejected.
  9. Single active launch [FRED-GATE]: Gateway reserves the stable conversation before host dispatch; host also enforces one active launch per stable conversation independent of browser idempotency key. Different concurrent keys cannot spawn two processes.
  10. No M1 schema migration [FRED-GATE]: reuse interaction_sessions plus durable interaction_outbox entries of kind provider.create / provider.stop for reservations and reconciliation. Host uses an exclusive, atomic private operation journal. If existing tables cannot safely provide these semantics, stop at Task 0 rather than adding an unauthorized migration.
  11. M1 authorization [FRED-GATE]: M1 is single-operator: Jason is the only tenant. Preserve a server-derived userIdtenantId seam without building multi-tenant machinery. Only the manifest-bound admin owner/tenant may list the host or create/stop sessions; Gateway derives the mapping from validated operator config, never host hello or browser fields.
  12. Provenance visibility [FRED-GATE]: browser receives the Section 4.2 safe target DTO only. Runtime audit stores allowlisted IDs, versions, launch ID, enrollment/contract generation, and only the path-free provenance fields selected by D2/D12. Path strings are neither logged nor hashed. Host-private evidence follows the final W-F contract; live QA emits only approved booleans/digests.
  13. No lease display [FRED-GATE]: host reports launcher/auth readiness and sanitized launch failures only. Agent Host has no broker API/path/status logic; it merely delegates to Freds sealed launcher, which may internally enforce broker policy. VERIFIED/TTL display remains F-V3/Fred-owned.
  14. Gateway restart [FRED-GATE]: never auto-launch. Pending create/stop operations reconcile from durable outbox plus host operation journal. An in-flight transcript turn without a durable accepted cursor is marked indeterminate and is not auto-persisted/resumed. Full process adoption after host-daemon restart remains M2.
  15. Stop [FRED-GATE]: browser first obtains a server-derived exact-action approval and stopOperationId. Gateway durably accepts that exact approval once, journals dispatch, and reconciles ambiguous responses by the same operation ID without accepting another approval. Host exact-target termination is idempotent.

Fred decision return packet

Fred's response must fill every value below; “approve direction” alone does not close Task 0.

Decision Required recorded value
D1 normalized create approve session.create as the only provider operation and HTTP/CLI start as naming only
D2 resolved launcher W-F binding verdict for the Section 4.1 candidate: final producer/API and accepted/replaced descriptor, custody, commitment/pinning, generation, and Node→Mosaic→interpreter→Claude rules; no candidate field is binding before that verdict
D3 pinned target exact host label/ID, seat, persona, byte-equal MOSAIC_AGENT_NAME, workspace ID/label, absolute host-only working directory, generated HOME, CLAUDE_CONFIG_DIR, and MOSAIC_HOME
D4 launch policy policy ID/digest, exact read-only tool-label allowlist, final non-YOLO Claude flags, and V1 environment allowlist/no inherited extras
D5 transport exact Gateway WSS URL, /agent-host namespace, TLS termination, heartbeat, reconnect/backoff, and maximum frame values
D6 enrollment manifest host/Gateway manifest sources, final W-F-bound schema/generation/expiry values, any descriptor commitments selected by D2, policy digest, reload signal authority, and systemd owner
D7 credential custody host/Gateway token and any D2-selected private commitment-key sources/formats, owner/mode/no-follow rules, rotation installer/authority, and proof Claude OAuth is separate under generated CLAUDE_CONFIG_DIR
D8 connection generations approve higher-generation replace/equal-lower refuse, config-epoch swap order, old-event rejection, and expiry/revocation timer
D9 one active launch approve owner/tenant/durable-session uniqueness on Gateway and host, including rotation and concurrent distinct browser keys
D10 existing storage only certify Section 4.3 encodings/repository transitions/outbox statuses with no DDL, or declare M1 blocked
D11 authorization exact authenticated Web userId, derived tenantId, deployment agentName, and server-owned interaction-http channel mapping
D12 presentation/provenance exact safe host/workspace/seat/persona labels, browser readiness/connection fields, D2-selected safe contract provenance, and audit artifact fields; no raw or hashed path strings
D13 F-V3/cleanup approve no bridge broker API/status/read path and give exact Fred-owned cleanup procedure for an indeterminate child
D14 restart behavior approve startup enumeration, create/stop lookup/status actions, no auto-launch, in-flight turn indeterminate behavior, and operator-visible recovery state
D15 exact stop approve verify→durable accept→claim/GETDEL→dispatch state machine, two-minute deadline, retry/status response semantics, and exact child termination
Immutable source exact FRED_APPROVED_REF; full IMPLEMENTATION_BASE_SHA containing the W-F-bound launcher and plan, currently BLOCKED-ON-W-F
Certified execution safe CERTIFIED_WEB_URL; literal private CERTIFIED_DATABASE_URL_LOADER that writes only the disposable URL to captured stdout; and KBN-safe startup/post-merge scripts supporting --worktree, --source-sha, and --web-url; smoke stdout is the exact safe JSON schema asserted in Task 10
Versions/readiness pinned Node, Mosaic, Claude, adapter/descriptor protocols, exact safe auth-readiness argv/shape—or explicit unknown-until-launch

3. Target execution spine

Browser (Authentik session)
  │  existing conversation + Socket.IO chat contract
  ▼
Gateway RuntimeChatBridgeService
  │  owner/tenant scope + durable interaction binding
  ▼
MosaicHostRuntimeProvider (`runtime.agent-host`)
  │  authenticated command ID + idempotency + normalized events
  ▼
Outbound-connected Agent Host (`mosaic agent-host connect`)
  │  exact generated seat + workspace policy
  ▼
<absolute node> <absolute mosaic-cli> claude
  │  --print --input-format stream-json --output-format stream-json
  │  --include-partial-messages --replay-user-messages --verbose
  │  --session-id <gateway-generated UUID> + frozen policy flags
  ▼
Claude Code native OAuth + host-local generated `CLAUDE_CONFIG_DIR`

The exact Claude flags remain subject to Task 1s red-first compatibility certificate against the pinned target version. No flag is production-authoritative merely because Claude Code 2.1.226 advertised it during discovery.

4. Contract freeze

4.1 Candidate consumer descriptor proposed to Fred's W-F work

[FRED-GATE] The following ResolvedSeatLaunchDescriptorV1 is non-binding design input to W-F, not a plan-imposed framework requirement. Fred owns the producer and final shape. W-F may accept, replace, simplify, or defer any listed field, artifact digest, or commitment mechanism. The Agent Host implementation must consume only the final D2-bound contract and must not infer omitted values:

export interface ResolvedSeatLaunchDescriptorV1 {
  protocolVersion: 1;
  generation: number; // integer 1..2_147_483_647
  producerId: string;
  privateDescriptorCommitment: `hmac-sha256:${string}`;
  publicContractDigest: `sha256:${string}`;
  hostLabel: string;
  seatName: string;
  personaLabel: string;
  workspacePolicyId: string;
  workspaceLabel: string;
  launchPolicyDigest: `sha256:${string}`;
  workingDirectory: string;
  nodePath: string;
  mosaicCliPath: string;
  claudePath: string;
  descendantExecutables: readonly Readonly<{
    role: 'lease-launcher' | 'python' | 'runtime-helper';
    path: string;
  }>[];
  expectedArtifacts: Readonly<{
    node: `sha256:${string}`;
    mosaicCli: `sha256:${string}`;
    claude: `sha256:${string}`;
    descendants: readonly Readonly<{
      role: 'lease-launcher' | 'python' | 'runtime-helper';
      digest: `sha256:${string}`;
    }>[];
  }>;
  seatHome: string;
  claudeConfigDir: string;
  mosaicHome: string;
  environment: Readonly<{
    HOME: string;
    CLAUDE_CONFIG_DIR: string;
    MOSAIC_HOME: string;
    MOSAIC_AGENT_NAME: string;
    PATH: string;
    LANG: 'C.UTF-8';
    LC_ALL: 'C.UTF-8';
    TERM: 'dumb';
    TMPDIR: string;
  }>;
}

Candidate mechanics, subject entirely to D2/W-F binding: privateDescriptorCommitment uses HMAC-SHA-256 over RFC 8785/JCS canonical full descriptor JSON with both commitment fields omitted, while publicContractDigest covers a safe path-free projection. A separate Fred-owned key and pre-spawn artifact-byte verification are proposed defenses, but W-F may simplify or defer either mechanism for V1. If selected, private values stay out of browser/audit/log data and audit retains only the D2-approved safe projection.

The candidate manifest compares the D2-selected descriptor provenance, generation, and policy before spawn. Candidate byte-exact invariants are environment.HOME === seatHome, CLAUDE_CONFIG_DIR === claudeConfigDir, MOSAIC_HOME === mosaicHome, and MOSAIC_AGENT_NAME === seatName; no ambient path/home value survives. These are proposed consumer needs, not claims about an already-built producer.

The final W-F contract defines which secure-open, ownership/mode, symlink/hardlink, generation, commitment, and artifact-pinning checks exist in V1. The approved immutable baseline must contain that bound launcher before Task 1. Task 1 may certify launch.ts and add consumer tests against the final contract, but Velma must not patch Fred-owned launcher behavior; a failing descendant certificate returns to Fred and blocks code.

Downstream binding rule: every descriptor field, commitment, digest, custody check, and descendant-path assertion named later in this plan is a symbolic candidate slot. Before Task 1, Task 0 must replace or remove each slot to match Fred's final D2/W-F contract. No downstream DTO, test, audit field, or acceptance criterion may preserve a Section 4.1 mechanism that W-F did not bind.

4.2 Runtime create and compatibility contract

Add session.create and a required createSession() method in packages/types/src/agent/agent-runtime-provider.ts. Keep browser input separate from the server-resolved provider command. Descriptor-derived names in the following DTO/protocol examples are candidate symbolic slots included to make the consumer flow reviewable; Task 0 must conform them to the final D2/W-F shape before implementation:

export interface BrowserRuntimeSessionStartInput {
  idempotencyKey: string;
}

export interface RuntimeSessionCreateRequest {
  durableSessionId: string;
  idempotencyKey: string;
  createOperationId: string;
  nativeSessionId: string;
  requestDigest: `sha256:${string}`;
  expectedPrivateDescriptorCommitment: `hmac-sha256:${string}`;
  expectedPublicContractDigest: `sha256:${string}`;
  expectedLaunchPolicyDigest: `sha256:${string}`;
  expectedEnrollmentGeneration: number;
}

export interface RuntimeLaunchProvenance {
  hostId: string;
  hostEnrollmentId: string; // internal/audit only
  launcher: 'mosaic';
  runtime: 'claude';
  launchId: string;
  launchedAt: string;
  launcherArtifactDigest: `sha256:${string}`;
  launcherVersion: string;
  runtimeArtifactDigest: `sha256:${string}`;
  runtimeVersion: string;
  adapterVersion: string;
  publicContractDigest: `sha256:${string}`;
  contractGeneration: number;
}

export interface RuntimeSessionCreateResult {
  session: RuntimeSession;
  disposition: 'created' | 'replayed';
  resumeCursor?: string;
  provenance: RuntimeLaunchProvenance;
}

export interface BrowserRuntimeTargetPresentationV1 {
  hostLabel: string;
  workspaceLabel: string;
  seatLabel: string;
  personaLabel: string;
  launcher: 'mosaic';
  runtime: 'claude';
  launcherVersion: string;
  runtimeVersion: string;
  adapterVersion: string;
  policyId: string;
  authReadiness: 'ready' | 'not-ready' | 'unknown';
  connectionState: 'connected' | 'disconnected' | 'reconnecting';
}

export interface BrowserAgentHostReadinessV1 {
  ready: boolean;
  target: BrowserRuntimeTargetPresentationV1;
  reasons: SafeReadinessCode[];
}

export interface BrowserRuntimeSessionStartResultV1 {
  durableSessionId: string;
  state: RuntimeSessionState;
  disposition: 'created' | 'replayed';
  target: BrowserRuntimeTargetPresentationV1;
}

The controller maps browser input to a fully server-resolved request. Before spawn, the host requires the exact equality/validation checks selected by the final D2/W-F contract among request, enrollment, policy, and resolved-launch inputs. The browser serializer emits only BrowserRuntimeSessionStartResultV1; it cannot serialize enrollment ID, operation/launch/native IDs, artifact/private/public contract digests, paths, or internal provenance.

Expand RuntimeSessionState additively to:

export type RuntimeSessionState =
  | 'enrolling'
  | 'ready'
  | 'starting'
  | 'running'
  | 'waiting-for-input'
  | 'waiting-for-approval'
  | 'stopping'
  | 'disconnected'
  | 'reconnecting'
  | 'active' // legacy provider mapping
  | 'idle' // legacy provider mapping
  | 'stopped'
  | 'failed';

Hermes, Matrix, and tmux implement createSession() but do not advertise session.create; they throw capability_unsupported before transport. Existing state mappings remain unchanged.

4.3 Reservation, idempotency, and create reconciliation

Gateway generates an opaque createOperationId. Hash input is RFC 8785/JCS canonical JSON with named fields—never delimiter concatenation:

{
  "operation": "session.create",
  "ownerId": "...",
  "tenantId": "...",
  "durableSessionId": "...",
  "idempotencyKey": "...",
  "hostEnrollmentId": "...",
  "enrollmentGeneration": 1,
  "privateDescriptorCommitment": "hmac-sha256:...",
  "publicContractDigest": "sha256:...",
  "launchPolicyDigest": "sha256:..."
}

Two identities are enforced:

  1. Active-launch identity is exactly owner/tenant/durable session; it excludes idempotency key, enrollment generation, and policy. Only one non-terminal runtime may exist for that stable conversation, including during credential rotation.
  2. Request identity additionally binds idempotency key, enrollment ID/generation, and launch-policy digest; an identical retry returns the same operation/result, while changed semantics return conflict until the prior runtime is explicitly terminal/recovered.

Use these exact existing-column encodings:

  • reserved runtimeSessionId: pending:v1:<createOperationId-uuid>;
  • definitive failed create: failed:v1:<createOperationId-uuid>;
  • create outbox idempotency key: create:v1:<64-lowercase-hex-request-digest>;
  • stop outbox idempotency key: stop:v1:<stopOperationId-uuid>;
  • providerId: runtime.agent-host.

RuntimeCreateOperationRecordV1 in outbox content contains only operation ID, request digest, expected private/public contract and policy digests, enrollment generation, accepted/dispatched/result timestamps, and disposition reserved | dispatched | created | failed | indeterminate; repository updates content and contentDigest together. Existing outbox status means: pending = durable reservation accepted but not claimed; processing = host may have accepted the effect and status must be reconciled; delivered = definitive success or failure recorded in content. No prompt, credential, workspace path, env, or raw provenance is stored.

Add exact repository operations reserveCreate(), activateCreate(), failCreate(), reserveStop(), completeStop(), and findRuntimeOperation(). reserveCreate() is one DB transaction: lock/select the stable session ID; insert or CAS it to the pending reference only when absent, failed:v1:*, or bound to a runtime with a matching delivered provider.stop; and insert the unique create outbox. Active or indeterminate bindings conflict. activateCreate() changes only the matching pending reference to the returned runtime ID and completes that outbox. failCreate() changes only that pending reference to failed:v1:* and records a delivered definitive failure. Existing create() must never replace a binding.

The host journal independently locks owner/tenant/durable session. Lookup, active check, spawn, journal commit, and response occur under a private single-daemon lock. Journal and parent directory are owner-only, no-follow regular files with secure ancestor checks, atomic rename, file-and-directory fsync, and a single-instance lock. It stores operation/request/private/public contract/policy digests, safe provenance, child handle, and disposition—never prompts, output, env, or credentials.

Identical retries return the same IDs/provenance as replayed; changed semantics conflict. A post-spawn lost response stays processing/indeterminate; Gateway calls only session.lookup with the same operation ID. If spawn succeeds before Gateway CAS, pending row + processing outbox + host journal prevent another launch. Task 0 statically certifies this use of current columns/indexes; Task 5 proves repository transitions with fakes, and Task 9 must run the real repository transaction test only through the KBN-safe path.

4.4 Typed host protocol and readiness

Every envelope is versioned and schema-validated before dispatch:

export type AgentHostOperation =
  | 'host.readiness'
  | 'session.create'
  | 'session.lookup'
  | 'session.send'
  | 'session.stream'
  | 'session.replay'
  | 'session.stop'
  | 'session.stopStatus';

export type AgentHostOperationPayloadV1 =
  | { operation: 'host.readiness' }
  | ({ operation: 'session.create' } & RuntimeSessionCreateRequest)
  | { operation: 'session.lookup'; createOperationId: string; durableSessionId: string }
  | {
      operation: 'session.send';
      runtimeSessionId: string;
      turnId: string;
      idempotencyKey: string;
      content: string; // UTF-8, 1..65_536 bytes
    }
  | { operation: 'session.stream'; runtimeSessionId: string; afterCursor?: string }
  | { operation: 'session.replay'; runtimeSessionId: string; afterCursor: string }
  | {
      operation: 'session.stop';
      runtimeSessionId: string;
      stopOperationId: string;
      actionDigest: `sha256:${string}`;
    }
  | { operation: 'session.stopStatus'; runtimeSessionId: string; stopOperationId: string };

export interface SafeRuntimeError {
  code: RuntimeErrorCode;
  boundary: RuntimeFailureBoundary;
  message: string; // fixed code-mapped text, max 256 UTF-8 bytes
  retryable: boolean;
}

type AgentHostEnvelopeV1 =
  | {
      type: 'hello';
      protocolVersion: 1;
      credentialId: string;
      generation: number;
      hostId: string;
      privateDescriptorCommitment: `hmac-sha256:${string}`;
      publicContractDigest: `sha256:${string}`;
      launchPolicyDigest: `sha256:${string}`;
    }
  | {
      type: 'command';
      protocolVersion: 1;
      commandId: string;
      connectionEpoch: string;
      payload: AgentHostOperationPayloadV1;
    }
  | {
      type: 'result';
      protocolVersion: 1;
      commandId: string;
      connectionEpoch: string;
      ok: boolean;
      payload?: AgentHostOperationResultV1;
      error?: SafeRuntimeError;
    }
  | { type: 'event'; protocolVersion: 1; connectionEpoch: string; event: MosaicRuntimeEventV1 };

export type AgentHostOperationResultV1 =
  | { operation: 'host.readiness'; readiness: HostReadinessV1 }
  | { operation: 'session.create'; result: RuntimeSessionCreateResult }
  | {
      operation: 'session.lookup';
      disposition: 'created' | 'failed' | 'indeterminate' | 'not_found';
      result?: RuntimeSessionCreateResult;
    }
  | { operation: 'session.send'; accepted: true; turnId: string }
  | { operation: 'session.stream' | 'session.replay'; accepted: true; afterCursor?: string }
  | { operation: 'session.stop'; disposition: 'stopped' | 'replayed' | 'indeterminate' }
  | {
      operation: 'session.stopStatus';
      disposition: 'stopped' | 'failed' | 'indeterminate' | 'not_found';
    };

Successful result payload must validate as exactly one AgentHostOperationResultV1 variant matching the correlated command. Failed result has no payload and requires SafeRuntimeError.

No arbitrary RPC method exists. DTO validators cap every identifier at 128 safe characters, cursor at 512 bytes, envelope at 1 MiB, and content as shown; unknown fields are rejected. WSS and Gateway session auth are mandatory; credential is in Socket.IO auth, never query. Heartbeat is 20 seconds, unavailable threshold 60 seconds, reconnect is capped exponential backoff with jitter, and old epochs cannot receive commands or publish accepted events.

Internal host.readiness returns only approved labels/versions plus { ready, hostLabel, workspaceLabel, seatLabel, personaLabel, authReadiness, enrollmentGeneration, launcherVersion, runtimeVersion, adapterVersion, policyId, reasons }. Gateway adds connection state and serializes only BrowserAgentHostReadinessV1; enrollment generation is internal. It never returns IDs, paths, env, credential identity/content, lease state, or native errors.

4.5 Ordered lifecycle and transcript effects

Legal host-path transitions are:

enrolling → ready → starting → running → waiting-for-input
                         ↘ waiting-for-approval
                         ↘ disconnected → reconnecting → previous live state
                         ↘ stopping → stopped
                         ↘ failed

Do not add an arbitrary metadata bag. Define the event as a validated base plus a closed payload union:

interface MosaicRuntimeEventBaseV1 {
  protocolVersion: 1;
  durableSessionId: string;
  runtimeSessionId: string;
  providerId: 'runtime.agent-host';
  hostId: string;
  runtime: 'claude';
  correlationId: string;
  turnId: string;
  messageId?: string;
  sequence: number; // positive safe integer, process scoped
  cursor: string;
  timestamp: string; // validated UTC RFC 3339
  eventDigest: `sha256:${string}`;
}

type MosaicRuntimeEventPayloadV1 =
  | { type: 'session.state'; state: RuntimeSessionState }
  | { type: 'message.delta'; text: string }
  | { type: 'message.complete'; text: string }
  | { type: 'tool.started'; toolLabel: SafeToolLabel; callId: string }
  | { type: 'tool.completed'; callId: string; status: 'succeeded' | 'failed' | 'cancelled' }
  | { type: 'approval.required'; approvalId: string; toolLabel: SafeToolLabel }
  | { type: 'runtime.error'; error: SafeRuntimeError };

type MosaicRuntimeEventV1 = MosaicRuntimeEventBaseV1 & MosaicRuntimeEventPayloadV1;

SafeToolLabel is the frozen policy allowlist; unknown labels become fixed other, never raw native names. Text events are individually capped at 64 KiB after streaming redaction; tool args/output are forbidden.

eventDigest is SHA-256 over RFC 8785/JCS canonical redacted event with eventDigest omitted. Host computes after validation/redaction and before ring append. Gateway validates schema, recomputes digest, and timing-safely compares before any sequence handling, persistence, audit, or browser emission.

Host sequence starts at 1 per runtime process. Gateway tracks expectedNext plus a map keyed by future sequence. For a lower accepted sequence, equal digest is duplicate/drop and differing digest is protocol_mismatch/disconnect. For a sequence already buffered in the future map, equal digest is duplicate/drop; differing digest immediately clears buffer and fails/disconnects. A new future sequence enters the 64-event/2-second buffer and triggers replay from expectedNext; no buffered event is persisted. Unresolved gaps fail visibly. message.complete and browser agent:end are withheld until every prior turn sequence is accepted.

Assistant persistence uses a UUIDv5 message ID derived from (runtimeSessionId, turnId, 'assistant.complete') and INSERT ... ON CONFLICT DO NOTHING; the event's stable message ID is stored in bounded message metadata. Duplicate completion or Gateway retry cannot create a second assistant message. Gateway restart with no durable accepted cursor marks the in-flight turn indeterminate and refuses automatic completion persistence or resume.

4.6 Error, replay, and host-daemon restart

Add failure boundary gateway | transport | host | launcher | runtime | authentication and codes host_unavailable, protocol_mismatch, authentication_required, launch_failed, cursor_expired, indeterminate, plus existing normalized codes. Safe errors contain code, stable message, boundary, and retryability only.

Host replay ring is bounded to 4,096 events and 8 MiB, whichever comes first; completed replay expires after 30 minutes. Resume is strictly after cursor. Missing/expired cursor reloads persisted transcript and warns without create. Browser disconnect leaves provider stream alive.

Host-daemon restart adoption is excluded. On restart, journal entries lacking a live registered child handle become indeterminate; readiness is false for them and no command except status/stop reconciliation is accepted. The host must not infer/adopt a process by PID, tmux, broker state, or process scanning. Operator cleanup uses the Fred-approved exact launcher/host procedure.

Gateway implements RuntimeOperationRecoveryService implements OnApplicationBootstrap. Repository method listRecoverableRuntimeOperations(['pending', 'processing']) enumerates only typed provider.create/provider.stop records for the manifest-bound owner/tenant:

  • pending create: expose pending; never dispatch/launch until the same browser/CLI idempotency retry;
  • processing create: call session.lookup only, then CAS to active/failed or remain indeterminate;
  • pending accepted stop: call session.stopStatus; if not_found and within deadline, atomically claim and dispatch that same accepted operation;
  • processing stop: call status first, then same-operation recovery from Section 4.7;
  • unavailable/revoked host: retain indeterminate state and surface safe recovery status; never select another host;
  • in-flight transcript without durable accepted cursor: mark indeterminate and never persist/auto-resume completion.

Startup recovery completes before host readiness/start routes report ready. Tests reconstruct service/repository, enumerate mixed owner/kind/status records, and assert only the transitions above; no auto-create occurs.

4.7 Retry-safe exact stop

Extend existing InteractionController routes; do not invent a second base path:

GET  /api/interaction/:agentName/host-readiness
POST /api/interaction/:agentName/sessions/:sessionId/start
  body { idempotencyKey }
POST /api/interaction/:agentName/sessions/:sessionId/stop-approval
  body {}
  -> { stopOperationId, approvalRef, expiresAt }
POST /api/interaction/:agentName/sessions/:sessionId/stop
  body { stopOperationId, approvalRef }
  -> { status: stopped | replayed | failed | indeterminate, stopOperationId, error?: SafeRuntimeError }
GET  /api/interaction/:agentName/sessions/:sessionId/stop-operations/:stopOperationId
  -> { status: pending | stopped | failed | indeterminate, stopOperationId, error?: SafeRuntimeError }

Existing apps/gateway/src/agent/interaction.controller.ts owns all five routes; Task 3 has no HTTP controller. :agentName must equal deployment MOSAIC_AGENT_NAME; owner/tenant come from scopeFromUser() and byte-equal manifest owner/tenant. Add operationContext(user) for these new routes: channel is fixed interaction-http, and correlation is server-generated UUID (createOperationId or stopOperationId). It neither requires nor trusts client X-Correlation-Id. Preserve current header requirement only for untouched legacy interaction routes. Browser supplies no provider, host, runtime, action correlation, or policy.

Redis and PostgreSQL cannot share a transaction, so stop uses durable acceptance before destructive consumption:

  1. Approval issuance generates stopOperationId and creates the existing Redis exact-action approval bound to actor, tenant, channel, provider, runtime session, and that operation ID.
  2. Stop POST calls a new non-destructive verifyRuntimeTerminationApproval(); invalid/expired/mismatched approval writes nothing.
  3. One DB transaction inserts unique provider.stop outbox stop:v1:<operationId> with an authorizationAccepted record containing only approval digest, action digest, accepted/expires timestamps, and server-derived IDs. This commit is the one durable authorization acceptance.
  4. An atomic outbox claim changes pending → processing. The claimant atomically GETDELs (or Lua compare-and-deletes) the exact Redis approval, then dispatches. Duplicate requests cannot claim twice.
  5. Crash before DB commit leaves approval usable and no authorization record. Crash after DB commit is safe: the exact durable acceptance authorizes only this operation. If the token is gone or expires later, recovery first queries session.stopStatus; not_found permits dispatch of this same accepted operation before a two-minute operation deadline. It cannot authorize another operation or target.
  6. Host journals disposition before reply; Gateway updates outbox content/digest and marks delivered. processing always reconciles status before any resend. Definitive timeout records delivered failed; a new action requires a newly issued approval.

Invalid/expired/mismatched approval is HTTP 403 and creates no operation. Once durably accepted, POST/status returns HTTP 200 with the operation status; definitive host/timeout failure is failed with only SafeRuntimeError. Tests inject failure before/after verify, DB commit, outbox claim, Redis GETDEL, host accept, graceful signal, force kill, host journal commit, and Gateway completion. Host binds stop to its registered child handle. Already stopped is success only for the same runtime/operation target.

4.8 Enrollment revocation and command authorization

export interface AgentHostEnrollmentManifestV1 {
  protocolVersion: 1;
  credentialId: string;
  generation: number; // integer 1..2_147_483_647
  hostId: string;
  ownerId: string;
  tenantId: string;
  expectedPrivateDescriptorCommitment: `hmac-sha256:${string}`;
  expectedPublicContractDigest: `sha256:${string}`;
  allowedLaunchPolicyDigest: `sha256:${string}`;
  issuedAt: string;
  expiresAt?: string;
  revoked: boolean;
}

Token digest is held by Gateway secret config, not the non-secret manifest. Gateway validates schema, integer range, UTC timestamps, manifest/digest equality, and credential at connect and immediately before every command and event acceptance. Generation ordering is numeric and strictly greater; equal/lower duplicate connection is refused.

Operator installs host and Gateway copies of generation N+1 atomically, then sends Gateway SIGHUP. AgentHostConfigService.reload() parses into a new immutable config epoch before swap; on success it removes old generation from registry/readiness first, rejects subsequent old-epoch events, disconnects its socket, and then allows N+1 registration. Parse failure retains prior config and reports safe not-ready reason. Expiry/revocation timer invokes the same path. Tests call reload() directly and via the signal adapter, including revocation during create/stream and events racing the swap. No product UI or DB migration is required.

4.9 Streaming redaction and provenance

Extract a shared bounded streaming redactor in packages/log before host event work. It retains at most 4 KiB suffix across chunks and recognizes split private keys/tokens/OAuth-like values/emails plus all absolute path classes: POSIX (including workspace/executable/working/config/home paths), Windows drive paths, UNC paths, file:// URIs, and tilde-home forms. It emits fixed [REDACTED_PATH]/overflow markers and never releases a partial candidate before delimiter or bounded decision. Approved labels/versions travel in typed fields, never by exempting arbitrary text paths. Host applies before ring/journal/transport; Gateway applies again before audit/transcript/browser. Hostile NDJSON, stdout, stderr, errors, provenance, and split boundaries use the same component.

Extend RuntimeAuditOperation with session.create and typed outcomes/error codes. Audit projection contains safe host/launch IDs, launcher/runtime/adapter versions, enrollment generation, contract generation, and only D2/D12-approved path-free provenance (which may include artifact-content digests if W-F binds them). Do not hash path strings: predictable paths are dictionary-recoverable. No raw paths, arbitrary metadata, prompts, output, approval references, or credentials enter audit.

Host performs the final D2/W-F-bound launch-input checks and records only the corresponding D12-approved boolean/digest evidence in controlled QA. Fake-process tests may assert literal fixture paths; live QA never emits an absolute home/config/executable path. Browser receives only the explicit DTO in Section 4.2.

4.10 F-V3 boundary

Agent Host code has no lease-broker imports, socket/path constants, daemon-file reads, VERIFIED/TTL parsing, or status UI. Calling Fred's sealed authoritative launcher is allowed even if that launcher internally contacts the broker. The bridge treats launch success/failure as the only boundary result and does not claim system-wide absence of broker interaction.

5. Implementation tasks

5.0 Orchestrator task manifest and delivery loop

Task 0 replaces $ISSUE_NUMBER, $IMPLEMENTATION_BASE_SHA, and branch placeholders, then Fred/orchestrator writes these rows under a new WebUI Fleet Bridge section in root docs/TASKS.md. The table uses the superset schema from ~/.config/mosaic/templates/docs/TASKS.md.template while retaining every repo-required field. Workers never edit rows or model assignments.

id status description issue repo branch depends_on blocks agent started_at completed_at estimate used notes
web-fleet-m1-00 blocked Fred contract, storage feasibility, plan PR, immutable base $ISSUE_NUMBER mosaicstack/stack docs/webui-fleet-claude-bridge-plan web-fleet-m1-01 opus 18K Fred decision; Velma docs
web-fleet-m1-01 not-started Claude/Mosaic descendant protocol certificate $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-00 web-fleet-m1-02 codex 20K Velma; Fred reviews launcher
web-fleet-m1-02 not-started shared provider/event/audit/redaction contracts $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-01 web-fleet-m1-03 codex 24K Velma; haiku review
web-fleet-m1-03 not-started host enrollment/revocation/transport $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-02 web-fleet-m1-04 codex 32K Velma; sonnet security review
web-fleet-m1-04 not-started host daemon/journal/replay/stop $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-03 web-fleet-m1-05 codex 35K Velma; sonnet security review
web-fleet-m1-05 not-started durable create/stop operations $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-04 web-fleet-m1-06 codex 35K Velma; sonnet security review
web-fleet-m1-06 not-started ordered ChatGateway bridge $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-05 web-fleet-m1-07 codex 28K Velma; haiku review
web-fleet-m1-07 not-started minimal Web runtime UI $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-06 web-fleet-m1-08 codex 24K Velma; haiku + accessibility review
web-fleet-m1-08 not-started no-service adversarial integration $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-07 web-fleet-m1-09 codex 28K Velma; sonnet security review
web-fleet-m1-09 blocked certified isolated live E2E/docs $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-08 web-fleet-m1-10 sonnet 24K Fred/Scooby startup; Jason login
web-fleet-m1-10 not-started exact-head review/PR/merge/post-merge proof $ISSUE_NUMBER mosaicstack/stack feat/$ISSUE_NUMBER-webui-fleet-claude-bridge web-fleet-m1-09 sonnet 20K coordinator + independent reviewers

After Task 0, use:

: "${ISSUE_NUMBER:?Task 0 must export the recorded provider issue number}"
: "${FRED_APPROVED_REF:?Task 0 must export the immutable approved ref}"
: "${IMPLEMENTATION_BASE_SHA:?Task 0 must export Fred's certified full SHA}"
: "${CERTIFIED_WEB_URL:?Task 0 must export the KBN-safe Web URL}"
: "${CERTIFIED_DATABASE_URL_LOADER:?Task 0 must export the approved private DB URL loader path}"
: "${CERTIFIED_STARTUP_SCRIPT:?Task 0 must export the exact approved startup script path}"
: "${CERTIFIED_POST_MERGE_SMOKE_SCRIPT:?Task 0 must export the exact smoke script path}"
test -x "$CERTIFIED_DATABASE_URL_LOADER"
CERTIFIED_DATABASE_URL="$("$CERTIFIED_DATABASE_URL_LOADER")"
: "${CERTIFIED_DATABASE_URL:?DB URL loader returned an empty value}"
IMPLEMENTATION_BRANCH="feat/${ISSUE_NUMBER}-webui-fleet-claude-bridge"
export ISSUE_NUMBER FRED_APPROVED_REF IMPLEMENTATION_BASE_SHA IMPLEMENTATION_BRANCH
export CERTIFIED_WEB_URL CERTIFIED_DATABASE_URL CERTIFIED_STARTUP_SCRIPT
export CERTIFIED_POST_MERGE_SMOKE_SCRIPT

Task 0 records the non-secret values/loader path in the reviewed scratchpad; every fresh implementation session repeats this block. The loader output exists only in that process environment and must never be echoed, placed in argv, committed, or included in evidence.

Mandatory loop before every Task 19 logical commit: run focused GREEN; run git diff --check; freeze git diff HEAD; dispatch the task's independent reviewer (never the author) with requesting-code-review; write docs/reports/code-review/${ISSUE_NUMBER}-task-NN.md; remediate every blocker; rerun tests; re-review the new exact diff. Then commit only the exact task paths, run ci-queue-wait.sh --purpose push -B "$IMPLEMENTATION_BRANCH", and push immediately. A non-ready guard blocks. Every pushed head runs PR CI; every source-head change invalidates the prior review.

Orchestrator-only tracker writes use the same mechanics: current delivery branch, full 14-column row update (blocks, timestamps, estimate/used included), Prettier/diff-check, independent haiku review, separate tracking commit, push queue guard, push, and terminal CI. A tracker commit changes the PR head and therefore requires exact-head review again; it is never pushed directly to next.

Task 1's first push creates the implementation PR with pr-create.sh -i "$ISSUE_NUMBER" -B next -H "$IMPLEMENTATION_BRANCH" (not draft, so no undocumented ready-state transition is needed). Subsequent task pushes update it. Record IMPLEMENTATION_PR_NUMBER; review reports are committed with their task. Task 10 takes the exact reviewed head through final CI/merge/closure.

Task 0: Fred contract gate, storage certificate, plan PR, tracking, and immutable base

Files:

  • Modify after review: docs/plans/2026-08-09-webui-fleet-claude-bridge.md
  • Modify: docs/scratchpads/webui-fleet-bridge-plan.md
  • Modify with orchestrator ownership: docs/PRD.md
  • Modify with orchestrator ownership: docs/TASKS.md
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-00-plan.md

Fred/orchestrator—not a worker—writes the Section 5.0 rows to docs/TASKS.md in the separately reviewed tracking commit defined below.

Authorized preliminary scope: Fred authorized an initial docs-only decision PR on next in comms/20260809T094952Z__from-fred__ec0e85.md, with the lane/descriptor corrections in comms/20260809T095437Z__from-fred__4ee79d.md. That PR contains only this plan and its discovery scratchpad. Opening it before the complete D1D15 packet is the sole exception to the Task 0 close gates; it does not close Task 0, authorize Task 1, create an implementation issue, or permit PRD/tracker/runtime changes.

Step 0: Open the exact docs-only decision PR on next

Create docs/webui-fleet-claude-bridge-plan from exact origin/next, add only the two decision documents, run Prettier/diff/scope/credential checks, obtain independent exact-diff review, and commit. Immediately before push, re-fetch next and require it still equals the recorded base. Run the required push queue guard; a non-ready result stops. Open the PR against next, then report PLAN_PR_NUMBER and the exact full PLAN_HEAD_SHA to Fred. His full contract set lands as provider review plus git comms on that exact head. Any source-head change invalidates that review.

Step 1: Obtain and record Fred's complete decision packet

Send Sections 14 through git comms. Record every field from Section 2's return table, including FRED_APPROVED_REF, safe Web URL, private DB URL loader path (never its output), and startup/post-merge executable paths. Task 0 validates the loader and performs the capture/export block in Section 5.0 without echoing the URL. Replace every [FRED-GATE] with the decision and decision-message commit/path. No marker may remain.

Step 2: Certify no-migration feasibility and source baseline

Fred/Scooby must explicitly accept the Section 4.3 encodings against the verified primary key, outbox unique index, and three existing status values. Record accepted repository transitions and KBN-safe real transaction test path in the scratchpad. This is static design review only; do not boot DB or write a migration.

git fetch origin main next
git rev-parse "$FRED_APPROVED_REF"

Expected: one full 40-character IMPLEMENTATION_BASE_SHA, not either discovery SHA unless Fred explicitly certifies reconciliation. If storage or source baseline is not certified, stop.

Step 3: Create and record the provider issue

MOSAIC_GIT_IDENTITY=velma GITEA_LOGIN=mosaicstack-velma \
  ~/.config/mosaic/tools/git/issue-create.sh \
  -t "WebUI: native Claude Agent Host vertical slice" \
  -b "Deliver the approved docs/plans/2026-08-09-webui-fleet-claude-bridge.md contract."

Record numeric ISSUE_NUMBER, verify creator velma, replace all issue/branch placeholders, and have Fred/orchestrator create every canonical task row from Section 5.0 with exact dependencies/models/estimates.

Step 4: Add normative PRD requirements

Add WEB-FLEET-M1-* requirements and Section 7 acceptance evidence. No ASSUMPTION, placeholder, or [FRED-GATE] may remain at Task 0 close.

Step 5: Independent plan review before commit

Freeze git diff HEAD; an independent architecture reviewer plus Fred reviews the exact plan/PRD/scratchpad delta using requesting-code-review. Separately, a haiku reviewer checks the complete 14-column docs/TASKS.md delta. Write docs/reports/code-review/${ISSUE_NUMBER}-task-00-plan.md, remediate blockers, rerun Prettier and git diff --check, and re-review any changed delta.

Step 6: Commit, push, and merge the docs-only plan PR

git add docs/PRD.md docs/plans/2026-08-09-webui-fleet-claude-bridge.md \
  docs/scratchpads/webui-fleet-bridge-plan.md \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-00-plan.md"
git commit -m "docs(#${ISSUE_NUMBER}): plan native Claude WebUI bridge"
# Fred/orchestrator executes this reviewed tracker-only commit:
git add docs/TASKS.md
git commit -m "chore(#${ISSUE_NUMBER}): track native Claude WebUI bridge"
PLAN_BRANCH="docs/webui-fleet-claude-bridge-plan"
test "$(git branch --show-current)" = "$PLAN_BRANCH"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B "$PLAN_BRANCH"
git push origin "$PLAN_BRANCH"
# The existing Step 0 PR is updated; do not create a second plan PR.

Record PLAN_PR_NUMBER, require terminal-green pr-ci-wait.sh -n "$PLAN_PR_NUMBER", coordinator go-ahead, full PLAN_HEAD_SHA, then:

~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge \
  -B "$PLAN_BRANCH" -R mosaicstack/stack --sha "$PLAN_HEAD_SHA"
~/.config/mosaic/tools/git/pr-merge.sh \
  -n "$PLAN_PR_NUMBER" -m squash --expect-head "$PLAN_HEAD_SHA"
~/.config/mosaic/tools/git/pr-ci-wait.sh -n "$PLAN_PR_NUMBER"

Do not close the implementation issue.

Step 7: Create implementation branch from the certified immutable base

After the plan merge and any Fred-owned baseline work, re-record Fred's final full SHA if it changed, then:

git fetch origin
git switch --detach "$IMPLEMENTATION_BASE_SHA"
git switch -c "$IMPLEMENTATION_BRANCH"
test "$(git rev-parse HEAD)" = "$IMPLEMENTATION_BASE_SHA"

Expected: clean implementation branch and all task rows unblocked only as dependencies permit. Task 1 is still forbidden if any gate/value/evidence is missing.

Task 1: Certify Claudes machine protocol through mosaic claude

Files:

  • Create: packages/mosaic/src/agent-host/claude-stream-adapter.ts
  • Create: packages/mosaic/src/agent-host/claude-stream-adapter.spec.ts
  • Create: packages/mosaic/src/agent-host/resolved-seat-launch-descriptor.ts
  • Create: packages/mosaic/src/agent-host/resolved-seat-launch-descriptor.spec.ts
  • Create: packages/mosaic/src/agent-host/fixtures/claude-stream-v1.ndjson
  • Create: packages/mosaic/src/agent-host/fixtures/claude-stream-malformed.ndjson
  • Inspect/certify only (Fred-owned implementation): packages/mosaic/src/commands/launch.ts
  • Modify: packages/mosaic/src/commands/launch.spec.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-01.md

Step 1: Write RED exact-boundary tests

Generate the host-adapter certificate from Fred's final D2/W-F contract, not from the candidate interface in Section 4.1. It must prove the approved launcher chain, generated-home identity, non-YOLO flags, and no ambient path/home override. Commitment, artifact-byte, descendant-path, owner/mode, symlink, and hardlink rejection cases are required only to the extent W-F binds those mechanisms. Independently reject shell strings, direct Agent Host claude, mosaic yolo claude, unknown flags, unapproved policy, and mismatched native session ID. A failure in Fred-owned descendant behavior blocks and returns to Fred; do not patch launch.ts in this task.

expect(spawnRequest.command).toBe(resolved.nodePath);
expect(spawnRequest.args.slice(0, 2)).toEqual([resolved.mosaicCliPath, 'claude']);
expect(spawnRequest.args).toContain('--input-format');
expect(spawnRequest.args).toContain('stream-json');
expect(spawnRequest.args).not.toContain('yolo');

Step 2: Run RED

pnpm --filter @mosaicstack/mosaic exec vitest run \
  src/agent-host/claude-stream-adapter.spec.ts src/commands/launch.spec.ts

Expected: FAIL because the adapter does not exist.

Step 3: Implement the minimal adapter

  • Use spawn, never a shell.
  • Pipe stdin/stdout/stderr; cap a single frame and buffered stderr.
  • Runtime-validate NDJSON before mapping it.
  • Map only allowlisted fields; never forward raw native objects.
  • Keep the process open for stream-json input until explicit stop.
  • Use --session-id <gateway UUID> and assert Claude's initialization frame agrees.
  • Pin the M1 permission policy to Fred/Jason-approved non-YOLO behavior.
  • Certify a non-secret native auth-readiness command through Mosaic; if the pinned Claude version has no safe machine-readable status, readiness must be unknown until launch rather than inspecting OAuth files.

Step 4: Run GREEN and negative mutation controls

Delete --output-format stream-json in a temporary test subject and confirm the compatibility test fails, then restore it.

Step 5: Offline pinned-version certificate

Inside the implementation Distrobox, record only the Fred-approved binaries' --version/--help output and run certified NDJSON fixtures through the adapter. Do not log in, read OAuth files, or send a live prompt in Tasks 18. Any mismatch from the pinned version/flags is a stop condition and returns to Fred. Native login and the only live prompt occur in Task 9.

Step 6: Commit

git add packages/mosaic/src/agent-host/claude-stream-adapter.ts \
  packages/mosaic/src/agent-host/claude-stream-adapter.spec.ts \
  packages/mosaic/src/agent-host/resolved-seat-launch-descriptor.ts \
  packages/mosaic/src/agent-host/resolved-seat-launch-descriptor.spec.ts \
  packages/mosaic/src/agent-host/fixtures/claude-stream-v1.ndjson \
  packages/mosaic/src/agent-host/fixtures/claude-stream-malformed.ndjson \
  packages/mosaic/src/commands/launch.spec.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-01.md"
git commit -m "test(#${ISSUE_NUMBER}): certify Claude stream protocol"

Task 2: Add normalized create, host event, audit, and redaction contracts

Files:

  • Modify: packages/types/src/agent/agent-runtime-provider.ts
  • Modify: packages/types/src/agent/agent-runtime-provider.spec.ts
  • Create: packages/types/src/agent/agent-host-protocol.dto.ts
  • Create: packages/types/src/agent/agent-host-protocol.dto.spec.ts
  • Modify: packages/types/src/agent/index.ts
  • Modify: packages/agent/src/hermes-runtime-provider.ts
  • Modify: packages/agent/src/hermes-runtime-provider.test.ts
  • Modify: packages/agent/src/matrix-native-runtime-provider.ts
  • Modify: packages/agent/src/matrix-native-runtime-provider.test.ts
  • Modify: packages/agent/src/tmux-fleet-runtime-provider.ts
  • Modify: packages/agent/src/tmux-fleet-runtime-provider.test.ts
  • Modify: packages/agent/src/runtime-provider-parity.test.ts
  • Modify: packages/agent/src/runtime-provider-registry.test.ts
  • Modify: packages/log/src/runtime-audit.ts
  • Modify: packages/log/src/runtime-audit.test.ts
  • Create: packages/log/src/streaming-redactor.ts
  • Create: packages/log/src/streaming-redactor.spec.ts
  • Modify: packages/log/src/index.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-02.md

Step 1: Write RED contract tests

Cover every field and descriptor-provenance mechanism retained by the final D2/W-F contract, plus positive integer sequence, UTC timestamps, exact state/error unions, legal transitions, safe error boundaries, field limits, unsupported create denial, protocol rejection, JCS event-digest recomputation, browser DTO exclusion, typed safe audit provenance, all split path classes/secrets, 4-KiB carry cap, and fail-closed overflow.

Step 2: Run RED

pnpm --filter @mosaicstack/types test -- agent-runtime-provider agent-host-protocol
pnpm --filter @mosaicstack/agent test -- runtime-provider
pnpm --filter @mosaicstack/log test -- runtime-audit streaming-redactor

Expected: FAIL on missing capability, DTOs, and provider methods.

Step 3: Implement the minimal shared contract

After D2 binding, define the exact versioned hello/command/result/event DTOs from Section 4 with every rejected candidate descriptor slot removed. Every external frame enters as unknown and is validated before use. Do not add arbitrary metadata maps. Extract the shared bounded streaming redactor and extend runtime audit with session.create, normalized error codes, and the D2/D12-approved allowlisted projection from Section 4.9.

Step 4: Make legacy providers fail closed

Legacy providers return capability_unsupported from createSession and do not call their transports. Preserve all existing advertised capabilities.

Step 5: Run GREEN, typecheck, lint

pnpm --filter @mosaicstack/types test
pnpm --filter @mosaicstack/agent test
pnpm --filter @mosaicstack/log test
pnpm --filter @mosaicstack/types typecheck
pnpm --filter @mosaicstack/agent typecheck
pnpm --filter @mosaicstack/log typecheck
pnpm --filter @mosaicstack/types lint
pnpm --filter @mosaicstack/agent lint
pnpm --filter @mosaicstack/log lint

Step 6: Commit

git add packages/types/src/agent/agent-runtime-provider.ts \
  packages/types/src/agent/agent-runtime-provider.spec.ts \
  packages/types/src/agent/agent-host-protocol.dto.ts \
  packages/types/src/agent/agent-host-protocol.dto.spec.ts packages/types/src/agent/index.ts \
  packages/agent/src/hermes-runtime-provider.ts packages/agent/src/hermes-runtime-provider.test.ts \
  packages/agent/src/matrix-native-runtime-provider.ts packages/agent/src/matrix-native-runtime-provider.test.ts \
  packages/agent/src/tmux-fleet-runtime-provider.ts packages/agent/src/tmux-fleet-runtime-provider.test.ts \
  packages/agent/src/runtime-provider-parity.test.ts packages/agent/src/runtime-provider-registry.test.ts \
  packages/log/src/runtime-audit.ts packages/log/src/runtime-audit.test.ts \
  packages/log/src/streaming-redactor.ts packages/log/src/streaming-redactor.spec.ts packages/log/src/index.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-02.md"
git commit -m "feat(#${ISSUE_NUMBER}): define Agent Host runtime contracts"

Task 3: Implement one-host authentication, revocation, and provider transport

Files:

  • Create: apps/gateway/src/agent-host/agent-host.config.ts
  • Create: apps/gateway/src/agent-host/agent-host.config.spec.ts
  • Create: apps/gateway/src/agent-host/agent-host.module.ts
  • Create: apps/gateway/src/agent-host/agent-host.gateway.ts
  • Create: apps/gateway/src/agent-host/agent-host.gateway.spec.ts
  • Create: apps/gateway/src/agent-host/agent-host-auth.service.ts
  • Create: apps/gateway/src/agent-host/agent-host-auth.service.spec.ts
  • Create: apps/gateway/src/agent-host/agent-host-connection.service.ts
  • Create: apps/gateway/src/agent-host/agent-host-connection.service.spec.ts
  • Create: packages/agent/src/mosaic-host-runtime-provider.ts
  • Create: packages/agent/src/mosaic-host-runtime-provider.test.ts
  • Modify: packages/agent/src/index.ts
  • Modify: apps/gateway/src/app.module.ts
  • Modify: apps/gateway/src/agent/agent.module.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-03.md

Step 1: Write RED authentication and scope tests

Prove missing/wrong/expired/revoked credentials disconnect before registration; manifest binding beats hello/browser fields; wrong owner/tenant cannot list or command; equal/lower duplicate generation is refused; a higher generation replaces the old epoch; revocation while connected/during create/during stream removes readiness and rejects commands/events; command envelopes are bounded and typed; and provider registration occurs only after validated config.

Also assert logs/audit never contain credential, authorization header, workspace path, OAuth metadata, or raw manifest/token values.

Step 2: Run RED

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/agent-host/agent-host.config.spec.ts \
  src/agent-host/agent-host.gateway.spec.ts \
  src/agent-host/agent-host-auth.service.spec.ts \
  src/agent-host/agent-host-connection.service.spec.ts
pnpm --filter @mosaicstack/agent test -- mosaic-host-runtime-provider

Expected: FAIL because Agent Host config, namespace, auth, connection registry, and provider do not exist.

Step 3: Implement operator config and outbound registry

  • Dedicated /agent-host namespace; WSS required outside loopback.
  • Parse AgentHostEnrollmentManifestV1 from the Fred-approved operator source and token from secret config.
  • Timing-safe credential validation at connect and before every command.
  • Server-derived host/owner/tenant/policy binding and random connection epoch.
  • Higher-generation replace; equal/lower refuse; force-disconnect revoked/superseded epoch.
  • 20-second heartbeat, 60-second unavailable threshold, bounded correlation map/timeouts.
  • Internal readiness projection exactly as Section 4.4; no HTTP controller and no DB write in this task. Task 5's existing InteractionController owns the sole browser/CLI readiness route.

Step 4: Implement MosaicHostRuntimeProvider

Delegate normalized create/stream/send/terminate through the exact current connection epoch. Implement required list/tree/attach/detach interface methods as fail-closed capability_unsupported methods and do not advertise them in M1. Validate every result/event DTO, enforce scope, reject sequence/epoch violations, and normalize safe errors.

Step 5: Run GREEN and existing security regressions

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/agent-host/agent-host.config.spec.ts \
  src/agent-host/agent-host.gateway.spec.ts \
  src/agent-host/agent-host-auth.service.spec.ts \
  src/agent-host/agent-host-connection.service.spec.ts \
  src/agent/__tests__/runtime-provider-registry.service.test.ts
pnpm --filter @mosaicstack/agent test -- mosaic-host-runtime-provider

Expected: PASS with no live Gateway/Web/PostgreSQL startup.

Step 6: Commit

git add apps/gateway/src/agent-host/agent-host.config.ts \
  apps/gateway/src/agent-host/agent-host.config.spec.ts \
  apps/gateway/src/agent-host/agent-host.module.ts \
  apps/gateway/src/agent-host/agent-host.gateway.ts \
  apps/gateway/src/agent-host/agent-host.gateway.spec.ts \
  apps/gateway/src/agent-host/agent-host-auth.service.ts \
  apps/gateway/src/agent-host/agent-host-auth.service.spec.ts \
  apps/gateway/src/agent-host/agent-host-connection.service.ts \
  apps/gateway/src/agent-host/agent-host-connection.service.spec.ts \
  apps/gateway/src/app.module.ts apps/gateway/src/agent/agent.module.ts \
  packages/agent/src/mosaic-host-runtime-provider.ts \
  packages/agent/src/mosaic-host-runtime-provider.test.ts packages/agent/src/index.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-03.md"
git commit -m "feat(#${ISSUE_NUMBER}): add authenticated Agent Host provider"

Task 4: Implement host daemon, exclusive operation journal, replay, and exact stop

Files:

  • Create: packages/mosaic/src/agent-host/agent-host-client.ts
  • Create: packages/mosaic/src/agent-host/agent-host-client.spec.ts
  • Create: packages/mosaic/src/agent-host/session-registry.ts
  • Create: packages/mosaic/src/agent-host/session-registry.spec.ts
  • Create: packages/mosaic/src/agent-host/operation-journal.ts
  • Create: packages/mosaic/src/agent-host/operation-journal.spec.ts
  • Create: packages/mosaic/src/agent-host/event-ring.ts
  • Create: packages/mosaic/src/agent-host/event-ring.spec.ts
  • Create: packages/mosaic/src/commands/agent-host.ts
  • Create: packages/mosaic/src/commands/agent-host.spec.ts
  • Modify: packages/mosaic/src/cli.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-04.md

Step 1: Write RED process, crash, and idempotency tests

Prove one spawn for concurrent same-key and concurrent distinct-key creates for one stable session; conflict on changed semantics; same-operation lookup after dropped response; exact stop/status replay; dropped responses after graceful signal and force kill; no second daemon lock; refusal of every unsafe/stale launch input defined by final D2/W-F; cursor order/expiry/caps; and restart behavior that marks an unregistered child indeterminate without PID/process scanning.

Inject crashes before spawn, after spawn/before journal commit, after journal commit/before response, and during stop. Every recovery path must use the same operation ID and never infer a target.

Step 2: Run RED

pnpm --filter @mosaicstack/mosaic exec vitest run \
  src/agent-host/agent-host-client.spec.ts \
  src/agent-host/session-registry.spec.ts \
  src/agent-host/operation-journal.spec.ts \
  src/agent-host/event-ring.spec.ts src/commands/agent-host.spec.ts

Expected: FAIL because host components and command do not exist.

Step 3: Implement mosaic agent-host connect

Read only the Fred-approved enrollment credential/manifest and final D2/W-F descriptor contract; validate its bound ownership/generation rules; connect outbound; report the exact safe readiness DTO; send 20-second heartbeats; use capped jittered reconnect; and wait for typed commands. No arbitrary RPC or config path is accepted from Gateway.

Step 4: Implement exclusive launch/session ownership

Under one private single-instance lock, resolve the frozen policy/descriptor, reject a second active launch identity, spawn through Task 1, and durably journal operation/request digests, IDs, versions, child handle, timestamps, and disposition. Use no-follow secure opens, atomic rename, and file/directory fsync. Never journal prompt/output/env/credential content. Apply the shared streaming redactor before ring, journal-safe error, or transport.

Step 5: Implement ordered replay and exact stop

Assign process-scoped sequence before ring/send, retain capped events, and support strict-after-cursor replay. Bind stop operation to the registered child handle. Signal graceful termination, wait a bounded interval, then force-kill that child only. Journal disposition before replying. session.stopStatus and same-operation retry reconcile; never fuzzy-match PID, tmux, broker, process list, or argv.

Step 6: Run GREEN and shell-injection mutation tests

pnpm --filter @mosaicstack/mosaic exec vitest run \
  src/agent-host/agent-host-client.spec.ts \
  src/agent-host/session-registry.spec.ts \
  src/agent-host/operation-journal.spec.ts \
  src/agent-host/event-ring.spec.ts src/commands/agent-host.spec.ts
pnpm --filter @mosaicstack/mosaic typecheck
pnpm --filter @mosaicstack/mosaic lint

Expected: PASS with fake child processes and fake transport only.

Step 7: Commit

git add packages/mosaic/src/agent-host/agent-host-client.ts \
  packages/mosaic/src/agent-host/agent-host-client.spec.ts \
  packages/mosaic/src/agent-host/session-registry.ts \
  packages/mosaic/src/agent-host/session-registry.spec.ts \
  packages/mosaic/src/agent-host/operation-journal.ts \
  packages/mosaic/src/agent-host/operation-journal.spec.ts \
  packages/mosaic/src/agent-host/event-ring.ts \
  packages/mosaic/src/agent-host/event-ring.spec.ts \
  packages/mosaic/src/commands/agent-host.ts packages/mosaic/src/commands/agent-host.spec.ts \
  packages/mosaic/src/cli.ts "docs/reports/code-review/${ISSUE_NUMBER}-task-04.md"
git commit -m "feat(#${ISSUE_NUMBER}): run native Claude sessions on Agent Host"

Task 5: Add durable create/start reservation and retry-safe stop operations

Files:

  • Modify: apps/gateway/src/agent/agent.module.ts
  • Modify: apps/gateway/src/agent/runtime-provider-registry.service.ts
  • Modify: apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts
  • Create: apps/gateway/src/agent/runtime-operation.service.ts
  • Create: apps/gateway/src/agent/runtime-operation.service.test.ts
  • Create: apps/gateway/src/agent/runtime-operation-recovery.service.ts
  • Create: apps/gateway/src/agent/runtime-operation-recovery.service.test.ts
  • Modify: apps/gateway/src/agent/interaction.controller.ts
  • Modify: apps/gateway/src/agent/interaction.controller.test.ts
  • Modify: apps/gateway/src/agent/durable-session.dto.ts
  • Modify: apps/gateway/src/agent/durable-session.service.ts
  • Modify: apps/gateway/src/agent/durable-session.repository.ts
  • Create: apps/gateway/src/agent/durable-session.repository.unit.test.ts
  • Modify: apps/gateway/src/commands/command-authorization.service.ts
  • Modify: apps/gateway/src/commands/command-authorization.service.spec.ts
  • Modify: apps/gateway/src/commands/runtime-approval-verifier.ts
  • Modify: packages/agent/src/durable-session.ts
  • Modify: packages/agent/src/durable-session.test.ts
  • Modify: packages/mosaic/src/commands/interaction.ts
  • Modify: packages/mosaic/src/commands/interaction.test.ts
  • Modify: packages/mosaic/src/tui/gateway-api.ts
  • Modify: packages/mosaic/src/tui/gateway-api.interaction-errors.test.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-05.md

Step 1: Write RED reservation, crash, audit, and stop tests

Cover admin scope, server-resolved policy, same-key replay, changed-semantics conflict, concurrent distinct-key single launch, and refusal to replace an active runtime binding. Inject failures before host dispatch, after host spawn, after host journal result, before Gateway compare-and-set, and after response creation.

For stop, cover server-derived operation correlation, duplicate clicks, wrong/expired/mismatched approval, and every Section 4.7 crash boundary around Redis verification/DB acceptance/atomic claim/GETDEL/host dispatch/reconciliation. Also cover dropped response after graceful/force termination, disconnect, Gateway reconstruction, already-stopped status, and prohibition on a new operation while pending. Assert InteractionController owns every new route, caller bodies contain no provider/host/path/command/correlation fields, missing/forged X-Correlation-Id cannot select the operation correlation, and audit is allowlisted only.

Step 2: Run RED

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/agent/__tests__/runtime-provider-registry.service.test.ts \
  src/agent/runtime-operation.service.test.ts \
  src/agent/runtime-operation-recovery.service.test.ts \
  src/agent/interaction.controller.test.ts \
  src/agent/durable-session.repository.unit.test.ts \
  src/commands/command-authorization.service.spec.ts
pnpm --filter @mosaicstack/agent test -- durable-session
pnpm --filter @mosaicstack/mosaic exec vitest run \
  src/commands/interaction.test.ts src/tui/gateway-api.interaction-errors.test.ts

Expected: FAIL on missing create reservation, stop approval/status routes, and operation reconciliation.

Step 3: Implement durable create reservation before dispatch

RuntimeOperationService.start() creates one Gateway UUID operation, resolves descriptor/policy/enrollment values server-side, JCS hashes Section 4.3, and calls exact reserveCreate(). Persist the specified pending/failed runtime references and typed provider.create outbox content. Dispatch the fully resolved request, then exact pending-reference CAS to runtime ID. A processing outbox is never automatically requeued; reconciliation calls session.lookup for that operation.

Implement all six Section 4.3 repository methods plus listRecoverableRuntimeOperations(). Same-scope create() cannot replace active identity. New reservation after stop requires a matching delivered stop record for current runtime. Task 5 tests through domain/in-memory and mocked repository transactions only; do not run existing PGlite repository tests. No schema or DDL change.

Step 4: Implement Gateway startup recovery

Register RuntimeOperationRecoveryService in AgentModule and implement the exact OnApplicationBootstrap enumeration/transitions from Section 4.6. Recovery gates readiness and never auto-launches a pending create.

Step 5: Add create audit and HTTP/CLI start

RuntimeProviderService.createSession() performs frozen scope/capability checks and requested/succeeded/denied/failed audit with typed internal provenance. Add exact POST /api/interaction/:agentName/sessions/:sessionId/start, body { idempotencyKey } only, returning only BrowserRuntimeSessionStartResultV1. mosaic interaction start <session-id> --idempotency-key <uuid> calls it with no host/path/runtime/command flags.

Step 6: Add stop approval, dispatch, and status routes

Implement the five Section 4.7 interaction routes: readiness, start, stop approval, stop dispatch, and stop status. Add non-destructive approval verification and atomic compare-delete to CommandAuthorizationService/verifier. Persist durable authorization acceptance before delete/dispatch exactly as Section 4.7; a processing retry queries host first. Browser/CLI never supplies action correlation.

Step 7: Run GREEN

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/agent/__tests__/runtime-provider-registry.service.test.ts \
  src/agent/runtime-operation.service.test.ts \
  src/agent/runtime-operation-recovery.service.test.ts \
  src/agent/interaction.controller.test.ts \
  src/agent/durable-session.repository.unit.test.ts \
  src/commands/command-authorization.service.spec.ts
pnpm --filter @mosaicstack/agent test -- durable-session
pnpm --filter @mosaicstack/mosaic exec vitest run \
  src/commands/interaction.test.ts src/tui/gateway-api.interaction-errors.test.ts

Expected: PASS with DB repository fakes only; do not connect PostgreSQL/PGlite.

Step 8: Commit

git add apps/gateway/src/agent/agent.module.ts \
  apps/gateway/src/agent/runtime-provider-registry.service.ts \
  apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts \
  apps/gateway/src/agent/runtime-operation.service.ts \
  apps/gateway/src/agent/runtime-operation.service.test.ts \
  apps/gateway/src/agent/runtime-operation-recovery.service.ts \
  apps/gateway/src/agent/runtime-operation-recovery.service.test.ts \
  apps/gateway/src/agent/interaction.controller.ts \
  apps/gateway/src/agent/interaction.controller.test.ts \
  apps/gateway/src/agent/durable-session.dto.ts \
  apps/gateway/src/agent/durable-session.service.ts \
  apps/gateway/src/agent/durable-session.repository.ts \
  apps/gateway/src/agent/durable-session.repository.unit.test.ts \
  apps/gateway/src/commands/command-authorization.service.ts \
  apps/gateway/src/commands/command-authorization.service.spec.ts \
  apps/gateway/src/commands/runtime-approval-verifier.ts \
  packages/agent/src/durable-session.ts packages/agent/src/durable-session.test.ts \
  packages/mosaic/src/commands/interaction.ts packages/mosaic/src/commands/interaction.test.ts \
  packages/mosaic/src/tui/gateway-api.ts \
  packages/mosaic/src/tui/gateway-api.interaction-errors.test.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-05.md"
git commit -m "feat(#${ISSUE_NUMBER}): add durable Agent Host operations"

Task 6: Route existing Web chat through an ordered runtime bridge

Files:

  • Create: apps/gateway/src/chat/runtime-chat-bridge.service.ts
  • Create: apps/gateway/src/chat/runtime-chat-bridge.service.spec.ts
  • Create: apps/gateway/src/chat/runtime-message-id.ts
  • Create: apps/gateway/src/chat/runtime-message-id.spec.ts
  • Create: apps/gateway/src/chat/chat.gateway-runtime-provider.spec.ts
  • Modify: apps/gateway/src/chat/chat.module.ts
  • Modify: apps/gateway/src/chat/chat.gateway.ts
  • Modify: apps/gateway/src/agent/durable-session.service.ts
  • Modify: packages/brain/src/conversations.ts
  • Create: packages/brain/src/conversations.idempotency.test.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-06.md

Step 1: Write RED routing, ordering, and persistence tests

Given an authenticated socket and authorized durable binding, assert Gateway sends through RuntimeProviderService, not AgentService.prompt(); owns one provider stream independent of sockets; double-redacts events; and maps only safe typed browser events.

Cover Gateway digest recomputation failure, accepted duplicate sequence, differing-digest accepted duplicate, equal/differing digest for the same future-buffer sequence, future gap, late-gap recovery, gap timeout, complete-before-delta, duplicate completion, transient reconnect/replay, cursor expiry, browser refresh, and service reconstruction. No message.complete/agent:end may persist/emit before prior turn sequences. UUIDv5 completion ID plus conflict-ignore must persist one assistant message across duplicate/retry; an in-flight turn after Gateway restart becomes visibly indeterminate.

Step 2: Run RED

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/chat/runtime-chat-bridge.service.spec.ts \
  src/chat/runtime-message-id.spec.ts \
  src/chat/chat.gateway-runtime-provider.spec.ts
pnpm --filter @mosaicstack/brain test -- conversations.idempotency

Expected: FAIL on missing bridge, reorder/gap handling, and deterministic message persistence.

Step 3: Implement RuntimeChatBridgeService

Keep provider stream ownership outside sockets. Track expected-next sequence, a 64-event/2-second reorder buffer, accepted cursor, per-turn completion, and event digests. Request strict replay on a gap and fail closed if unresolved. Apply shared streaming redaction before persistence/browser even though host already redacted.

Persist completed assistant content through the existing conversations repository with deterministic UUIDv5 ID and conflict-ignore. Store only bounded runtime/turn/message IDs in metadata; never native frames or tool data.

Step 4: Add the narrow ChatGateway branch

If an authorized durable runtime binding exists, route through the bridge. Otherwise preserve current Pi behavior. The client cannot choose provider or target. Map delta/tool/state/error/end into existing typed events only after ordering checks.

Step 5: Implement refresh/reconnect semantics

Refresh reloads persisted conversation messages and rejoins the same bridge stream. Transient host disconnect emits disconnected/reconnecting and resumes after accepted cursor. Cursor expiry reloads persisted transcript, warns, and never calls create. Gateway service reconstruction marks any uncompleted turn indeterminate rather than auto-resuming/persisting it.

Step 6: Run GREEN and legacy regressions

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/chat/runtime-chat-bridge.service.spec.ts \
  src/chat/runtime-message-id.spec.ts \
  src/chat/chat.gateway-runtime-provider.spec.ts \
  src/chat/chat.gateway-redaction.spec.ts \
  src/chat/chat.gateway-command-approval.spec.ts \
  src/chat/__tests__/chat-security.test.ts
pnpm --filter @mosaicstack/brain test -- conversations.idempotency

Expected: PASS; current Pi chat, auth, ownership, redaction, Discord, and durable-session behavior remain green. No service or database starts.

Step 7: Commit

git add apps/gateway/src/chat/runtime-chat-bridge.service.ts \
  apps/gateway/src/chat/runtime-chat-bridge.service.spec.ts \
  apps/gateway/src/chat/runtime-message-id.ts \
  apps/gateway/src/chat/runtime-message-id.spec.ts \
  apps/gateway/src/chat/chat.gateway-runtime-provider.spec.ts \
  apps/gateway/src/chat/chat.module.ts apps/gateway/src/chat/chat.gateway.ts \
  apps/gateway/src/agent/durable-session.service.ts \
  packages/brain/src/conversations.ts packages/brain/src/conversations.idempotency.test.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-06.md"
git commit -m "feat(#${ISSUE_NUMBER}): bridge runtime events into Web chat"

Task 7: Add minimal Web host/session UI and working stop control

Files:

  • Create: apps/web/src/components/chat/runtime-target-card.tsx
  • Create: apps/web/src/components/chat/runtime-target-card.test.tsx
  • Create: apps/web/src/components/chat/runtime-session-status.tsx
  • Create: apps/web/src/components/chat/runtime-session-status.test.tsx
  • Create: apps/web/src/lib/runtime-session.ts
  • Create: apps/web/src/lib/runtime-session.test.ts
  • Modify: apps/web/src/app/(dashboard)/chat/page.tsx
  • Modify: apps/web/src/components/chat/chat-input.tsx
  • Modify: apps/web/src/lib/api.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-07.md

Step 1: Write RED pure-state and server-rendered component tests

Cover ready/not-ready, create replay, boundary errors, every lifecycle label, disabled send while starting/stopping, refresh state that does not request start, semantic labels/focus hooks, safe metadata only, and Stop calling approval issuance then stop with the returned operation ID/reference.

pnpm --filter @mosaicstack/web test -- runtime-session runtime-target-card runtime-session-status

Expected: FAIL because runtime state/components and API methods do not exist. Do not start Next.js or Playwright in this task.

Step 2: Implement one pinned target card

Display safe host label, Mosaic/Claude versions, auth readiness, opaque workspace label, seat/persona label, and connection state. Never display filesystem paths, credential state, enrollment ID, or lease/TTL state.

Step 3: Start once and refresh safely

Generate one UUID idempotency key per unbound conversation, call start, and retain it for retries. Refresh loads conversation/durable binding first and never generates/calls create when already pending or active. Disable duplicate clicks while request/reconciliation is pending.

Step 4: Wire existing chat events and exact stop

Reuse typed agent:start, agent:text, safe tool/state events, agent:end, and error; retain Pi fallback. Confirmation calls stop-approval, then sends exactly the server-returned stopOperationId and approvalRef. Browser never creates correlation/action fields. Poll same-operation status after an indeterminate result; already stopped is success; definitive failed renders only SafeRuntimeError and requires a newly confirmed stop action if retryable.

Step 5: Repair touched chat theming only

Use existing design tokens and semantic controls. Ensure opaque backgrounds, visible text/caret/borders, focus rings, disabled/loading states, and reduced-motion-safe status changes. Do not redesign the dashboard. Browser light/dark/WCAG evidence is deferred to certified Task 9.

Step 6: Run GREEN

pnpm --filter @mosaicstack/web test
pnpm --filter @mosaicstack/web typecheck
pnpm --filter @mosaicstack/web lint

Expected: PASS without starting Web/Gateway/PostgreSQL.

Step 7: Commit

git add apps/web/src/components/chat/runtime-target-card.tsx \
  apps/web/src/components/chat/runtime-target-card.test.tsx \
  apps/web/src/components/chat/runtime-session-status.tsx \
  apps/web/src/components/chat/runtime-session-status.test.tsx \
  apps/web/src/lib/runtime-session.ts apps/web/src/lib/runtime-session.test.ts \
  'apps/web/src/app/(dashboard)/chat/page.tsx' \
  apps/web/src/components/chat/chat-input.tsx apps/web/src/lib/api.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-07.md"
git commit -m "feat(#${ISSUE_NUMBER}): chat with an enrolled Claude host"

Task 8: Prove service-level security, crash, ordering, replay, and provenance

Files:

  • Create: apps/gateway/src/__tests__/integration/agent-host-claude.service.test.ts
  • Create: packages/mosaic/src/agent-host/agent-host-security.spec.ts
  • Modify: apps/gateway/src/agent-host/agent-host.gateway.spec.ts
  • Modify: apps/gateway/src/agent/runtime-operation.service.test.ts
  • Modify: apps/gateway/src/chat/runtime-chat-bridge.service.spec.ts
  • Modify: packages/log/src/streaming-redactor.spec.ts
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-08.md

Step 1: Build a no-service/no-model fixture

Compose Gateway services, fake authenticated Socket.IO transport, host client, and fake absolute Mosaic launcher in-process. Launcher emits certified Claude NDJSON. Still assert exact mosaic claude descendant argv/environment; do not mock around protocol, reservation, journal, event ordering, or redaction. Do not bind ports, start Nest/Next/PostgreSQL, or apply DDL.

Step 2: Add abuse, crash, and ordering cases

Cover forged/revoked/superseded/wrong-host credential; wrong actor/tenant/session; unapproved policy; shell metacharacters/oversized frames; malformed event and same-sequence/different-digest; gap rejection, late-gap recovery, complete-before-delta, duplicate completion; disconnect before dispatch, after spawn, after journal commit, before Gateway compare-and-set, and during stream; replay across reconnect; Gateway reconstruction with no duplicate message; stale cursor; same-key replay versus concurrent distinct-key conflict; stop response loss after graceful/force kill; and split OAuth-like values/private keys/tokens/emails/home paths across NDJSON/stdout/stderr/error/provenance chunks.

Step 3: Assert typed provenance and secret absence

Safe event/readiness/audit identify only D2/D12-approved host label and contract provenance, versions, adapter, enrollment generation, and launch ID. Fake-process assertions verify absolute fixture paths match the descriptor, but audit/browser/transcript assertions require the Section 4.2 projection only. Scan every captured frame, audit entry, transcript row, error, and log for canaries and raw argv/env/OAuth bytes.

Step 4: Run focused GREEN

env -u DATABASE_URL pnpm --filter @mosaicstack/gateway exec vitest run \
  src/__tests__/integration/agent-host-claude.service.test.ts \
  src/agent-host/agent-host.gateway.spec.ts \
  src/agent/runtime-operation.service.test.ts \
  src/chat/runtime-chat-bridge.service.spec.ts
pnpm --filter @mosaicstack/mosaic exec vitest run \
  src/agent-host/agent-host-security.spec.ts
pnpm --filter @mosaicstack/log test -- streaming-redactor

Expected: PASS using fakes only, with all crash points producing one launch and one assistant completion at most.

Step 5: Run migration-free static baselines

pnpm typecheck
pnpm lint
pnpm format:check
pnpm build

Expected: PASS. Root pnpm test is explicitly forbidden here because it includes PGlite migration and framework-shell/lease-broker suites. Do not invoke Playwright, package-wide Gateway tests, dev, PostgreSQL/PGlite integration targets, Compose, systemd tests, or migrations in Task 8. Focused GREEN commands above are the complete permitted dynamic allowlist until Task 9's certified path.

Step 6: Commit

git add apps/gateway/src/__tests__/integration/agent-host-claude.service.test.ts \
  apps/gateway/src/agent-host/agent-host.gateway.spec.ts \
  apps/gateway/src/agent/runtime-operation.service.test.ts \
  apps/gateway/src/chat/runtime-chat-bridge.service.spec.ts \
  packages/mosaic/src/agent-host/agent-host-security.spec.ts \
  packages/log/src/streaming-redactor.spec.ts \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-08.md"
git commit -m "test(#${ISSUE_NUMBER}): prove Agent Host failure boundaries"

Task 9: Certify isolated Distrobox and one live OAuth browser flow

Files:

  • Create: docs/guides/agent-host-claude-m1.md
  • Create: docs/reports/qa/${ISSUE_NUMBER}-agent-host-live-e2e.md
  • Modify: docs/openapi-tess.yaml
  • Modify: docs/tess/USER-GUIDE.md
  • Modify: docs/SITEMAP.md
  • Modify: .env.example with names only and no secret defaults
  • Modify: apps/web/e2e/chat.spec.ts
  • Modify: apps/web/playwright.config.ts
  • Create: apps/gateway/src/agent/durable-session.repository.certification.test.ts
  • Create: docs/reports/documentation/${ISSUE_NUMBER}-agent-host-checklist.md
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-task-09.md

Step 1: Freeze the candidate and prepare greenfield environment

Record base separately, then freeze remote implementation head:

git fetch origin "$IMPLEMENTATION_BRANCH"
IMPLEMENTATION_PR_HEAD_SHA="$(git rev-parse "origin/$IMPLEMENTATION_BRANCH")"
test "$IMPLEMENTATION_PR_HEAD_SHA" = "$(git rev-parse HEAD)"

Create Debian Distrobox with dedicated home. Do not inherit/overwrite host ~/.config/mosaic, ~/.mosaic, stock ~/.claude, Codex, Pi, or OpenCode state. Inside it, create an attached clean implementation branch—not detached base/head:

GREENFIELD_REPO="$HOME/src/stack-web-fleet-${ISSUE_NUMBER}"
git clone https://git.mosaicstack.dev/mosaicstack/stack.git "$GREENFIELD_REPO"
cd "$GREENFIELD_REPO"
git fetch origin "$IMPLEMENTATION_BRANCH"
git switch -c "$IMPLEMENTATION_BRANCH" --track "origin/$IMPLEMENTATION_BRANCH"
test "$(git rev-parse HEAD)" = "$IMPLEMENTATION_PR_HEAD_SHA"

Record base/head/image versions. Develop, review, commit, and push Task 9's e2e/docs delta from this attached branch via Section 5.0. Task 10 repeats evidence after that delta is committed/pushed.

Step 2: Respect the Gateway startup gate

Use only the exact Fred/Scooby-certified startup script recorded in Task 0:

"$CERTIFIED_STARTUP_SCRIPT" --worktree "$PWD" \
  --source-sha "$IMPLEMENTATION_PR_HEAD_SHA" --web-url "$CERTIFIED_WEB_URL"

Expected: exit 0 and deployed/test artifact reports the exact candidate SHA. Do not run root pnpm dev, ad hoc Gateway/Web, Compose PostgreSQL, a migration runner, or manual image build around KBN.

Create a certification test that connects only to the pre-provisioned disposable certified database, uses unique owner/session IDs plus transaction cleanup, and imports/calls no migration or DDL function. It proves real reserveCreate/activate/fail/reserveStop/completeStop/listRecoverable CAS and concurrency:

! rg -n 'run.*Migration|migrate|CREATE TABLE|ALTER TABLE' \
  apps/gateway/src/agent/durable-session.repository.certification.test.ts
: "${CERTIFIED_DATABASE_URL:?Load the certified disposable DB URL from Task 0's secret source}"
MOSAIC_KBN_CERTIFIED_DB_TEST=1 DATABASE_URL="$CERTIFIED_DATABASE_URL" \
  pnpm --filter @mosaicstack/gateway exec vitest run \
  src/agent/durable-session.repository.certification.test.ts

Expected: PASS with no DDL/migration. If exact DB/startup path is absent, this task and PR completion are blocked.

Step 3: Authenticate Claude locally

Jason performs native Claude login inside the isolated home. The report records only ready/not-ready, account classification approved for testing, CLI version, and success/failure. Never copy or print the OAuth store.

Step 4: Run the live slice and record bounded evidence

  1. Sign into WebUI through Authentik and record authenticated user/tenant classification, not cookies.
  2. Confirm the pinned host readiness DTO and versions.
  3. Capture Agent Host registered-child count 0; start one conversation; capture count 1, internal launch/runtime IDs, and only the D2-approved private match/provenance evidence. None of these internal fields may appear in the browser response.
  4. Send a harmless deterministic prompt: request the configured Mosaic seat name plus one non-secret contract marker. Compare output with Fred's descriptor/contract. Treat this only as behavioral corroboration, never an authorization/security signal.
  5. Verify ordered streamed response, one persisted assistant message, sanitized provenance, and no raw path in browser/API.
  6. Refresh during/after the session; capture the same launch/runtime IDs and registered-child count 1.
  7. Send a second prompt; verify same native session and incremented sequence.
  8. Issue exact stop approval, repeat the same Stop click/request safely, reconcile the same operation, and capture terminal child count 0. Dropped-response fault injection remains Task 8; do not manipulate the certified live network.
  9. Exercise one sanitized host, runtime, auth, and cursor failure; verify each reports the correct boundary without create.
  10. Run the approved repository secret scanner against captured browser network/logs, Gateway logs/audit fixture, transcript query, host event capture, QA report, and git diff. Record command names, exit codes, and canary counts only.

Step 5: Run certified headless Web checks

Update apps/web/playwright.config.ts with exactly four Chromium projects: desktop-light (1440×900, colorScheme: light), desktop-dark, mobile-light (390×844), and mobile-dark. Keep webServer absent and require external PLAYWRIGHT_BASE_URL. Update apps/web/e2e/chat.spec.ts with deterministic CSS contrast calculations and runtime flow assertions.

PLAYWRIGHT_BASE_URL="$CERTIFIED_WEB_URL" \
  pnpm --filter @mosaicstack/web exec playwright test e2e/chat.spec.ts

Expected: all four projects PASS headlessly. Assert keyboard order, visible focus, status announcements, disabled/loading states, opaque surfaces, and WCAG AA contrast. Store screenshot artifact paths/test exit code in QA report; do not commit images containing personal content.

Step 6: Document exact operator inputs and scan evidence

Document Fred-approved sources and these canonical names only. Host: MOSAIC_AGENT_HOST_GATEWAY_URL, MOSAIC_AGENT_HOST_MANIFEST, MOSAIC_AGENT_HOST_TOKEN_FILE, MOSAIC_AGENT_HOST_LAUNCH_DESCRIPTOR. Gateway: MOSAIC_GATEWAY_AGENT_HOST_MANIFEST, MOSAIC_GATEWAY_AGENT_HOST_TOKEN_FILE. No aliases, secret defaults, or raw path values. Run reviewed secret scanner; inspect key names/status only and never print credential-bearing files.

Step 7: Complete documentation checklist and commit

git add docs/guides/agent-host-claude-m1.md \
  "docs/reports/qa/${ISSUE_NUMBER}-agent-host-live-e2e.md" docs/openapi-tess.yaml \
  docs/tess/USER-GUIDE.md docs/SITEMAP.md .env.example apps/web/e2e/chat.spec.ts \
  apps/web/playwright.config.ts \
  apps/gateway/src/agent/durable-session.repository.certification.test.ts \
  "docs/reports/documentation/${ISSUE_NUMBER}-agent-host-checklist.md" \
  "docs/reports/code-review/${ISSUE_NUMBER}-task-09.md"
git commit -m "docs(#${ISSUE_NUMBER}): certify native Claude Agent Host"

Task 10: Exact-head review, CI, merge, post-merge proof, and issue closure

Files:

  • Update: docs/scratchpads/webui-fleet-bridge-plan.md
  • Create: docs/reports/code-review/${ISSUE_NUMBER}-agent-host.md
  • Create: docs/reports/security/${ISSUE_NUMBER}-agent-host.md
  • Complete: docs/reports/documentation/${ISSUE_NUMBER}-agent-host-checklist.md

Step 1: Freeze and self-review the full implementation diff

git fetch origin next "$IMPLEMENTATION_BRANCH"
PR_HEAD_SHA="$(git rev-parse HEAD)"
test "$PR_HEAD_SHA" = "$(git rev-parse "origin/$IMPLEMENTATION_BRANCH")"
git diff --check "origin/next...$PR_HEAD_SHA"

Check scope creep, duplicated prompt assembly, direct broker imports/reads, raw secret/path surfaces, untrusted target selection, process ambiguity, ordering/replay, Redis/DB stop boundaries, and Pi regressions. Repeat Task 8 focused/static commands and Task 9 certified DB/browser evidence on this exact SHA.

Step 2: Write independent review reports

Author cannot review own code. Fred reviews structure/launcher/F-V3; an independent security reviewer covers enrollment/revocation, OAuth isolation, create/stop crash safety, ordering, redaction, and provenance. Write both report files with reviewed full SHA and zero unresolved blockers. Remediate, rerun tests/evidence, and regenerate both whenever source changes.

Step 3: Commit reports/checklist/scratchpad and push through guard

git add docs/scratchpads/webui-fleet-bridge-plan.md \
  "docs/reports/code-review/${ISSUE_NUMBER}-agent-host.md" \
  "docs/reports/security/${ISSUE_NUMBER}-agent-host.md" \
  "docs/reports/documentation/${ISSUE_NUMBER}-agent-host-checklist.md"
git commit -m "docs(#${ISSUE_NUMBER}): record Agent Host delivery evidence"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B "$IMPLEMENTATION_BRANCH"
git push origin "$IMPLEMENTATION_BRANCH"
FINAL_HEAD_SHA="$(git rev-parse HEAD)"

A non-ready guard blocks; never bypass. Require terminal-green:

~/.config/mosaic/tools/git/pr-ci-wait.sh -n "$IMPLEMENTATION_PR_NUMBER"

Step 4: Exact-final-head provider reviews

Because the report commit changed the head, Fred and the independent security reviewer each inspect FINAL_HEAD_SHA and post a provider review naming that full SHA through their own verified principals:

~/.config/mosaic/tools/git/pr-review.sh \
  -n "$IMPLEMENTATION_PR_NUMBER" -a approve \
  -c "Structure review APPROVE exact head $FINAL_HEAD_SHA"
~/.config/mosaic/tools/git/pr-review.sh \
  -n "$IMPLEMENTATION_PR_NUMBER" -a approve \
  -c "Security review APPROVE exact head $FINAL_HEAD_SHA"

These are executed by the respective reviewers, not Velma impersonating them. Any request-changes or head change returns to Steps 14.

Step 5: Coordinator go-ahead, exact-head merge guard, and squash merge

After coordinator explicitly authorizes merge:

git fetch origin next
PRE_MERGE_NEXT_SHA="$(git rev-parse origin/next)"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge \
  -B "$IMPLEMENTATION_BRANCH" -R mosaicstack/stack --sha "$FINAL_HEAD_SHA"
~/.config/mosaic/tools/git/pr-merge.sh \
  -n "$IMPLEMENTATION_PR_NUMBER" -m squash --expect-head "$FINAL_HEAD_SHA"
~/.config/mosaic/tools/git/pr-ci-wait.sh -n "$IMPLEMENTATION_PR_NUMBER"
git fetch origin next
MERGE_SHA="$(git rev-parse origin/next)"
test "$MERGE_SHA" != "$PRE_MERGE_NEXT_SHA"
git merge-base --is-ancestor "$PRE_MERGE_NEXT_SHA" "$MERGE_SHA"
test "$(git rev-parse "$MERGE_SHA^")" = "$PRE_MERGE_NEXT_SHA"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge \
  -B next -R mosaicstack/stack --sha "$MERGE_SHA" --require-status

Every command/assertion must succeed. Queue serialization, the exact-head merge precondition, and direct parent assertion identify MERGE_SHA as the squash result produced from the reviewed head.

Step 6: Exact merged-SHA smoke, issue closure, and tracking closure

POST_MERGE_WORKTREE="$(mktemp -d)"
SMOKE_REPORT="$(mktemp)"
git worktree add --detach "$POST_MERGE_WORKTREE" "$MERGE_SHA"
"$CERTIFIED_POST_MERGE_SMOKE_SCRIPT" \
  --worktree "$POST_MERGE_WORKTREE" \
  --source-sha "$MERGE_SHA" --web-url "$CERTIFIED_WEB_URL" > "$SMOKE_REPORT"
test "$(git -C "$POST_MERGE_WORKTREE" rev-parse HEAD)" = "$MERGE_SHA"
python3 - "$SMOKE_REPORT" "$MERGE_SHA" <<'PY'
import json, sys
with open(sys.argv[1], encoding='utf-8') as handle:
    raw = handle.read(4097)
if len(raw.encode('utf-8')) > 4096:
    raise SystemExit('post-merge smoke report exceeded 4 KiB')
def strict_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError(f'duplicate key: {key}')
        result[key] = value
    return result
report = json.loads(raw, object_pairs_hook=strict_object)
expected = sys.argv[2]
allowed = {
    'sourceSha', 'worktreeSha', 'deployedSha', 'status',
    'readiness', 'headlessChat', 'maxChildCount',
}
if not isinstance(report, dict) or set(report) != allowed:
    raise SystemExit('post-merge smoke used a non-exact or extended schema')
for field in ('sourceSha', 'worktreeSha', 'deployedSha'):
    if report[field] != expected:
        raise SystemExit(f'{field} did not match exact merged SHA')
if report['status'] != 'passed' or report['readiness'] is not True:
    raise SystemExit('post-merge readiness did not pass')
if report['headlessChat'] is not True:
    raise SystemExit('post-merge headless chat assertion failed')
if type(report['maxChildCount']) is not int or report['maxChildCount'] != 1:
    raise SystemExit('post-merge exact integer child-count assertion failed')
PY
git worktree remove "$POST_MERGE_WORKTREE"
~/.config/mosaic/tools/git/issue-close.sh -i "$ISSUE_NUMBER" \
  -c "Delivered by PR #$IMPLEMENTATION_PR_NUMBER at $MERGE_SHA; merged-next CI and certified smoke green."
~/.config/mosaic/tools/git/issue-list.sh \
  --state closed --limit 1000 --repo mosaicstack/stack \
  > /tmp/web-fleet-closed-issues.txt
python3 - /tmp/web-fleet-closed-issues.txt "$ISSUE_NUMBER" <<'PY'
import re, sys
text = open(sys.argv[1], encoding='utf-8', errors='replace').read().splitlines()
issue = re.escape(sys.argv[2])
if not any(re.match(rf'^\s*#?{issue}(?:\s|\|)', line) for line in text):
    raise SystemExit('exact issue was absent from provider closed-state query')
PY

Smoke stdout must be one exact-key JSON object: three SHA fields equal MERGE_SHA, status: passed, readiness: true, headlessChat: true, and maxChildCount: 1. Unknown/extra keys fail, so credentials, paths, prompts, or personal content cannot hide in the report. It must not expose provider OAuth or paths.

Fred/orchestrator then closes tracking through the same wrappers (the comment marks where the single-writer edit occurs):

TRACKING_BRANCH="docs/${ISSUE_NUMBER}-close-webui-fleet-tracking"
git fetch origin next
git switch --detach origin/next
git switch -c "$TRACKING_BRANCH"
# Fred/orchestrator marks all rows done, fills started_at/completed_at/used/notes.
pnpm exec prettier --check docs/TASKS.md
git diff --check
# Independent haiku review of the exact docs/TASKS.md delta must pass here.
git add docs/TASKS.md
git commit -m "chore(#${ISSUE_NUMBER}): close WebUI Fleet bridge tracking"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B "$TRACKING_BRANCH"
git push -u origin "$TRACKING_BRANCH"
~/.config/mosaic/tools/git/pr-create.sh \
  -t "chore(#${ISSUE_NUMBER}): close WebUI Fleet bridge tracking" \
  -b "Post-delivery tracker closure for merged PR #${IMPLEMENTATION_PR_NUMBER}." \
  -B next -H "$TRACKING_BRANCH"
# Record TRACKING_PR_NUMBER, then:
~/.config/mosaic/tools/git/pr-ci-wait.sh -n "$TRACKING_PR_NUMBER"
TRACKING_HEAD_SHA="$(git rev-parse HEAD)"
git fetch origin next
TRACKING_PRE_NEXT_SHA="$(git rev-parse origin/next)"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge \
  -B "$TRACKING_BRANCH" -R mosaicstack/stack --sha "$TRACKING_HEAD_SHA"
~/.config/mosaic/tools/git/pr-merge.sh \
  -n "$TRACKING_PR_NUMBER" -m squash --expect-head "$TRACKING_HEAD_SHA"
~/.config/mosaic/tools/git/pr-ci-wait.sh -n "$TRACKING_PR_NUMBER"
git fetch origin next
TRACKING_MERGE_SHA="$(git rev-parse origin/next)"
test "$TRACKING_MERGE_SHA" != "$TRACKING_PRE_NEXT_SHA"
test "$(git rev-parse "$TRACKING_MERGE_SHA^")" = "$TRACKING_PRE_NEXT_SHA"
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge \
  -B next -R mosaicstack/stack --sha "$TRACKING_MERGE_SHA" --require-status

Any failure leaves delivery blocked. Do not report completion before tracking PR, issue closure, merged-next CI, and post-merge smoke all succeed.

6. Explicit M1 non-goals

  • Codex, Pi CLI, or OpenCode adapters;
  • arbitrary host/workspace/persona selection;
  • generalized enrollment/pairing UI;
  • host failover or process adoption after host-daemon restart;
  • Gateway restart stream recovery beyond fail-closed preservation;
  • lease/VERIFIED/TTL display;
  • Matrix transport or federation;
  • tmux streaming/scraping;
  • task/project/Mosaic Brain integration;
  • broad dashboard redesign;
  • provider approval-response UI beyond visible state and exact session stop;
  • any database migration.

7. Acceptance criteria to evidence map

Acceptance criterion Planned evidence
Browser response came from native Claude OAuth Task 9 native login classification, runtime version, launch ID, ordered event capture, and transcript assertion
Launched through Mosaic, not direct Claude Task 1 descendant argv/environment certificate + Task 8 fake-process assertion + Task 9 descriptor-match/artifact/process proof
Current framework identity reached the session Descriptor generation/digest + sanitized provenance + Task 9 exact seat/contract-marker prompt (behavioral corroboration only)
Registered durable Fleet session Task 5 interaction_sessions pending→active CAS + provider.create outbox evidence + host journal/audit projection
Browser ↔ Gateway ↔ Host ↔ Claude stream Task 8 in-process full protocol fixture + Task 9 live E2E
Lifecycle visible and correlated Typed Task 2 contract + Tasks 6/7 tests + Task 9 launch/runtime/turn IDs
Refresh reuses process Task 6 socket-independent stream + Task 9 same IDs and registered-child count remains 1
Exact idempotent stop Host operation journal + Gateway stop outbox/approval tests + Task 9 same-operation reconciliation and child count 0
OAuth absent from browser/Gateway/DB/argv/events Task 8 split canaries + Task 9 scans of network/log/audit/transcript/event/report/diff artifacts
Light/dark chat usable Task 7 semantic/unit checks + Task 9 headless screenshots, focus, and contrast assertions
Boundary-specific errors Typed Task 2 taxonomy + Tasks 3/4/7 tests + Task 9 sanitized failure samples
Authorization/order/redaction/reconnect/failed launch covered Task 8 explicit abuse/crash/gap matrix and pass output
Agent Host has no direct broker/daemon read path Static import/string scan + Fred structure review + independent security diff review
No unsafe Gateway startup bypass Task 0 baseline gate + exact Fred/Scooby startup command/reference and Task 9 execution log

8. Stop conditions

Stop and report to Fred before code or before proceeding when:

  1. no reconciled immutable baseline exists;
  2. Fred has not approved the seat-home/launcher/enrollment contract;
  3. Claudes pinned version cannot sustain structured stream-json input/output through mosaic claude;
  4. /user or git/provider actions stop resolving to velma;
  5. a required queue guard is non-ready;
  6. a safe Gateway/Web startup path is unavailable;
  7. M1 requires a DB migration before KBN migration authority lands;
  8. any design requires browser/Gateway access to provider OAuth, raw workspace paths, or direct broker state;
  9. existing durable tables cannot atomically reserve one active launch without migration;
  10. enrollment revocation cannot be revalidated per command or stale connection epochs cannot be rejected;
  11. exact stop cannot preserve durable one-time approval plus same-operation reconciliation;
  12. ordered completion cannot prevent a gap/duplicate from persisting a false or duplicate transcript;
  13. a live test would require overwriting an existing runtime home.

9. Execution handoff

After Fred approves this plan, replaces every [FRED-GATE], records IMPLEMENTATION_BASE_SHA, and Task 0 closes, execute serially with the external executing-plans skill at ~/.config/mosaic/skills/executing-plans/SKILL.md. Do not parallelize changes that touch the shared runtime contract, Gateway provider registry, durable operation coordinator, or ChatGateway. Independent reviewers may work in parallel only after each implementation head is frozen.