FlowForge Generated Documentation

ADR-0036

Controller-User Relay Protocol

ACCEPTED Date 2026-05-13 Author @cruzalex

ADR-0036: Controller-User Relay Protocol — File-Based Inbox/Outbox + Daemon Push (N-Worker Relay Tax Collapse)

Status

Proposed — Authored 2026-05-13 under ticket #1266 as the operational moat of TUI The Bomb v1.0 cascade-launch (target ship: 2026-06-04). Founder-ratified master plan D4 on 2026-05-13 selects FS inbox/outbox + .ratified flag + parallelism-native slash commands /ratify all and /assign. This ADR formalizes that decision in the architecture registry.

Lifecycle: Becomes Accepted upon PR merge + founder go-ahead per feedback_no_auto_merge.md. Master plan D4 verbal ratification 2026-05-13 is the architectural intent; this ADR codifies that ratification into the registry. Once Accepted, REGISTRY.md row Status is updated to Accepted in a follow-up registry-housekeeping commit (BLUEPRINTS-SACRED — no silent rewrite).

Companion ADRs:

  • ADR-0034 — Fleet Aggregation Substrate (the daemon process this protocol layers on; merged 2026-05-13 in PR #1247 at commit 5dc6d689).
  • ADR-0035 (in-flight on ticket #1265) — TUI State Storage Schema, including the in-memory RelayInbox / RelayOutbox projection of the FS envelopes this ADR specifies.
  • ADR-0037 (in-flight on ticket #1261) — Schedule-View Rendering, which consumes the kind=dispatch_brief envelopes this ADR defines to render NOW/NEXT/QUEUED strips per swim-lane.
  • ADR-0028 — Hook layer as canonical writer of ~/.flowforge/ (architectural precedent for FS-as-truth).
  • ADR-0025 — Single-install layout (this ADR's paths sit under ~/.flowforge/repos/<id>/workers/<W-id>/).
  • ADR-0033 — UUID-based <repo-id> resolution.

Where this ADR and documentation/2.0/strategy/tui-the-bomb/architect.md §7 disagree, architect.md is the source of truth for design detail and this ADR is the source of truth for the decision intent. Any divergence surfaces as a gap-analysis ticket per the Milestone Quality Seal Rule — BLUEPRINTS-SACRED.

Context

The bottleneck (verbatim from architect.md §7.1)

Today the founder runs N workers in tmux panes. The controller (Claude in the ACE/maestro role) produces dispatch briefs in one terminal. The founder reads them, switches tmux pane, pastes into the worker terminal, switches back, pastes the response into the controller terminal. N× this load. Manual relay. Hours per day. Scales linearly with N.

This is the load-bearing operational pain that TUI The Bomb must solve to be more than a deletion-sprint redesign. The pain is inherent to N-worker parallelism, not to multi-monitor display: a founder running N=11 on a single monitor suffers it identically to a founder running N=11 across 11 monitors.

Parallelism-is-the-moat — the operational translation

Per memory pointer feedback_parallelism_is_the_moat.md (founder direction 2026-05-13), parallelism is FF's competitive differentiator versus Anthropic Agent View, Cursor, Aider, and Cline. The visual moat (D6 swim-lane fleet view) makes high-N legible. This ADR is the operational moat — the protocol that makes high-N actually operable. Without it, the visual moat shows the cost rather than collapsing it.

The ROI is super-linear in N (architect.md §7.5):

N Relays/hour Time saved/hour Per 8-hour day
3 15 ~11 min ~1.5 hours
5 25 ~19 min ~2.5 hours
11 55 ~41 min ~5.5 hours
25 125 ~94 min (1.5h) ~12.5 hours (>1 day!)

At N=11 the relay tax is ~5.5 hours/day; at N=25 it exceeds the workday. Collapsing this to O(keystrokes) is the highest-ROI architectural decision in the TUI The Bomb cohort (architect.md §11.2, master plan D4 "biggest single operational payoff").

What the daemon (ADR-0034) provides

ADR-0034 establishes flowforge-daemon as the single per-user state aggregator with an event-bus seam (SUB /v1/relay/inbox/events). The daemon is the substrate this protocol rides on:

  • The daemon owns the FS-watcher (inotify / fsnotify) that detects new inbox/outbox files and emits push-events to subscribed TUI surfaces.
  • The daemon exposes the /v1/relay/allocate-seq (sequence number allocator), /v1/relay/outbox/ratify (flag-file mediator), and unified SUB /v1/relay/inbox/events (event channel — historically named; carries both inbox and outbox events) endpoints (ADR-0034 §3.6 endpoint table). The daemon does NOT expose an envelope-write endpoint; producing sessions write FS directly.
  • The daemon does NOT own envelope semantics; this ADR does. The daemon is plumbing; the protocol below is the contract.

Foundational property — FS-as-canonical-truth

Per ADR-0028, the hook layer (Bash) is the canonical writer of ~/.flowforge/. The daemon is read-only on those files. This ADR preserves that invariant: brief writes (controller → worker) come from the ACE Claude Code session via FS atomic-write; paste-back writes (worker → controller) come from the worker Claude Code session via FS atomic-write. The daemon never originates envelope writes; it observes and republishes them as events.

This means FS state is the canonical message log. Daemon crash, view crash, or full-system reboot loses no ratified messages — the inbox/outbox tree on disk is the source of truth. Replay-on-restart is trivial: enumerate the tree, project the in-memory RelayInbox / RelayOutbox per ADR-0035, resume.

Decision

FlowForge v1.0 ships a file-based inbox/outbox controller-user relay protocol rooted at ~/.flowforge/repos/<repo-id>/workers/<W-id>/. Each worker has an inbox/ (controller → worker) and outbox/ (worker → controller) directory. Each message is a monotonic-sequenced pair of files: a raw-markdown brief (<seq>.brief for inbox or <seq>.json for outbox) plus a metadata sidecar (<seq>.brief.meta.json). Ratification state is encoded as zero-byte flag files (<seq>.ratified, <seq>.rejected, <seq>.edited) — atomic to create, idempotent to retouch, durable across crashes. Sequence numbers are per-(worker, direction) monotonic, never reused. The daemon owns the allocator (single source of truth for the next <seq>); the producing session (controller-side ACE for inbox, worker-side Claude Code for outbox) calls the allocator endpoint, receives an integer, and writes the FS file itself. This preserves the ADR-0028 invariant that the daemon is read-only on ~/.flowforge/ (architect.md §3.7) — the daemon mediates allocation and observation, never originates envelope writes. All writes are tmp-rename atomic (<path>.tmpfsyncrename) so readers never observe half-state. The flowforge-daemon watches the tree via inotify/fsnotify and pushes controller.brief_proposed and worker.paste_back events on the daemon socket (ADR-0034 §3.6) so subscribed TUI surfaces render new traffic without polling. Founder ratification happens via keystroke (y / n / e) in any TUI controller surface — the daemon translates the keystroke into a <seq>.ratified flag-file touch (zero-byte flag files are daemon-originated by design — they ARE the founder-keystroke mediation surface, intentionally distinct from envelope-body writes). The single exception to daemon-read-on-envelope-body is /edit: when the founder presses e, the daemon opens $EDITOR, captures the edited brief content on save, and rewrites <seq>.brief atomically before creating <seq>.edited (per architect.md §7.4 line 1166 — canonical). This exception is narrow and founder-keystroke-mediated; it is not a general envelope-write capability. The worker's polling shell (inside the Claude Code session) detects the flag and injects the brief body as a user-turn. Parallelism-native slash commands /ratify all and /assign are first-class, designed for fleet-scale decisioning at N=11 and N=25. The protocol uses Go stdlib + Bash exclusively; zero npm, zero node, zero new dependencies beyond what ADR-0034 already establishes.

This is Option A from §"Options considered" below. Options B (unix-socket-direct worker→daemon) and C (hybrid FS-as-truth + socket-for-notify) are rejected for v1.0; Option C remains a viable v1.1+ optimization if push-latency becomes the operational bottleneck.

Options considered

Per-worker directories under ~/.flowforge/repos/<repo-id>/workers/<W-id>/ hold the inbox/outbox tree. Briefs are raw markdown + JSON metadata sidecars. Ratification is a zero-byte flag file. The daemon watches via inotify and pushes events to the TUI; the worker's polling shell detects flag files and injects briefs.

~/.flowforge/repos/<repo-id>/workers/
├── W1/
│   ├── inbox/                       (controller → worker)
│   │   ├── 0001.brief               (raw markdown — 15-section dispatch brief)
│   │   ├── 0001.brief.meta.json     (metadata sidecar — JSON)
│   │   ├── 0001.ratified            (zero-byte flag; founder pressed `y`)
│   │   ├── 0002.brief
│   │   ├── 0002.brief.meta.json
│   │   ├── 0002.rejected            (zero-byte flag; founder pressed `n`)
│   │   └── 0003.brief               (pending — no flag yet)
│   ├── outbox/                      (worker → controller)
│   │   ├── 0001.json                (paste-back payload — JSON)
│   │   └── 0002.json
│   └── archive/                     (per-worker archival — quarterly rollover)
│       └── 2026-05/
└── W2/
    ├── inbox/
    ├── outbox/
    └── archive/
        └── 2026-05/                 (per-worker archival — quarterly rollover)

Pros

  • Crash-recoverable. FS is canonical; daemon restart enumerates the tree and resumes from sidecar flags. No in-memory queue to lose; nothing volatile in the message path.
  • tail -f-debuggable. Founder can cat ~/.flowforge/repos/<id>/workers/W3/inbox/0007.brief to read what the controller sent; ls workers/*/inbox/*.ratified to count ratifications; tail -f workers/W3/outbox/*.json to watch paste-backs in real time. This matters during dogfood and during customer support.
  • Zero new dependencies beyond ADR-0034. Go stdlib (encoding/json, os, path/filepath, time) + Bash. Stone-rule clean (project_zero_npm_constraint.md).
  • Idempotent replay. Sequence numbers are the wire-level unique key; flag-file touch is safe to repeat (presence/absence is the only signal). A daemon retry or a TUI re-render never produces duplicate downstream effects.
  • Symmetric with existing patterns. ADR-0023 event-log uses FS-append; ADR-0028 hooks write ~/.flowforge/ via FS. This protocol fits the established mental model.
  • Cross-tmux-session universal. Worker processes are spawned by tmux; they do not have a clean way to discover a Unix socket. FS paths are universal and require no socket-discovery handshake inside the worker's Claude Code session.
  • Per-worker partition. Each worker owns its own inbox/outbox tree under its W-id; concurrent fleet operations cannot collide on a shared global queue. At N=25 there is no contention on a single hot directory.
  • Atomic-write semantics. <path>.tmpfsyncrename is a POSIX-guaranteed atomic visibility step. Readers (daemon watcher, founder cat) never observe a half-written file.

Cons

  • Push latency is bounded by inotify event delivery + daemon dispatch (typically <10ms on Linux; <30ms on macOS via fsnotify). Negligible for human-keystroke cadence but a theoretical ceiling.
  • Inotify exhaustion (fs.inotify.max_user_watches) can cause the daemon to fall back to 1Hz polling (per ADR-0034 §10c failure modes). At N=25 with 4 repos this is 100 watched directories — well below the default 8192 limit.
  • Two-file-per-message overhead (brief body + metadata sidecar). Mitigated by the atomic-write contract: both files MUST land before the daemon publishes the event.
  • No native fan-out: a brief targets exactly one worker. Multi-worker dispatch (e.g., /ratify all) is implemented by iterating per-worker, not by a single broadcast file. Acceptable; iteration cost is O(N) inboxes.

Performance budget (N=11, fleet of 11 workers under load)

Metric p95 budget Source
Brief write (atomic tmp-rename + fsync) <30ms local SSD
Daemon FS-watch event delivery <10ms inotify
Daemon push-event to subscribed TUI <30ms ADR-0034 §10b
Worker polling-shell flag-detection latency <500ms 2Hz poll
End-to-end keystroke → worker turn injection <750ms sum p95

Failure modes

  • Daemon down: FS state preserved; on daemon restart, watcher re-arms; pending pre-ratified briefs are visible on first enumeration. No messages lost.
  • TUI surface crashes: daemon and worker unaffected; on TUI re-launch it re-subscribes and re-renders from the daemon's projection.
  • Worker session dies before reading ratified flag: flag persists on disk; on next worker turn the shell loop detects the flag and proceeds.
  • Disk full: brief write fails atomically (tmp file exists, rename does not occur); controller surfaces this as a transient error and prompts retry.
  • Concurrent writers: prevented by per-(worker, direction) monotonic sequence number assignment; the daemon owns the sequence allocator (single-threaded counter per worker).
  • Half-written metadata sidecar: the daemon publishes the event ONLY after both <seq>.brief and <seq>.brief.meta.json are durably visible; subscribers never see a brief without its metadata.

Customer-install-premise alignment (ADR-0025): clean — all paths under ~/.flowforge/repos/<repo-id>/workers/; zero touch outside ~/.flowforge/.

Stone-rule alignment (project_zero_npm_constraint.md): clean — Go stdlib + Bash; no npm, no node, no JS/TS runtime, no new package manager surface.

Verdict: ACCEPTED — founder-ratified master plan D4 (2026-05-13). The operational moat of TUI The Bomb v1.0.

Option B — Unix-socket-direct worker→daemon (REJECTED)

The worker Claude Code session opens a direct connection to ~/.flowforge/run/flowforge.sock (ADR-0034) and exchanges messages with the daemon via the length-prefixed JSON wire format. No FS message log; the daemon's in-memory state is canonical.

Pros

  • Lower latency (no FS roundtrip; ~5ms vs ~30ms).
  • Single message channel (socket only) — no two-file metadata pattern.
  • Native push semantics with no inotify dependency.

Cons (load-bearing rejection)

  1. Worker processes have no clean socket-discovery handshake inside Claude Code. Workers are spawned by tmux; the Claude Code session has no native mechanism to discover and connect to a daemon socket without bespoke shell glue. FS paths are universal — every shell, every language, every editor can read them.

  2. Daemon becomes the canonical state-holder for messages. A daemon crash loses any ratified-but-not-yet-acknowledged briefs. This violates the FS-as-canonical-truth invariant established by ADR-0028 and breaks the BLUEPRINTS-SACRED contract that the hook layer is the canonical writer.

  3. No tail -f / cat debuggability. During dogfood and customer support, the founder cannot inspect what was sent. The only state is in the daemon's RAM.

  4. Replay-on-restart is non-trivial. Without an on-disk log, daemon restart must reconstruct state from… nothing. A separate WAL would have to be added — at which point we have FS-as-truth with extra steps.

  5. Conflates daemon roles. ADR-0034 positions the daemon as a state aggregator + view-renderer. Making it the relay endpoint conflates concerns and creates a non-FS source of truth.

Verdict: REJECTED. Per architect.md §11.2, FS-based is debug-friendly, symmetric with the existing event-log pattern (ADR-0023), and works across tmux sessions without coordinating socket connections inside Claude Code worker processes.

Option C — Hybrid (FS-as-truth + socket-for-notify) (DEFERRED to v1.1+)

Use FS inbox/outbox as in Option A for canonical truth, but ALSO have workers open a notify-only socket connection to the daemon to receive push-notifications (rather than relying on polling). The FS write remains the source of truth; the socket is a latency optimization.

Pros

  • Lower worker-side flag-detection latency (~30ms via socket vs ~500ms via 2Hz polling).
  • Preserves FS-as-truth invariant.

Cons

  • Adds a socket discovery + connection lifecycle inside the worker Claude Code session — exactly the complexity Option B fails on.
  • The latency optimization is only relevant if 500ms polling is empirically too slow. Polling at 2Hz is the architect.md §7.4 default and is well-bounded for human-cadence ratification (a founder pressing y does not need sub-500ms turnaround).

Verdict: DEFERRED. Viable v1.1+ optimization if dogfood reveals 2Hz worker-side polling as the operational bottleneck. FS-poll is sufficient for v1.0 at the cadences observed; revisit if the empirical evidence demands. Recording the option here so future revisitors have the rationale chain.

Rationale (anchored to master plan D4)

Per founder ratification 2026-05-13 (MASTER-PLAN.md §"The 8 founder decisions" D4):

  1. FS-as-canonical-truth preserves the foundational invariant. ADR-0028 establishes the hook layer as canonical writer of ~/.flowforge/. This protocol fits that pattern; the daemon never originates messages, only observes and republishes. No new architectural class to maintain.

  2. Crash-recoverability is non-negotiable at high N. At N=11, a daemon crash that loses 5 pending ratified briefs costs the founder ~10-30 minutes of re-dispatch work. At N=25, it costs an hour. FS-as-truth eliminates this failure class entirely.

  3. tail -f debuggability is essential during dogfood and customer support. The founder must be able to inspect "what did the controller actually send to W3 at 14:25?" from a shell. FS paths support this trivially; socket-only state does not.

  4. Cross-tmux universality. Workers are spawned by tmux and have no canonical socket-discovery handshake. FS paths work from any shell, any session, any tooling — including future non-Claude-Code worker substrates.

  5. Stone-rule alignment. No new dependencies. Go stdlib for daemon-side; Bash for worker-side polling shell. Zero npm/node — clean per project_zero_npm_constraint.md.

  6. Symmetry with ADR-0023 event-log. The existing event-log pattern is FS-append; this protocol is FS-message-tree. The patterns rhyme — same mental model, same debugging tools.

Wire format — canonical specification

Directory layout (per worker)

~/.flowforge/repos/<repo-id>/workers/<W-id>/
├── inbox/                       # controller → worker
│   ├── <seq>.brief              # raw markdown (15-section dispatch brief)
│   ├── <seq>.brief.meta.json    # metadata sidecar (JSON)
│   ├── <seq>.ratified           # zero-byte flag — founder pressed `y`
│   ├── <seq>.rejected           # zero-byte flag — founder pressed `n`
│   └── <seq>.edited             # zero-byte flag — founder edited then accepted
└── outbox/                      # worker → controller
    └── <seq>.json               # paste-back payload (JSON)

<repo-id> is the UUID stored in .git/config flowforge.repoId per ADR-0033. <W-id> is the worker identifier (W1, W2, ..., W25) assigned at session-spawn time per the existing tmux-session-naming convention.

Sequence number contract

  • <seq> is a zero-padded 4-digit monotonic integer per (worker, direction).
  • Width is 4 digits (0001 ... 9999); rollover at 10000 is handled by quarterly archival (see Retention policy below) — in practice no worker reaches 10000 messages before archival.
  • The daemon owns the allocator: a single-threaded per-worker counter that reads the highest existing <seq> on directory init and increments. Two concurrent producers cannot collide because the daemon serializes allocation.
  • Sequence numbers are NEVER reused. A <seq>.rejected brief still consumes its number; the next brief gets <seq>+1.

Why monotonic integers instead of ULIDs: architect.md §7.8.1 specifies sequence numbers; the daemon already serializes allocation through its single-aggregator role; monotonic integers are simpler to debug (ls -1 inbox/ is chronological without sorting) and use no extra dependency. ULIDs were considered (lexicographic-sortable, time-prefix) but offer no benefit over a centralized allocator and add a dependency.

Inbox brief metadata sidecar (canonical schema)

Note (schema additions vs architect.md §7.8): The version and idempotency_key fields below are ADR-0036 additions to the architect.md §7.8 canonical schema. They are flagged here for ADR-0035 (#1265) struct-shape reconciliation. version enables forward-compatible envelope-format evolution; idempotency_key provides a sha256 content-fingerprint for at-least-once retry safety (canonical-form definition in the "Idempotency contract" subsection below). Architect.md remains source of truth for the design intent; this ADR formalizes the wire schema. Any divergence surfaces as a milestone-seal item per BLUEPRINTS-SACRED.

// ~/.flowforge/repos/<repo-id>/workers/W1/inbox/0001.brief.meta.json
{
  "seq": 1,                                     // matches the brief filename
  "version": 1,                                 // envelope schema version (ADR-0036 addition vs architect.md §7.8)
  "kind": "dispatch_brief",                     // see kind enum below
  "submitted_at": "2026-05-13T14:25:00Z",       // RFC3339 UTC
  "controller_session_id": "<ACE-session-uuid>",// the producing controller session
  "target_worker": "W1",                        // matches the directory <W-id>
  "target_ticket": "1192",                      // GitHub issue number (string for portability)
  "expires_at": null,                           // optional auto-expire RFC3339
  "summary": "Ratify Phase-2 cycle 3 dispatch for #1192 merge-seal subcommand",
  "in_reply_to": null,                          // optional <seq> of prior message in chain
  "idempotency_key": "<sha256-base64url>"       // sha256 of canonical-form brief body (ADR-0036 addition vs architect.md §7.8)
}

kind enum (inbox):

  • dispatch_brief — full 15-section dispatch brief (the common case)
  • merge_go — founder authorizes merge-go on a PR
  • redirect — mid-task redirection ("shift focus to #1234")
  • freeform — open-ended controller message

Outbox paste-back payload (canonical schema)

Note (schema additions vs architect.md §7.8): As with the inbox sidecar, version and idempotency_key below are ADR-0036 additions to the architect.md §7.8 outbox schema, flagged for ADR-0035 (#1265) reconciliation. Same rationale: forward-compat versioning + at-least-once retry safety. BLUEPRINTS-SACRED compliance via explicit milestone-seal flag rather than silent extension.

// ~/.flowforge/repos/<repo-id>/workers/W1/outbox/0001.json
{
  "seq": 1,                                     // monotonic per worker outbox
  "version": 1,                                 // envelope schema version (ADR-0036 addition vs architect.md §7.8)
  "kind": "cycle_report",                       // see kind enum below
  "produced_at": "2026-05-13T14:32:18Z",        // RFC3339 UTC
  "worker_id": "W1",                            // matches directory <W-id>
  "ticket_id": "1192",                          // GitHub issue number (string)
  "claude_session_id": "<worker-session-uuid>", // the producing worker session
  "body": "Cycle 3 complete. Reviewer GOLDEN. PR #1192 mergeable. Awaiting merge-go.",
  "pr_number": 1192,                            // optional — present when kind implies a PR
  "next_action": "merge_go",                    // optional hint for controller-side rendering
  "in_reply_to": 12,                            // optional <seq> of inbox brief this responds to
  "idempotency_key": "<sha256-base64url>"       // sha256 of canonical-form body (ADR-0036 addition vs architect.md §7.8)
}

kind enum (outbox):

  • cycle_report — reviewer-cycle status (common case at end of a worker turn)
  • merge_go_request — worker requests merge authorization on a PR
  • blocked — worker is blocked; needs founder/controller intervention
  • freeform — open-ended worker message

Ratification flag-file semantics

Flag file Created by Triggers worker behavior
<seq>.ratified founder press y → daemon writes zero-byte worker polling shell detects flag → reads <seq>.brief body → injects into next worker turn as user-message
<seq>.rejected founder press n → daemon writes zero-byte worker reads body but with rejection annotation; controller re-drafts
<seq>.edited founder press e; $EDITOR opens; on save daemon rewrites <seq>.brief and creates <seq>.edited worker proceeds as if ratified, with edited body

Flag-file invariants:

  • All flag files are zero-byte. Presence/absence is the only signal.
  • Creation is via O_EXCL | O_CREAT (atomic, race-free against concurrent ratification attempts).
  • Workers MUST NOT read <seq>.brief body until they observe a flag file (ratified or edited). A pending brief with no flag is invisible to the worker.
  • Flag creation is idempotent at the FS level (a second touch after the file exists is a no-op).
  • Directionality disambiguation (load-bearing). The .ratified and .read suffixes appear in BOTH the inbox tree AND the outbox tree; disambiguation is DIRECTORY-based, never filename-based. <inbox>/<msgID>.ratified is written by the controller-side Ratify (founder press y → daemon) and signals to the worker that its inbound dispatch envelope was ratified; <outbox>/<msgID>.ratified is written by the worker-side MarkRatified and signals to the controller that the worker has acknowledged round-trip completion on its paste-back. Both .ratified sidecars MAY co-exist for the same <msgID> simultaneously (one per tree) — this is the canonical wire-format, not a collision; the same symmetric model applies to .read (currently outbox-only via MarkRead, reserved-symmetric for future inbox-side use).

Idempotency contract

Two layers of idempotency:

  1. Wire-level uniqueness: <seq> is the canonical wire-level key. The daemon's allocator guarantees no duplicate <seq> per (worker, direction). Producers MUST use the daemon's allocator; direct FS writes that bypass the allocator are a contract violation.

  2. Content fingerprint: idempotency_key is sha256(canonical_form(body)) base64url-encoded. Canonical form for body:

    • For inbox briefs: read the raw <seq>.brief file bytes, normalize line endings to LF, ensure UTF-8.
    • For outbox JSONs: marshal the body field as a UTF-8 string with LF line endings; sha256 the resulting bytes.
    • Algorithm: base64url(sha256(canonical_body_bytes)).

    If a retry produces a brief with the same idempotency_key as a recent prior message (within a 5-minute window per worker direction), receivers MAY treat the second as a no-op replay. This is an optimization, not a correctness requirement — the <seq> uniqueness is sufficient for correctness.

Atomic-write contract (mandatory)

All FS writes in this protocol MUST follow the tmp-rename pattern:

1. Write content to `<target-path>.tmp`
2. fsync(<target-path>.tmp)
3. rename(<target-path>.tmp, <target-path>)

For the brief + metadata sidecar pair, the daemon publishes the controller.brief_proposed event ONLY after BOTH files are durably visible. Implementation: write metadata first, then brief; the watcher arms on the brief file (post-rename); when it fires, both files are guaranteed visible.

For outbox paste-backs: single-file atomic write; watcher arms on <seq>.json.

Half-written state is never visible to readers. Daemon watcher events on .tmp files are ignored.

Slash command syntax + behavior

Slash commands route through POST /v1/controller/dispatch (ADR-0034 §3.6 — though note ADR-0036's relay-specific endpoints /v1/relay/allocate-seq and /v1/relay/outbox/ratify are the canonical surfaces). The slash-command palette (Ctrl+P in the cockpit) is a shortcut UI. Under the hood:

  • For inbox brief writes (/assign, /redirect, /merge, etc.): the slash command runs inside the ACE controller session. It calls the daemon's /v1/relay/allocate-seq endpoint to receive a <seq>, then performs the FS atomic-write itself (<seq>.brief.meta.json then <seq>.brief, each tmp-rename). The daemon NEVER writes envelope content; it only allocates the sequence number and observes the resulting file via inotify/fsnotify, then publishes controller.brief_proposed. This preserves the ADR-0028 / architect.md §3.7 daemon-read-only invariant on envelope bodies.
  • For ratification flag writes (/ratify, /reject, /edit): the daemon performs touch <seq>.{ratified,rejected,edited} (O_EXCL|O_CREAT). Flag files are zero-byte and carry no envelope content; their FS-write origin is the daemon by design (the daemon mediates founder keystrokes from any TUI surface into atomic flag creation), and they remain outside the envelope-content scope of the read-only invariant.

/ratify all — the parallelism-native fleet ratification

Synopsis: /ratify all [--scope=<worker-glob>]

Behavior: For every inbox brief across all worker directories where:

  • A <seq>.brief file exists (the body)
  • A <seq>.brief.meta.json file exists (the metadata)
  • NEITHER <seq>.ratified NOR <seq>.rejected NOR <seq>.edited exists (the brief is pending)

...the daemon performs touch <seq>.ratified (atomic O_EXCL|O_CREAT).

Output (per ratified brief):

RATIFIED #<ticket> <summary> → <W-id>

Exit status: integer count of newly-ratified briefs.

--scope parameter: optional glob match against <W-id>. Examples:

  • /ratify all --scope=W1 — ratify only W1's pending briefs
  • /ratify all --scope=W{1,2,3} — ratify W1, W2, W3
  • /ratify all --scope=W* — equivalent to no scope filter (all workers)

Batch atomicity: best-effort per-file; no global lock. A failure on one brief (e.g., concurrent ratification) does not block others. Failures are printed to stderr; the exit count reflects successful ratifications only.

Why this is parallelism-native: at N=11, after a focused work block the founder typically has 3-7 pending briefs across the fleet. Per-worker ratification = 3-7 pane-switches × ~30s = 1.5-3.5 minutes. /ratify all = one keystroke. This is the master plan D4 ROI claim made concrete.

/assign <ticket> <worker> — the parallelism-native dispatch shortcut

Synopsis: /assign <ticket-ref> <worker-session> [--brief=<file>|--inline=<one-liner>]

Behavior: Generate a new inbox brief for <worker-session>:

  1. The ACE controller session (where the slash command runs) calls POST /v1/relay/allocate-seq?worker=<worker-session>&direction=inbox.
  2. Daemon returns the next <seq> integer (single-threaded allocator per (worker, direction)).
  3. The ACE session writes <seq>.brief.meta.json (atomic tmp-rename) with kind=dispatch_brief, target_ticket=<ticket-ref>, target_worker=<worker-session>.
  4. The ACE session then writes <seq>.brief body sourced from --brief file or --inline string (or stdin if neither provided), via atomic tmp-rename.
  5. The daemon's FS-watcher observes the <seq>.brief file appearing (post-rename) and — after confirming the sidecar is also visible — publishes controller.brief_proposed on the unified event channel (SUB /v1/relay/inbox/events per ADR-0034 §3.6 — the channel name is historical and carries both inbox brief_proposed and outbox paste_back events).

The daemon never touches envelope content for inbox briefs. This preserves the ADR-0028 / architect.md §3.7 read-only invariant.

Output:

ASSIGNED <ticket-ref> → <worker-session> (seq=<NNNN>)

Validation:

  • Refuses if <worker-session> is not in the live-session set (queried from daemon's worker-state cache; absent worker → non-zero exit + clear stderr message).
  • Refuses if <ticket-ref> is malformed (must match #?\d+ for GitHub issue refs).
  • If --brief=<file> provided, file MUST exist and be readable; daemon does not auto-create.

Why this is parallelism-native: at N=11, the founder is fleet-orchestrating — picking which worker takes which unclaimed-queue ticket. Per-worker tmux-pane-switching + manual paste = the relay tax. /assign #1244 W7 from the cockpit = one line, one keystroke commit. Combined with /ratify all, the founder dispatches N workers in N keystrokes from a single cockpit.

Discovery / auxiliary commands

These are mentioned for completeness; their full specification lives in ADR-0034's daemon endpoint table.

Command Behavior
/ratify <W-id> Ratify the most recent pending brief for <W-id> (single-worker shortcut).
/reject <W-id> [<seq>] Reject brief — most recent if <seq> omitted; specific if provided.
/edit <W-id> [<seq>] Open $EDITOR on the brief body; on save, daemon writes <seq>.edited.
/inbox [<W-id>] List pending inbox briefs (no flag) — fleet-wide if <W-id> omitted.
/outbox [<W-id>] List recent outbox paste-backs — fleet-wide if <W-id> omitted.
/redirect <W-id> <text> Send a kind=redirect brief to <W-id> with body <text>.
/merge <pr-number> Send a kind=merge_go brief to the worker owning that PR.
/relay-gc Trigger immediate archival sweep (normally daemon-scheduled; archives at quarterly boundaries).

Sequence diagrams

Dispatch → ratify → consume flow (the common case)

sequenceDiagram
    participant Controller as ACE (controller session)
    participant Daemon as flowforge-daemon
    participant FS as ~/.flowforge/repos/<id>/workers/W1/
    participant TUI as TUI cockpit
    participant Founder
    participant Worker as W1 (worker Claude session)

    Controller->>Daemon: POST /v1/relay/allocate-seq?worker=W1&direction=inbox
    Daemon-->>Controller: { seq: 7 }
    Controller->>FS: write inbox/0007.brief.meta.json.tmp + fsync + rename
    Controller->>FS: write inbox/0007.brief.tmp + fsync + rename
    FS-->>Daemon: inotify event (inbox/0007.brief visible)
    Daemon-->>TUI: SUB /v1/relay/inbox/events { brief_proposed, W1, seq=7 }
    TUI-->>Founder: render brief in controller panel; "Ratify? [y/n/e]"
    Founder->>TUI: press `y`
    TUI->>Daemon: POST /v1/relay/outbox/ratify { W1, seq=7 }
    Daemon->>FS: touch inbox/0007.ratified (O_EXCL|O_CREAT)
    Daemon-->>TUI: ack
    Note over Worker,FS: worker polling shell (2Hz) detects 0007.ratified
    Worker->>FS: read inbox/0007.brief body
    Worker->>Worker: inject body as user-turn message
    Worker->>Daemon: POST /v1/relay/allocate-seq?worker=W1&direction=outbox
    Daemon-->>Worker: { seq: 1 }
    Worker->>FS: write outbox/0001.json.tmp + fsync + rename
    FS-->>Daemon: inotify event (outbox/0001.json visible)
    Daemon-->>TUI: SUB /v1/relay/inbox/events { paste_back, W1, seq=1 }
    TUI-->>Founder: render paste-back in controller panel
    Note over Daemon,TUI: Both brief_proposed and paste_back are delivered through the unified SUB /v1/relay/inbox/events channel per ADR-0034 §3.6 (the channel name is historical — it carries both inbox and outbox events).

/ratify all fleet-batch flow

sequenceDiagram
    participant Founder
    participant TUI
    participant Daemon as flowforge-daemon
    participant FS as ~/.flowforge/repos/<id>/workers/

    Founder->>TUI: type `/ratify all` + Enter
    TUI->>Daemon: POST /v1/relay/outbox/ratify { scope=all }
    loop for each worker W in fleet
      Daemon->>FS: enumerate W/inbox/*.brief.meta.json (no flag)
      loop for each pending <seq>
        Daemon->>FS: touch W/inbox/<seq>.ratified (O_EXCL|O_CREAT)
        Daemon-->>TUI: stream "RATIFIED #<ticket> <summary> → W"
      end
    end
    Daemon-->>TUI: final exit count
    TUI-->>Founder: render fleet ratification summary
    Note over FS: each worker's polling shell detects its own .ratified flags

Concurrency, ordering, crash recovery

Ordering model

  • Sequence numbers are per-(worker, direction) monotonic. Within a single worker's inbox or outbox, <seq> order IS chronological order. Lexicographic sort of zero-padded <seq> filenames matches chronological sort.
  • Cross-worker ordering is unspecified by design — the fleet is parallel; events on W1 and W3 have no canonical happens-before relationship.
  • submitted_at / produced_at RFC3339 timestamps provide best-effort cross-worker ordering for human inspection but are NOT load-bearing for protocol correctness.

Concurrency model

  • The daemon owns sequence allocation. A single-threaded counter per (worker, direction) prevents concurrent allocator-side collisions.
  • Two TUI surfaces ratifying the same brief (e.g., cockpit + floating overlay) both call POST /v1/relay/outbox/ratify → daemon attempts touch <seq>.ratified with O_EXCL|O_CREAT → exactly one wins; the loser's call returns "already ratified" idempotent success.
  • Two workers writing outbox simultaneously: they have different <W-id> directories, so no contention.
  • Multiple subscribers to SUB /v1/relay/inbox/events: the daemon broadcasts; subscriber-side idempotency is handled by the <seq> key (a TUI rendering the same event twice is a UI bug, not a protocol bug).

Crash recovery semantics

Failure Recovery
Daemon crash On restart, daemon re-enumerates all workers/*/{inbox,outbox}/ trees; reads max <seq> per direction to resume allocator; replays unconsumed flag files. No messages lost.
TUI crash On re-launch, TUI reconnects to daemon; re-subscribes to SUB /v1/relay/inbox/events; daemon resends current state.
Worker session crash On worker re-attach (tmux session preserved), worker polling shell resumes; detects any unread .ratified flags from before the crash.
Founder TUI re-launch No state lost; daemon and FS preserve everything; cockpit re-renders from current state.
Power loss Atomic-rename + fsync ensures no half-written files visible. Worst case: the most recent in-flight tmp file is orphaned; daemon GC sweeps .tmp files older than 60s on startup.
Disk full Atomic write fails at rename (tmp file exists, target does not); controller surfaces transient error; founder is prompted to free space or change repo location.
Inotify watch limit Per ADR-0034 §10c, daemon falls back to 1Hz polling of workers/*/{inbox,outbox}/. Protocol unchanged; only event-delivery latency increases.

Cross-ADR coordination

  • ADR-0034 (Daemon): owns the FS-watcher and the /v1/relay/* endpoint table; daemon is plumbing, this ADR is contract. Reference: ADR-0034 §3.6 endpoints, §10b performance budgets.
  • ADR-0035 (State schema, #1265): defines the in-memory RelayInbox / RelayOutbox Go structs that project the FS envelopes specified here. The wire format in this ADR is canonical; the in-memory projection in ADR-0035 is derived. ADR-0035 owns the struct shapes; this ADR owns the JSON schemas. If a field appears here but not in ADR-0035 (or vice versa), the gap is a milestone-seal item.
  • ADR-0037 (Schedule view, #1261): consumes kind=dispatch_brief envelopes to render the NOW/NEXT/QUEUED strips per swim-lane. The target_ticket and target_worker fields in the inbox metadata sidecar are the join keys.
  • ADR-0028 (instructions-loaded hook): hook layer is the canonical writer of ~/.flowforge/; this protocol fits that pattern (controller and worker sessions, not daemon, originate FS writes).
  • ADR-0023 (event-log): same FS-as-truth philosophy; this protocol extends the pattern from append-only logs to inbox/outbox message trees.

Stone-rule compliance

Per project_zero_npm_constraint.md (founder direction 2026-05-09 "written on stone"):

Layer Stone-rule alignment
Daemon-side envelope handling Go stdlib (encoding/json, os, path/filepath, time, crypto/sha256, encoding/base64). No external Go modules required beyond ADR-0034's existing surface.
Worker-side polling shell Bash. Reads flag files via test -f; reads brief body via cat.
Slash-command parser Go (in the TUI binary). No external CLI framework.
Metadata sidecar serialization Go encoding/json standard library.
Idempotency key computation crypto/sha256 + encoding/base64 standard library.
Atomic-write primitive os.OpenFile + os.Rename standard library.
FS-watcher golang.org/x/sys/unix inotify (Linux) / fsnotify (cross-platform) — same dependency surface as ADR-0034.

ZERO at runtime: node, npm, npx, pnpm, yarn, package.json, package-lock.json, node_modules/.

Pre-merge grep gate (implementation tickets MUST verify):

grep -rE 'npm|node_modules|package\.json|pnpm|yarn|npx' \
    --include='*.go' --include='*.sh' --include='*.md' \
    cmd/flowforge-daemon/ internal/daemon/relay/ documentation/architecture/ADR-0036*

Exception: occurrences in prohibition prose (e.g., "we do NOT depend on npm") are allowed. Any reference to node/npm/etc. as a runtime call site fails the gate.

Validation plan

This ADR is documentation; formal RED/GREEN/REFACTOR does not apply. The validation plan below specifies how the recommended option is empirically validated post-implementation. Each item maps to an acceptance criterion for the relay-MVP implementation ticket (TUI The Bomb Week 2 Day 5 per MASTER-PLAN.md).

Smoke test harness (acceptance gate)

# scripts/test/relay-smoke.sh (sketch)
# 1. Spawn N=11 synthetic workers (tmux sessions with stub polling shells)
# 2. Start daemon
# 3. Connect 1 TUI client; subscribe to /v1/relay/inbox/events
# 4. Controller writes 11 dispatch briefs (one per worker) via /v1/relay/inbox
# 5. Assert: all 11 controller.brief_proposed events received within 100ms
# 6. TUI calls /v1/relay/outbox/ratify scope=all
# 7. Assert: all 11 <seq>.ratified flag files created
# 8. Each worker stub detects its flag, writes outbox paste-back
# 9. Assert: all 11 worker.paste_back events received within 1s
# 10. Kill daemon mid-operation; assert FS state preserved; restart; assert resume

Acceptance: all 10 assertions pass on the FF dogfood machine and on a clean Ubuntu 22.04 VM.

Performance benchmarks

Numeric floors per architect.md §10b + this ADR's Option A budget:

Metric p95 budget p99 budget
Brief write (atomic tmp-rename + fsync + metadata sidecar) <30ms <100ms
Daemon FS-watch event delivery (inotify) <10ms <50ms
End-to-end controller-write → TUI render <100ms <250ms
/ratify all at N=11 <500ms <1s
/ratify all at N=25 <1s <2s
Worker polling-shell flag-detection <500ms <1s
End-to-end keystroke → worker turn injection <750ms <1.5s

Benchmark harness lives in scripts/test/relay-bench.sh. CI runs the harness on every PR touching cmd/flowforge-daemon/relay/ or internal/daemon/relay/.

Failure-mode injection

Injection Expected behavior
Daemon killed mid-brief-write Brief tmp file remains; on restart, daemon GC sweeps orphan tmp files older than 60s
Daemon killed mid-ratification Flag file present-or-absent atomically; on restart, daemon enumerates and resumes
Concurrent /ratify all from two TUI surfaces Both succeed idempotently; exactly one flag-create per brief winning O_EXCL race
Worker polling shell killed mid-flag-read Flag persists; worker re-attach detects and proceeds
Disk full at ~/.flowforge/repos/<id>/ Atomic rename fails; controller sees transient error; nothing half-written
<seq>.brief exists but .meta.json absent Daemon does NOT publish event; logs corruption warning; archives the orphan brief
Two concurrent producers (bypassing daemon allocator) Detected by <seq> collision; second write fails O_EXCL on rename; daemon logs contract violation
Inotify exhaustion Daemon falls back to 1Hz polling per ADR-0034 §10c; protocol unchanged; latency budget reverts to polling-bounded

Each row above is an integration test under scripts/test/relay-failures/.

Retention policy

File class Retained Archived
Recent inbox/outbox (last 100 messages per worker) live none
Older inbox/outbox messages move to workers/<W-id>/archive/<YYYY-MM>/...
Ratification flag files move with their corresponding brief quarterly
Orphan .tmp files older than 60s sweep deleted on daemon startup

Archival is a daemon-side scheduled task: the daemon may evaluate the archival predicate on a frequent cadence (e.g., weekly cron) but ACTIONS — moving inbox/outbox files into archive/<YYYY-MM>/ — occur at quarterly boundaries (the retention boundary). The /relay-gc slash command triggers immediate evaluation + action. Archive directories preserve sequence ordering for retroactive audit; the controller-history.jsonl in ADR-0034 indexes archived briefs by date + ticket-ID for fast search.

Rule #38 compliance

This protocol does NOT propose:

  • Any auth-token files under .agent-auth/.
  • Any FLOWFORGE_BYPASS=* or *_AUTH_TOKEN env vars.
  • Any hook-disable flags.
  • Any configuration overrides that bypass FF's existing security boundaries.
  • Any network exposure — all FS paths are local-only under ~/.flowforge/repos/.

Security properties at v1.0:

  • All envelope files inherit Unix ownership of the founder's UID; per ADR-0025 install layout, ~/.flowforge/ is 0700 and contents are 0600 / 0700 as appropriate.
  • The daemon socket (ADR-0034) enforces 0600 + peer-UID match; only the founder's processes can issue ratify RPCs.
  • Brief bodies are markdown text only. No auth tokens, no credentials, no secrets in envelope bodies. This is a soft contract — the daemon does not scan envelope content — but is documented as a usage guideline.
  • idempotency_key is computed over body content only, NOT over headers. Avoids leaking session IDs if envelopes are ever exported for debugging.
  • Garbage collection (/relay-gc + scheduled daemon job at quarterly boundaries) is a Class-B operation per the Rule #38 risk-graded rescope (file movement into ~/.flowforge/repos/<id>/workers/<W-id>/archive/; quarterly-aged files only).
  • The protocol does NOT proxy or relay decisions through Anthropic's supervisor (~/.claude/daemon/) per ADR-0034 wrap posture. Maestro pattern (founder + controller) is the decision authority.

Out of scope (explicit non-deliverables)

To prevent scope creep, the following are explicitly NOT part of this ADR:

  • Daemon FS-watch implementation — lives in ADR-0034 + its implementation ticket. This ADR specifies the protocol; the daemon ticket specifies the inotify/fsnotify integration.
  • TUI slash-command parser implementation — lives in the TUI cockpit implementation ticket (TUI The Bomb Week 2 Day 5 per MASTER-PLAN.md). This ADR specifies the slash-command surface; the impl ticket owns the parser.
  • RelayInbox / RelayOutbox Go struct definitions — owned by ADR-0035 (#1265). This ADR specifies the wire format; ADR-0035 specifies the in-memory projection.
  • Web dashboard relay surface — v1.1+ per ADR-0040, out of v1.0 cascade-launch scope. The dashboard will consume the same protocol via HTTPS-proxied daemon API; no protocol changes anticipated.
  • Multi-host relay — single-machine only for v1.0. Multi-host cross-device sync is a v1.1+ Supabase concern (per architect.md §8.3) and may layer on top of this protocol without protocol changes.
  • Per-user authentication on the relay — v1.0 is single-user (Unix permissions + peer-UID match). v1.1 enterprise tier may add per-user tokens.
  • Custom retention policies — v1.0 ships fixed 100-message live retention + quarterly archival. v1.2 may open user-configurable retention via TUI config.
  • Encrypted envelope bodies — v1.0 envelopes are plain UTF-8. v1.1+ may add at-rest encryption if compliance requirements emerge.

Downstream / Dependencies

This ADR unlocks

  • TUI cockpit relay surfaces (A + B + C from architect.md §7.2) — inline controller panel, dedicated --view controller layout, and floating Ctrl+\ overlay. All three subscribe to SUB /v1/relay/inbox/events.
  • /ratify all and /assign slash commands — the parallelism-native fleet operations that translate this protocol's keystroke economy into the master plan D4 ROI.
  • Worker-side polling shell — Bash script in the worker's Claude Code session that detects flag files and injects ratified brief bodies as user-turn messages.
  • Controller-side dispatch — ACE Claude Code session allocates a <seq> via the daemon's POST /v1/relay/allocate-seq endpoint, then writes the brief + metadata sidecar directly to ~/.flowforge/repos/<id>/workers/<W-id>/inbox/ via atomic tmp-rename. The dispatch-brief 15-section template (feedback_dispatch_brief_template.md) becomes the body content.
  • controller-history.jsonl indexing (architect.md §7.7) — searchable via /search slash command + indexed by date and ticket-ID.
  • Schedule-view NOW/NEXT/QUEUED strips (ADR-0037) — consumes kind=dispatch_brief metadata to render swim-lane content.

This ADR blocks on

  • ADR-0034 — must merge first (it did, on 2026-05-13 in PR #1247 at commit 5dc6d689). ✅
  • ADR-0035 — in flight on ticket #1265. May ratify in parallel with this ADR; the wire format here is canonical regardless of ADR-0035 ratification order.

Dependency chain (visual)

ADR-0034 (daemon — MERGED 2026-05-13)
   ├─→ ADR-0035 (state schemas — in flight #1265)
   ├─→ ADR-0036 (relay protocol — THIS ADR)
   │    ├─→ TUI cockpit relay surfaces (Week 2 Day 5)
   │    ├─→ /ratify all + /assign slash commands
   │    └─→ controller-history.jsonl indexing
   └─→ ADR-0037 (schedule-view — in flight #1261)
        └─→ NOW/NEXT/QUEUED strips consume ADR-0036 envelopes

Consequences

Positive

  • Relay tax collapses to O(keystrokes) — the master plan D4 super-linear ROI claim is structurally encoded. At N=11, ~5.5h/day saved; at N=25, >12h/day saved.
  • FS-as-canonical-truth invariant preserved — symmetric with ADR-0023, ADR-0028; no new architectural class.
  • Crash-recoverable across daemon, TUI, worker, and full-system failures — no message-loss class introduced.
  • tail -f / cat debuggability — operationally essential during dogfood and customer support.
  • Cross-tmux universality — workers don't need socket discovery; any shell, any session, any future worker substrate can participate.
  • Zero new dependencies beyond ADR-0034 — stone-rule clean.
  • /ratify all + /assign are parallelism-native — they exist because at N=11 fleet-scale decisioning IS the operating mode; individual-worker addressing would itself be a tax.
  • Web dashboard substrate (v1.1, ADR-0040) is pre-paid — same protocol over HTTPS-proxied daemon API.

Negative

  • Two-file-per-message overhead (brief body + metadata sidecar) — bounded by atomic-rename contract; never visible to readers as half-state.
  • Per-worker monotonic sequence number rollover at 9999 — addressed by quarterly archival; in practice unreachable at human cadence.
  • Worker-side polling shell at 2Hz — adds ~500ms p95 to keystroke→worker-turn latency. Acceptable for human-cadence ratification; Option C deferred-hybrid socket-for-notify can revisit if empirically bottlenecked.
  • Inotify watch consumption at N=25 × 4 repos × 2 dirs = 200 watches — well below default 8192 limit, but documented as an operational consideration.
  • No native fan-out/ratify all iterates per-worker. O(N) work, but O(N) keystrokes saved; net super-linear win.

Neutral

  • The protocol is stateless across daemon restarts in the sense that the daemon's in-memory state is a projection of FS state. Restart is replay.
  • The daemon does NOT modify envelope content — it only mediates allocator, watcher, and event-bus duties. The integrity of the wire format is owned by the producer (controller or worker).
  • BLUEPRINTS-SACRED: architect.md §7 is the source of truth; this ADR formalizes (not re-litigates) that design. Any future divergence surfaces as a gap-analysis ticket per the Milestone Quality Seal Rule.

Alternatives considered (summary)

Option Verdict Reason
A — FS inbox/outbox + .ratified sidecar flag ACCEPTED Crash-recoverable; debuggable; zero new deps; cross-tmux universal; FS-as-truth invariant preserved
B — Unix-socket-direct worker→daemon Rejected Worker socket-discovery is brittle; daemon becomes canonical state-holder; no tail -f debugging; violates ADR-0028
C — Hybrid FS-as-truth + socket-for-notify Deferred to v1.1+ Latency optimization not justified by v1.0 empirical evidence; revisit if 2Hz polling proves bottleneck

References

  • Master plan: documentation/2.0/strategy/tui-the-bomb/MASTER-PLAN.md (§"The 8 founder decisions" D4, ratified 2026-05-13)
  • Architect brief: documentation/2.0/strategy/tui-the-bomb/architect.md §7 entire (controller-user interaction protocol — load-bearing source of truth), §10.3 (this ADR's scope spec), §11.2 (founder ratification surface — Choice 2)
  • Designer brief: documentation/2.0/strategy/tui-the-bomb/designer.md §3 (visual ground truth — 28 wireframes; controller-panel surface affordances)
  • ADR-0034: documentation/architecture/ADR-0034-fleet-aggregation-substrate.md (daemon — companion; merged 2026-05-13 PR #1247 commit 5dc6d689)
  • ADR-0035 (in flight): documentation/architecture/ADR-0035-tui-state-storage-schema.md (state schemas — ticket #1265)
  • ADR-0037 (in flight): documentation/architecture/ADR-0037-tui-schedule-view-rendering.md (schedule view — ticket #1261)
  • ADR-0028: documentation/architecture/ADR-0028-instructions-loaded-hook.md (hook layer as canonical writer of ~/.flowforge/)
  • ADR-0025: documentation/architecture/ADR-0025-single-install-architecture.md (install layout)
  • ADR-0033: documentation/architecture/ADR-0033-global-install-architecture.md (UUID repo-id)
  • ADR-0023: event-log pattern (FS-as-truth precedent for this protocol)
  • Stone-rule: feedback/project_zero_npm_constraint.md (founder direction 2026-05-09 — zero npm/node/js/ts at every layer)
  • Parallelism-is-the-moat: feedback/feedback_parallelism_is_the_moat.md (founder direction 2026-05-13 — N-slot capacity, not monitors)
  • Dispatch brief template: feedback/feedback_dispatch_brief_template.md (15-section template — inbox brief body content)
  • BLUEPRINTS-SACRED: top-level memory rule — architect.md is source of truth; this ADR formalizes, never re-litigates
  • Ticket: #1266 — ADR-0036 Controller-User Relay Protocol
  • Milestone: #66 v1.0 — Moat-First

ADR-0036 is the operational moat of TUI The Bomb v1.0. ADR-0034's daemon is the substrate; ADR-0037's swim-lane is the visual moat; this protocol is what collapses the N-worker relay tax from hours-per-day to keystrokes-per-day. Without it, parallelism is visible but not operable at high N. With it, FF is the only product in the competitive set where N=11 and N=25 are not just legible but governable from a single keyboard.

Refs ADR-0034, ADR-0035 (#1265), ADR-0037 (#1261)

Closes #1266