ADR-0036
Controller-User Relay Protocol
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/RelayOutboxprojection of the FS envelopes this ADR specifies. - ADR-0037 (in-flight on ticket #1261) — Schedule-View Rendering, which consumes the
kind=dispatch_briefenvelopes 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 unifiedSUB /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>.tmp → fsync → rename) 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
Option A — FS inbox/outbox + .ratified sidecar flag (RECOMMENDED, ratified master plan D4)
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 cancat ~/.flowforge/repos/<id>/workers/W3/inbox/0007.briefto read what the controller sent;ls workers/*/inbox/*.ratifiedto count ratifications;tail -f workers/W3/outbox/*.jsonto 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
touchis 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>.tmp→fsync→renameis a POSIX-guaranteed atomic visibility step. Readers (daemon watcher, foundercat) 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>.briefand<seq>.brief.meta.jsonare 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)
-
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.
-
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.
-
No
tail -f/catdebuggability. During dogfood and customer support, the founder cannot inspect what was sent. The only state is in the daemon's RAM. -
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.
-
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
ydoes 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.
Recommended option: A — FS inbox/outbox + .ratified sidecar flag
Rationale (anchored to master plan D4)
Per founder ratification 2026-05-13 (MASTER-PLAN.md §"The 8 founder decisions" D4):
-
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. -
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.
-
tail -fdebuggability 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. -
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.
-
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. -
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>.rejectedbrief 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
versionandidempotency_keyfields below are ADR-0036 additions to the architect.md §7.8 canonical schema. They are flagged here for ADR-0035 (#1265) struct-shape reconciliation.versionenables forward-compatible envelope-format evolution;idempotency_keyprovides 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 PRredirect— 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,
versionandidempotency_keybelow 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 PRblocked— worker is blocked; needs founder/controller interventionfreeform— 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>.briefbody until they observe a flag file (ratifiedoredited). A pending brief with no flag is invisible to the worker. - Flag creation is idempotent at the FS level (a second
touchafter the file exists is a no-op). - Directionality disambiguation (load-bearing). The
.ratifiedand.readsuffixes appear in BOTH the inbox tree AND the outbox tree; disambiguation is DIRECTORY-based, never filename-based.<inbox>/<msgID>.ratifiedis written by the controller-sideRatify(founder pressy→ daemon) and signals to the worker that its inbound dispatch envelope was ratified;<outbox>/<msgID>.ratifiedis written by the worker-sideMarkRatifiedand signals to the controller that the worker has acknowledged round-trip completion on its paste-back. Both.ratifiedsidecars 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 viaMarkRead, reserved-symmetric for future inbox-side use).
Idempotency contract
Two layers of idempotency:
-
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. -
Content fingerprint:
idempotency_keyissha256(canonical_form(body))base64url-encoded. Canonical form forbody:- For inbox briefs: read the raw
<seq>.brieffile bytes, normalize line endings to LF, ensure UTF-8. - For outbox JSONs: marshal the
bodyfield 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_keyas 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. - For inbox briefs: read the raw
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-seqendpoint to receive a<seq>, then performs the FS atomic-write itself (<seq>.brief.meta.jsonthen<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 publishescontroller.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 performstouch <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>.brieffile exists (the body) - A
<seq>.brief.meta.jsonfile exists (the metadata) - NEITHER
<seq>.ratifiedNOR<seq>.rejectedNOR<seq>.editedexists (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>:
- The ACE controller session (where the slash command runs) calls
POST /v1/relay/allocate-seq?worker=<worker-session>&direction=inbox. - Daemon returns the next
<seq>integer (single-threaded allocator per(worker, direction)). - The ACE session writes
<seq>.brief.meta.json(atomic tmp-rename) withkind=dispatch_brief,target_ticket=<ticket-ref>,target_worker=<worker-session>. - The ACE session then writes
<seq>.briefbody sourced from--brieffile or--inlinestring (or stdin if neither provided), via atomic tmp-rename. - The daemon's FS-watcher observes the
<seq>.brieffile appearing (post-rename) and — after confirming the sidecar is also visible — publishescontroller.brief_proposedon the unified event channel (SUB /v1/relay/inbox/eventsper ADR-0034 §3.6 — the channel name is historical and carries both inboxbrief_proposedand outboxpaste_backevents).
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_atRFC3339 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 attemptstouch <seq>.ratifiedwithO_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/RelayOutboxGo 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_briefenvelopes to render the NOW/NEXT/QUEUED strips per swim-lane. Thetarget_ticketandtarget_workerfields 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_TOKENenv 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/is0700and contents are0600/0700as 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_keyis 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/RelayOutboxGo 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 controllerlayout, and floatingCtrl+\overlay. All three subscribe toSUB /v1/relay/inbox/events. /ratify alland/assignslash 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'sPOST /v1/relay/allocate-seqendpoint, 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.jsonlindexing (architect.md §7.7) — searchable via/searchslash command + indexed by date and ticket-ID.- Schedule-view NOW/NEXT/QUEUED strips (ADR-0037) — consumes
kind=dispatch_briefmetadata 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/catdebuggability — 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+/assignare 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 alliterates 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 commit5dc6d689) - 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