Extract the tag machinery (fleet config, board dispatch, fleet broadcast) into
joule_core::tag so a running editor and a joule-coded daemon are equal peers on
one board -- not two copies of the logic.
- joule_core::tag::TagNode owns the board + a per-node id sequence; dispatch() is
pure (mutates the board, returns the event to broadcast) so it's testable and
the caller does the I/O. broadcast_event() fans out to local unix peers and
TCP-paired remotes. Fleet config (Remote/FleetConfig/load) moves here as its
canonical home.
- joule_coded: Worker now holds a TagNode; handle_tag is a thin wrapper over
dispatch() + broadcast_event(). Removed the duplicated board/broadcast/config
code. pair/fleet use free functions for the remote transport (orphan rule).
- agent_joule_panel: the control server holds a TagNode global and answers the
"tag" method in dispatch_foreground -- a fleet concern, like peers/relay, so it
works with no window open and stays out of the panel view. Broadcast runs
off-thread so the UI never stalls on peer socket I/O.
Verified: joule_core::tag unit tests; a two-daemon regression (post, replicate,
claim, converge -- identical to before); and end-to-end against the running
editor, where the GUI posts a task, holds its board, and replicates it to a
daemon peer (submitter = the editor's socket).
Co-Authored-By: Claude Opus 4.8 <[email protected]>