Architecture

Pinard’s components communicate exclusively via NATS JetStream over WebSocket (wss://). There is no direct HTTP between components — NATS is the single bus, which is why an agent can run from any network (or another machine) with only outbound NATS.

Components

ComponentCodeRuns asModel
Daemoncmd/aoc/daemon.goself-supervising background process
Régisseurpi-extension/pinard/Pi extension, conductor tmux window [régisseur]Opus
Maîtrepi-extension/pinard/ (PINARD_PARCELLE set)Pi extension, one tmux window per parcelleOpus
Vendangeur (worker)pi-extension/worker/spawned tmux sessionSonnet
CLIcmd/aoc/Go binary (aoc)
Launcherbin/pinardshell script

The daemon is always running; the régisseur/maîtres are optional LLM layers. See Orchestration & Parcelles for how the three conductor tiers relate.

The daemon

The daemon is the engine. Started with aoc daemon start, it re-execs itself detached (no systemd), writes .state/daemon.pid, and logs to logs/aoc-daemon.log. It runs all watchers as goroutines on one persistent NATS connection and handles:

  • MR watching — pipelines, reviews, approvals, auto-merge, post-merge monitoring
  • Issue watching — assignee detection and auto-spawn (every assigned issue)
  • Schedule evaluation — cron spawns with backfill
  • Direct dispatch — actionable events go straight to worker inboxes (no conductor needed)
  • Liveness — orphan recovery and maître window recovery

Hot-reload is in-process: a tick polls the mtimes of the aoc binary, vignes.yaml, and schedules.yaml, and execs itself in place on any change (same PID, same logs).

Communication

A hand-drawn topology with civilian vineyard planners and estate managers above a central NATS pipe, worker processes at its sides, and daemon watchers below it. Régisseur & maîtres Vendangeurs Vendangeurs NATS JetStream Daemon Watchers & persisted state
One bus, no side channels. Every component communicates through NATS; the daemon owns the mechanical watchers and direct dispatch.
  • Régisseur & maîtres — optional LLM conductors above the bus.
  • Vendangeurs — independent workers connected through NATS, never directly to one another.
  • NATS JetStream — durable inboxes and events use the central bus.
  • Daemon & watchers — issue, MR, schedule, dispatch, liveness, and state machinery.
  • Broken terracotta routes — ephemeral core-NATS traffic such as BTW and interrupt.

Three channels (conductor → worker)

ChannelSubject suffixTransportPurpose
Main inbox…inboxJetStream (durable)Actionable work, queued until the turn ends
BTW…btwcore NATS (real-time)Parallel questions, immediate reply, no persistence
Interrupt…interruptcore NATS (real-time)Cancel the current turn

Events (worker / watcher → conductor)

Actionable events are dispatched by the daemon directly to the worker’s inbox; the rest are informational (the conductor sees them for visibility). ACK_REQUIRED events need a human/conductor acknowledgement.

EventSourceDispatched to worker?
pipeline_failedMR watcherYes
review_commentMR watcherYes
main_pipeline_failedMR watcherYes
tag_pipeline_failedMR watcherYes
mr_merged / mr_closedMR watcherNo
needs_approvalMR watcherNo (ACK_REQUIRED)
circuit_breakerMR watcherNo (ACK_REQUIRED)
issues_new / issues_commentIssue watcherNo
schedule_spawned/skipped/failedSchedulerNo (ACK_REQUIRED)
agent_idle / session_endedWorkerNo

NATS subjects

Agent subjects are parcelle-scoped — they carry a literal parcelles.<parcelle> segment (a worker always has a parcelle, defaulting to its project). Vignoble-level subjects (issues/schedules/notifications) are not parcelle-scoped.

pinard.<vignoble>.parcelles.<parcelle>.agents.<session>.events.<type>
pinard.<vignoble>.parcelles.<parcelle>.agents.<session>.inbox
pinard.<vignoble>.parcelles.<parcelle>.agents.<session>.btw
pinard.<vignoble>.parcelles.<parcelle>.agents.<session>.interrupt
pinard.<vignoble>.notifications
pinard.<vignoble>.issues.<new|comment>
pinard.<vignoble>.schedules.<name>.<status>

Subjects are built with centralized helpers (internal/pnats/subjects.go) — never hand-formatted. A maître consumer filters to its own parcelle; the régisseur uses the vignoble-level subjects plus a KV overview rather than the per-parcelle firehose.

tmux topology

One tmux server per vignoble (socket pinard-<vignoble>). The conductor session holds the [régisseur] window plus one window per active parcelle maître. Vendangeurs are flat tmux sessions on the same server, named parcelle-first (<parcelle>--<project>-<id>) so the session list is self-describing and filterable.

Visual palette

The launcher applies a role-colored tmux palette on startup so each agent tier is visually distinct:

RoleSurfaceColor
Régisseurconductor session base barTrellis grey; status-left shows 🍇 <vignoble>
Régisseur[régisseur] tabBurgundy — inactive: rosé text / grey; active: white on burgundy
MaîtrePer-parcelle window tabsGold — inactive: gold text / grey; active: ink on gold
VendangeurWhole-session status barLeaf green, 🧺 vendangeur label

Session/window pickers (ctrl+b s, ctrl+b w, ctrl+b f) are rebound to a colored choose-tree that tints entries by role (🍷 conductor session, 🎩 régisseur window, 🧑‍🌾 maître windows, 🧺 vendangeur sessions), making the multi-tier layout easy to navigate at a glance.

State

All state is persisted on every mutation (write-through, atomic rename, flock):

FileContents
.state/mr-watcher.yamlWatched MRs, pipeline counts, post-merge state
.state/issue-watcher.yamlTracked issues, last note IDs
.state/scheduler-runs.yamlLast run timestamps per schedule
.state/daemon.pidDaemon PID (liveness via signal 0)

Conductor and maître sessions are resumable Pi sessions (.state/regisseur-session.jsonl, parcelles/<name>/session.jsonl).