Skip to content

feat(web): dock the state-grouped fleet beside the conductor's chat - #325

Merged
saucam merged 1 commit into
mainfrom
feat/p5-2-fleet-lanes
Sep 6, 2026
Merged

feat(web): dock the state-grouped fleet beside the conductor's chat#325
saucam merged 1 commit into
mainfrom
feat/p5-2-fleet-lanes

Conversation

@saucam

@saucam saucam commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

P5.2 — the centerpiece of the front-doors work (conductor-frontends-design.md §4, §12). Focusing the conductor now renders its ordinary chat with the fleet board docked in a right rail.

Shape

Classification lives in lib/fleet-lanes.ts as pure functions — the status vocabulary (§6) and triage lanes (§4) are the design decision here, so they belong somewhere a test can reach without a reactive root. FleetRail.tsx renders over state/fleet.ts (#324).

Two rules from §6 that are easy to get wrong

awaiting outranks working. A node needing a human must never hide behind one that's merely busy, so it's checked before the task's own status and sorts first. A worker wedged on an approval reads as awaiting even while its task still says running — the dispatch status describes the queue, the session status describes the agent, and it's the agent that's stuck on a person.

disconnected is not failed. A dropped runner is a transport event the dispatcher reconciles. Colouring it red trains the operator to ignore red, so it renders quiet grey. Relatedly: a claimed spawn that hasn't created its worker yet is queued, not disconnected — otherwise every healthy dispatch flashes a fault for a moment.

Two deliberate narrowings

"Ready to review" means a finished task with a digest — something a human can actually read. §4 envisages this lane eventually carrying worktree diffs and sequenced merge (P5.4); until then a review lane holding nothing readable would be worse than none.

Empty lanes are omitted. Four permanent headers over one running task is chrome, not information.

§3.B — the rail links out, never embeds

Every node is a real session, so clicking one focuses its ordinary cockpit; "take over" is the existing attach flow reached from a node. A task with no session to drill into renders as a plain row rather than a button that would do nothing.

SessionBody is extracted so the conductor branch docks a rail beside the same cockpit every agent gets, rather than reimplementing it.

Verified against a live conductor

Not just fixtures — I drove a real dispatch and classified the resulting board:

needs-you count: 2

Needs you  (2)
   [blocked     ] spawn cff8e6c5    scout  err:reclaimed: stale claim
   [blocked     ] spawn c0234348    scout  err:reclaimed: stale claim
Ready to review  (3)
   [done        ] spawn 1c261b20    scout  digest:task 1c261b20 (spawn/scout) in …
   [done        ] spawn 17960316    scout  digest:…
   [done        ] spawn 14bac189    scout  digest:…

The two tasks blocked by the old daemon-collision bug (#319) land in Needs you with their real error; the three successful dispatches land in Ready to review; needsYouCount matches agg.blockedTasks.

That run also surfaced something worth knowing: a finished task has no worker session — the dispatcher tears it down after the digest — so the session join returns null and the row falls back to the task id. Handled, and it's why done is classified before the null-session branch.

Verification

16 lane tests, 215 web tests total, typecheck / lint / build clean.

Component rendering itself still isn't locally testable (the Node <20.19 jsdom gap), but CI runs those suites.

Next

P5.1's remaining piece is the Conductor ⇄ Sessions toggle (§3.A); P5.3 is the cross-session "Needs you" inbox, which this rail's needsYouCount already feeds.

🤖 Generated with Claude Code

P5.2, the centerpiece of the front-doors work
(docs/conductor-frontends-design.md §4, §12). Focusing the conductor now
renders its ordinary chat with the fleet board docked in a right rail.

Classification lives in `lib/fleet-lanes.ts` as pure functions — the
status vocabulary (§6) and the triage lanes (§4) ARE the design decision
here, so they belong somewhere a test can reach without a reactive root.
`FleetRail.tsx` is the renderer over `state/fleet.ts`.

Two rules from §6 drive the vocabulary and are easy to get wrong:

**`awaiting` outranks `working`.** A node that needs a human must never
hide behind one that is merely busy, so it is checked before the task's
own status and sorts first. A worker wedged on an approval reads as
`awaiting` even while its task still says `running` — the dispatch status
describes the QUEUE, the session status describes the AGENT, and it is
the agent that is stuck on a person.

**`disconnected` is not `failed`.** A dropped runner is a transport
event; the dispatcher reconciles it. Colouring it red trains the operator
to ignore red, so it renders quiet grey. A claimed spawn that has not
created its worker yet is `queued`, not `disconnected` — otherwise every
healthy dispatch flashes a fault for a moment.

"Ready to review" is deliberately narrow: a finished task that produced a
digest, i.e. something a human can actually read. §4 envisages this lane
eventually carrying worktree diffs and sequenced merge (P5.4); until
then, a review lane holding nothing readable would be worse than none.
Empty lanes are omitted — four permanent headers over one running task is
chrome, not information.

Per §3.B the rail links out and never embeds a second copy of a session's
chat: every node is a real session, so clicking one focuses its ordinary
cockpit. "Take over" is the existing attach flow reached from a node. A
task with no session to drill into renders as a plain row rather than a
button that would do nothing. `SessionBody` is extracted so the conductor
branch docks a rail beside the SAME cockpit every agent gets, rather than
reimplementing it.

Verified against a live conductor, not just fixtures: a real board of 5
tasks classified correctly — the two tasks blocked by the old
daemon-collision bug landed in "Needs you" with their real error, the
three successful dispatches in "Ready to review" with digests, and
needsYouCount matched agg.blockedTasks. That run also confirmed a
finished task has NO worker session (the dispatcher tears it down after
the digest), so the session join returns null and the row falls back to
the task id — handled, and the reason `done` is classified before the
null-session branch.

16 lane tests; 215 web tests total; typecheck, lint and build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 98967db into main Sep 6, 2026
4 checks passed
saucam added a commit that referenced this pull request Sep 6, 2026
conductor-design §3 specifies that the fleet READ verbs — fleet_list,
fleet_find, fleet_summary, fleet_recall, fleet_tasks, machine_map — "run
silently". They did not. `isSafeTool` knew the memory and blackboard
mounts but not the fleet, so in guarded mode every one of them raised an
approval prompt.

Found by driving a real conductor rather than reading the code: asking it
to resolve a session reference produced an approval request for
`fleet_find`, which is a read. An assistant that asks permission to look
something up is not an assistant, and this is the first friction anyone
meets in the new conductor pane (#325).

The daemon already put these verbs in the provider's `allowedTools`, so
the SDK warned it would auto-approve them — but codeoid's own gate is
consulted independently and did not recognise them, which is why the two
disagreed.

Follows the established shape exactly: match the namespace prefix, then
require the suffix to be a known read verb. Never a bare prefix match — an
over-broad match here is a prompt bypass, which is why a look-alike
segment (`mcp__evil_codeoid_fleet__…`) is asserted to gain nothing.

The read list is `FLEET_READ_TOOLS` from the shared protocol package, so
the send half cannot leak in by someone editing one of two copies. Send
verbs remain hard-gated BEFORE this function is consulted
(`isFleetSendTool` in Session#shouldAutoApprove, checked ahead of any mode
logic); this is defence in depth, not the fence.

Verified live, both directions. With the fix, fleet_list / fleet_find /
machine_map go straight to `executing` with zero prompts; fleet_spawn
still lands in `waiting_confirmation` and waits for the owner. The R3
invariant is intact.

Also lists the bare (non-`mcp__`) namespacing so a mounted fleet (#245)
does not silently regress to prompting on every read.

10 tool-safety tests; 2452 daemon tests; typecheck and lint clean.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants