clankie-agent/docs/spec.md

Clankie Architecture Spec

Status: target architecture. This document is the source of truth for Clankie.

1. Summary

Clankie is a swarm visibility and management engine: a lead agent that runs your coding agents as visible, steerable panes on a bundled terminal stage, with provider-neutral integrations for work tracking, design, and version control. It is a good lead — it plans, spawns, watches, unblocks, harvests, verifies, and reports — and its active delegation profile controls how much bulk work it delegates. In the default lead profile, coding is done by worker harnesses (Claude Code, Codex, OpenCode, or Clankie's own clankie worker) running as performers on the stage; in solo, the lead can do reasonable work itself while the visible-pane rule still covers watchable work.

Clankie ships as one bundled product — the eve-backed brain, the clankie-herdr terminal stage, and the clankie CLI, installed together (create-clankiescripts/install.sh); ClankVox (the presence plane) and the cold-start supervisor are opt-in install flags. It is built by integrating a terminal stage, the eve runtime, and worker harnesses with a thin layer of glue:

  • The terminal stage is the visible mux layer and Clankie's own bundled stage: clankie-herdr (the Clankie-owned Herdr fork, ADR-0017) is the stage Clankie builds, version-locks, and ships (root ADR-0005) and provides the swarm coordination CLI. The StageProvider seam stays as internal design discipline and a testing seam, not a cross-mux portability promise. Every agent is a visible pane.
  • eve is Clankie's brain — the durable backend runtime for the lead agent. eve owns inbound channels (Discord, voice), cron schedules, durable sessions, and memory. It runs headless (eve dev --no-ui); what you see as Clankie is his own face — a pi-tui client (scripts/clankie.ts) that renders eve's event stream in a stage pane.
  • Performers are panes — worker harnesses (clankie, claude, codex, or opencode) that Clankie spawns into the stage for parallel or specialized work, all visible.
  • The window is the Clankie iOS app, a polished remote onto the always-on stage: it reaches the host over the tailnet through the lifecycle supervisor for cold-start and the eve relay for live interaction, and never assumes the brain is nearby.

Visible by design. Every worker is a named, attributable pane (clankie:<slug>) — no hidden background agents. The pane recorder, per-worker transcripts, and the orchestration graph make runs auditable after the fact: the pitch is "verify it worked," not just "watch it work."

Pi's @earendil-works/pi-tui is the face's presentation layer — the UI toolkit the face renders the eve/client event stream through. eve remains the brain; pi is the face renderer only, never the brain, the runtime, or a performer. A local ~/dev/pi checkout also remains a source reference for the Codex OAuth implementation in agent/lib (§4.6).

2. Goals and non-goals

Goals

  • Clankie is always on (today hosted on a Mac; the brain host is portable, §7) and part of a persistent terminal-stage session by default.
  • Turning Clankie on shows him in the iOS app; everything he does is a visible TUI pane in the active stage.
  • Inbound Discord and voice work surfaces as panes, not as hidden in-process subagents.
  • Clankie can spawn other agents — clankie, claude, codex, opencode, or custom commands — all visible.
  • Clankie ships its own stage. clankie-herdr is the stage Clankie builds, version-locks, and ships (root ADR-0005) — the Clankie-owned Herdr patch-stack fork for provider-specific capabilities (ADR-0017); Clankie product semantics and remote access live above the stage boundary, which stays a StageProvider seam for design discipline and testing, not a cross-mux portability promise.
  • The swarm is decoupled from Clankie: a stage session is a swarm-ready environment on its own. Agents coordinate with or without Clankie present, and any agent can take the orchestrator role on demand.

Non-goals

  • Clankie is not primarily another coding agent and not a chat-ubiquity personal agent — it is the management layer above coding harnesses. The lead does general work (debugging a stuck worker, reading logs, judgment calls) according to the active delegation profile: solo allows reasonable inline work, while lead and hands-off bias bulk work to visible performers.
  • No custom multiplexer, scheduler, or chat server. The active mux adapter and eve own those layers.
  • No hidden background agents. If it runs, it is a pane.
  • No legacy compatibility shims or alternate runtime surfaces.
  • Cloud (host-off) availability is out of scope for v1. The brain runs on a single always-on host — a Mac today — but the host is portable by design (§7), not hard-coded to a Mac.

3. Mental model

Stage, lead, performers, window.

flowchart TB
  subgraph mac["always-on host (Mac today)"]
    subgraph stage["terminal stage — clankie-herdr adapter today"]
      direction TB
      face["pane: clankie<br/>pi-tui client · main session"]
      disc["pane: clankie:discord-*<br/>presence-session mirror"]
      voice["pane: clankie:voice-*<br/>presence-session mirror + transcript"]
      w1["pane: claude<br/>delegated worker"]
      w2["pane: codex<br/>delegated worker"]
    end
    eve["eve service — Clankie's BRAIN (the lead agent)<br/>Discord gateway · channels · schedules · sessions · memory"]
    sup["clankie supervisor<br/>launchd · lifecycle up/status/down"]
    relay["eve relay channel<br/>WS over stage adapter sockets"]
  end

  phone["Clankie iOS — the WINDOW"]
  discord["Discord<br/>text + voice"]

  discord -->|gateway WS + webhook| eve
  eve -->|spawns visible work| stage
  eve --- face
  sup -->|ensure session + brain pane| stage
  eve --> relay
  relay -->|adapter sockets/API| stage
  phone <-->|tailnet lifecycle token| sup
  phone <-->|tailnet relay token| relay

  face -. swarm CLI .- w1
  face -. swarm CLI .- w2

Read it as:

  • The terminal stage is the mux boundary. clankie-herdr is the current/default adapter: a persistent named session (clankies) on the host. It provides panes, the swarm coordination CLI (herdr agent list/read/send/wait, herdr pane report-agent), and session durability. It is the Clankie-owned Herdr patch-stack fork (ADR-0017). Because Clankie builds and version-locks the stage it ships (root ADR-0005), fork-carried provider capabilities are assumed present and may be hard-required; the StageProvider capability checks remain a testing seam rather than a cross-mux compatibility mechanism.
  • eve is Clankie's brain — the lead-agent runtime. A long-lived local service that owns Discord/voice channels, cron schedules, durable session state, and memory. The visible Clankie face is a custom eve/client pi-tui, not eve's stock TUI. When eve has inbound or background work, it does not answer invisibly — it spawns or routes to a stage pane so the work is visible.
  • Performers are panes. clankie, claude, codex, or opencode agents started through the active stage adapter. (Herdr starts them with herdr agent start; the supported performer set is defined in §4.3.)
  • The window is the iOS app. It reaches the host over the tailnet through two narrow front doors: the lifecycle supervisor below the brain for up/status/down, and the eve relay inside the brain for chat, terminal, and pane control.

4. Roles in detail

4.1 Terminal stage — mux adapter boundary

The stage is a mux adapter boundary, not a Herdr product requirement. Herdr is the current/default adapter. It provides:

  • Panes — every agent is a real terminal, visible and attributable.
  • A persistent named session (herdr --session clankies) that survives across restarts and disconnects, so "always on" is herdr's job, not a daemon Clankie writes.
  • The swarm coordination CLI, all over the local unix socket:
    • discovery: herdr agent list, herdr agent get <name>
    • live observation: herdr agent read <name> / herdr pane read
    • messaging: herdr agent send <name> <text> / herdr pane send-text
    • synchronization: herdr agent wait <name> --status idle|working|blocked
    • presence: herdr pane report-agent --state … --message …
    • spawning: herdr agent start <name> -- <argv…>

Herdr may be the Volpestyle patch-stack fork when Clankie needs Herdr-side provider capabilities such as stage performance fixes or pane/session metadata (ADR-0017). Those capabilities stay behind the adapter boundary. Remote access is provided by eve (4.4), and product policy stays in Clankie, not in Herdr.

The mux adapter is not Clankie's durable historical transcript store. Visible and recent pane reads are live-stage inspection. Historical output for workers Clankie spawns is captured by Clankie's transcript layer (§4.3).

4.2 eve — Clankie's brain (the lead-agent runtime)

Clankie is an eve agent: a directory of files (agent/instructions.md, agent/tools/, agent/channels/, agent/schedules/, agent/skills/, agent/lib/) that eve compiles and runs as a durable local service.

eve provides Clankie's durable runtime primitives:

Clankie needeve primitive
Discord text in/outagent/channels/discord-gateway.ts + agent/channels/discord.ts
Voice in/outagent/channels/voice.ts + ClankVox media
Linear delegation in/outagent/channels/linear.ts (native Agent Sessions, ADR-0015)
Durable session stateeve sessions + continuationToken
Scheduled/autonomous runsagent/schedules/*.md (cron frontmatter)
Slash-command executionheadless command host (scripts/clankie.ts --command-host) registered with the relay
Visible facecustom face on eve/client (clankie dev while editing, pnpm dev direct)
Memoryeve session context + Clankie memory lib

Clankie's face is a custom client on eve/client. eve dev's slash-command set is fixed and non-extensible, so Clankie's visible face (scripts/clankie.ts, run through clankie dev while editing or pnpm dev directly) is our own terminal UI built on the public eve/client. It attaches to the same headless eve dev --no-ui brain (same sessions, memory, tools), starts a session over the default eve HTTP channel (POST /eve/v1/session, POST /eve/v1/session/:id, GET /eve/v1/session/:id/stream), and renders the streamed events (message.appended, reasoning.completed, actions.requested, action.result, turn.failed, …) closely mirroring eve dev's look — gutter glyphs, an expo-agent-spinners-backed phase-aware working spinner with width presets, curated same-width cycles, exact spinner picks, custom cycles, and a configurable cycle rate, and a persistent bottom status line (model · effort · tokens · endpoint). On top it adds Clankie-specific slash commands eve dev can't. The deterministic command executor is the command host registered with the relay; a visible face may also register as a command host, but iOS and other remote clients depend on command-host presence rather than a visible TUI. The live command list is /help and the shared COMMANDS registry in scripts/clankie.ts; this spec does not duplicate that list. Beyond slash commands, the face has an inline shell escape (mirroring codex/opencode): pressing ! on an empty input enters bash mode (accent border, suppressed typeahead), where a submitted line runs as a host shell command in the repo via the user's $SHELL and its output renders inline as a transcript block. Esc or backspace-on-empty exits; Ctrl-C kills a running command; a !-prefixed line runs the same path without entering the mode. The runner and renderer live in agent/lib/clankie-face-bash.ts, and each run is summarized into the TUI ledger so the brain stays aware. Config commands rewrite .env.local and restart the brain through the host below Eve. Behavior-policy config that is not an env secret lives in wizard-owned JSON under CLANKIE_HOME: /integrations writes role bindings and /profile writes the delegation profile. /profile shows the active delegation preset, the five role assignments, and a runtime-stack submenu for the local/API conductor and voice profiles. Profile selection is manual; Clankie never infers it from provider, billing class, or credential type. Remote settings clients read and patch the delegation profile plus the adjacent worker_subagents and auto_validation policy flags through the relay profile.get and profile.set ops; the slash-command menu protocol remains available for the interactive /profile and /policies flows.

On an otherwise empty install, the main face offers /setup once, writes CLANKIE_SETUP_SEEN=1, and renders the status as "not set up yet — run /setup" instead of startup noise. /setup is a guided SetupFlow wizard: it first asks for user or developer mode, then walks model + auth, local config/token bootstrap, model tuning + harness, integration roles, Discord, iOS pairing, voice/push, and a reserved policy-preset slot. Developer mode adds an idempotent bootstrap menu for pnpm install, clankie install, pnpm clankvox:setup, and clankie supervisor install. Each step is skippable and re-entrant, and the leaves delegate to the same command logic as /model, /harness, /integrations, /auth, /login, /discord-token, /discord-scope, /pair, /voice, and /push; the policy-preset slot is a placeholder for the /policies picker. /setup and the noninteractive clankie setup command share agent/lib/env-bootstrap.ts: they seed .env.local from .env.example when needed and idempotently mint Clankie's own bearer tokens (CLANKIE_RELAY_TOKEN, plus CLANKIE_LIFECYCLE_TOKEN for local lifecycle setup) without overwriting existing values.

The face surfaces input.requested (tool-approval / human-input prompts) and session.waiting, then resumes the turn with explicit responses. /approvals auto (env CLANKIE_AUTO_APPROVE=1, read by agent/lib/approvals.ts) remains available for uninterrupted tool execution; /approvals prompt restores per-tool gating. This only affects approval gates, not the model's own ask_question clarifications.

The same HTTP routes back the iOS chat surface. For any non-local client, eve's default dev auth is not sufficient — public surfaces need their own route auth (see eve's docs/guides/auth-and-route-protection.md); eve/client supports bearer/basic auth and custom headers for that.

4.3 Performers — panes

When the active delegation profile, parallelism, specialization, or watchability calls for worker execution, Clankie spawns it as a pane via herdr agent start, never as an eve in-process subagent (which would render only inside eve's own transcript). Performer types:

  • clankie — Clankie's own CLI runtime (clankie worker <prompt>), backed by the running Eve brain and Clankie's configured skills.
  • claude — a Claude Code worker for coding tasks.
  • codex — a Codex worker (and the way to use the OpenAI subscription for delegated coding, distinct from Clankie's own model in §4.6).
  • opencode — an OpenCode worker that uses OpenCode's native internals.

Allowed coding performers are selected through coding harness profiles, not by importing another agent's internals. /harness allow writes CLANKIE_CODING_HARNESSES (clankie, claude, codex, opencode, custom, or all) as the policy allowlist. Every worker spawn must choose a harness explicitly (clankie, claude, codex, opencode, or custom); there is no automatic or fallback harness selection. Direct /harness <id> commands configure per-harness launcher/model env for claude, codex, and opencode, or CLANKIE_CODING_HARNESS_COMMAND for custom harnesses. The launcher is either the native CLI default model or an Ollama CLI integration with a local model; Codex Ollama mode uses ollama launch codex, not codex-app. herdr_spawn rejects disallowed harnesses, resolves the selected profile, and starts the command as a visible stage pane through the current Herdr adapter. Clankie then supervises by reading and steering the pane with herdr_read / herdr_send. Richer mux adapters can be added later, but the base protocol is always a visible pane.

Model and effort are also selectable per spawn, overriding the per-harness _MODEL env default so the lead picks the tier by task shape (grunt/mechanical work → cheaper or local model; judgment-heavy work → higher tier). herdr_spawn model/effort and spawn.sh --model/--effort inject the performer CLI's own flags — claude/codex/opencode --model; claude --effort, codex model_reasoning_effort; the clankie performer and a raw command passthrough carry neither and degrade cleanly. Omitting them preserves the env-default argv byte for byte. A bound consultant (consultant.json under the data root, or CLANKIE_CONSULTANT_HARNESS/_MODEL/_EFFORT; a harness+model reference) turns a consult-<slug> spawn into an on-call top-tier advisor auto-pinned on the consultant model via that same per-spawn param — a bounded, brief-in / verdict-out, advisory-only pane, never resident, so a cheap resident lead can escalate a taste or architecture call without paying top rates on every turn. Unbound, a consult-<slug> spawn is an ordinary spawn. Because workers are separate CLI processes whose tokens never reach the usage ledger (§4.4.2), the resolved per-spawn model/effort are stamped on the orchestration spawn event so the model split stays visible in the orchestration plane.

Goal mode defaults from CLANKIE_WORKER_GOAL (off by default; /harness goal on|off writes it) and can be overridden per spawn. herdr_spawn goal: true or goal: false and spawn.sh --goal or --no-goal control whether Clankie arms the worker after launch with the harness-native /goal loop when the resolved coding harness profile supports it. Support is a CodingHarnessProfile.supportsGoal capability and is true only for Claude Code and Codex on the default launcher; Ollama launchers, OpenCode, Clankie, custom commands, and raw argv passthroughs spawn unchanged and report the goal request as unsupported. The performer argv is unchanged under goal mode: Clankie waits briefly for the visible pane to produce output, then injects herdr pane run "$PANE" "/goal <condition>". The condition includes the task, the worker completion contract (result.md plus DONE, or result.md plus BLOCKED), a blocked escape so the loop halts instead of spinning, and an optional soft budget / --budget <turns> clause. Budget or provider usage-limit exits write GOAL_BUDGET_EXHAUSTED or GOAL_USAGE_LIMITED into the blocked result so harvest can report budget instead of a human-input blocker. Relay/iOS raw start goal support is deferred; the relay can still start arbitrary panes and the raw api passthrough remains an explicit no-policy escape hatch.

Clankie-spawned performers are wrapped with clankie transcript-run by default when worker transcript capture is enabled (CLANKIE_WORKER_TRANSCRIPTS, default on; /harness transcripts on|off writes it). Herdr still starts and owns the pane, while the runner passes terminal output through unchanged and appends a local transcript under ~/.clankie/herdr-transcripts/<herdr-session>/<agent>/<run-id>/:

manifest.json
stream.ansi
stream.txt

stream.ansi is the lossless terminal stream and stream.txt is the normalized readable history. Stored transcripts are retention-swept opportunistically at run creation (30 days / 500 runs / 2 GiB across sessions, never touching runs active within 6 hours). Any agent in the same Herdr session can read it with clankie transcript read clankie:<slug> --lines N. herdr_read defaults to source: "auto", which prefers durable history (the worker transcript for agents, the pane recording in §4.3.1 for panes) and falls back to Herdr recent-unwrapped output. Explicit Herdr sources (visible, recent, recent_unwrapped) still read Herdr for current screen/debugging state. Herdr intentionally has no full-history read: retained scrollback is a bounded in-memory buffer, reads are capped server-side (1000 lines, no pagination), and nothing survives pane close — durable history is Clankie's, captured at the byte stream (ADR-0007).

A worker has a transcript iff it was launched under clankie transcript-run; sharing Clankie's HERDR_SESSION only grants read access to transcripts that already exist. Capture must sit in the pipe because Herdr exposes bounded retained scrollback snapshots and attach-time live byte streams, not a retroactive lossless transcript — so there is no way to transcribe a pane after the fact. Every spawn entry point therefore funnels through one wrapping seam (wrapTranscriptArgv in agent/tools/herdr_spawn.ts): the eve herdr_spawn tool, the clankie-lead spawn.sh, the TUI /spawn command, and the relay start op all resolve the transcript default from CLANKIE_WORKER_TRANSCRIPTS with pinned HERDR_SESSION/CLANKIE_HOME when wrapping is enabled. Per-spawn overrides remain explicit: herdr_spawn transcript: true|false, relay start transcript: true|false, and lead spawn.sh --transcript|--no-transcript. New spawn surfaces call this seam, never raw herdr agent start. The relay's raw api/agent.start passthrough stays the explicit escape hatch that starts an unwrapped pane with no transcript.

Every spawn also names its pane at this seam. The clankie:<slug> routing name stays the durable identity (pane name, transcript-dir segment, orchestration graph node id); a separate human-readable label is chosen synchronously at spawn — the caller's title, else the task's first line, capped — and applied with a follow-up pane.rename. deriveWorkerLabel/WORKER_SLUG_RE in agent/lib/worker-naming.ts are the single source of truth for both the slug grammar and the label, and every spawn surface passes a title. pane.rename sets the field paneDisplayName() prefers over the clankie: agent name, so the label reaches the roster, orchestration graph, and iOS canvas with no parser change. The label is a zero-cost context clue available the instant a pane exists; the LLM pane summaries (ADR-0010, described below) are the opt-in enhancement layered on top.

Spawns are also wake-armed by default: the spawn funnel (spawnClankieWorker) and the lead spawn.sh each arm a detached one-shot completion watcher (clankie watch; classification logic in agent/lib/worker-watch.ts) per worker. The watcher blocks on the mux's agent-status events for that pane, classifies the outcome against the run's DONE/BLOCKED sentinels — sentinels are completion truth, agent_status is a heuristic confirmed by a quiet-screen window plus a slow recheck under the event stream — and delivers exactly one provenance-stamped [from watch:<slug>] [worker done|blocked|idle|dead] wake into the spawning lead's pane through the clankie msg machinery, then exits. The notify target is resolved to a durable name at spawn time (never a stored pane id, which compacts). Opt out per spawn with herdr_spawn watch: false or spawn.sh --no-watch; spawn.sh records the armed watcher in manifest.json and logs it to workers/<slug>/watch.log. harvest.sh additionally reports budget when a BLOCKED sentinel/result carries GOAL_BUDGET_EXHAUSTED or GOAL_USAGE_LIMITED; watcher wakes still use the blocked wake family and the lead harvests for the finer state. This wake bridge is the supervisor's wake substrate in the pool orchestration operating model (docs/adr/0002-pool-orchestration-operating-model.md). The relay start op does not arm a watcher yet.

The same watcher is available on demand for a pane Clankie did not spawn through the herdr_watch tool (docs/adr/0014-first-class-pane-watch-tool.md): it arms one detached clankie watch on any pane in the live roster and wakes the notify target (default clankie:main) once. Two modes share the watcher and the events.subscribe stream: completion (agent-status settle — heuristic for a foreign pane with no run dir) and milestone (--match on pane.output_matched, delivering [from watch:<slug>] [watch matched|gone|timeout]). The tool validates the notify target against the roster and fails loudly rather than arming a dead-letter watch; the watcher resolves label-only panes via the pane roster when herdr agent get (agent identities only) does not.

NeedSource
Running, idle, blocked, doneHerdr
Completion wake ("ping me when done")clankie watch, armed per spawn at the seam or via herdr_watch
Output-milestone wake ("ping me when it prints X")herdr_watch match (clankie watch --match)
Watch a pane Clankie didn't spawnherdr_watch
Send text or keysHerdr
Current TUI screenHerdr visible
Recent terminal screen bufferHerdr recent / recent_unwrapped
Full history of a wrapped workerClankie worker transcript
Full history of any other pane (iOS-created, manual, mirrors)Clankie pane recording (§4.3.1)
Historical worker outputClankie transcript when capture is enabled
Cross-agent audit trailClankie transcript + pane recordings
Full-text search across all of the aboveherdr_search / clankie transcript search
One-pane harvest briefclankie harvest <pane-label|pane-id|agent>

4.3.1 Session-wide pane recorder

The worker transcript covers only panes spawned through the wrapping seam. The pane recorder (ADR-0007) is the second, observational capture plane: a service inside the always-on brain that subscribes to Herdr pane lifecycle events, attaches to every pane in the connected session via the Herdr 0.7.1+ pane.attach byte stream, and persists per-pane recordings beside the worker transcripts under the reserved panes/ name:

~/.clankie/herdr-transcripts/<herdr-session>/panes/<recording-id>/
  manifest.json        kind: pane-recording (paneId, terminalId, agent, label)
  events.jsonl         attach / seed / stream-gap / rotate / prune / finalized
  seed-NNNNNN.txt      pane.read snapshot at each attach epoch (≤1000 lines)
  stream.ansi          lossless raw bytes since attach (active segment)
  stream.txt           normalized readable history (active segment)
  archive-NNNNNN.*.gz  rotated segments (gzip, ~10x for terminal output)

The two planes carry different guarantees and both stay: the wrapper is in-path (lossless from birth, survives brain downtime, has exit codes); the recorder is observational (best-effort from first attach, gaps seeded from pane.read and marked in events.jsonl, pauses with the brain). Panes whose foreground process is a transcript-run wrapper are detected via pane.process_info and recorded lifecycle-only (coveredBy), so bytes are stored once; CLANKIE_PANE_RECORDER_RECORD_ALL=1 overrides. Recordings ride the same retention sweep as worker runs, plus a per-recording cap (256 MiB) enforced by pruning oldest archives at rotation so an immortal pane cannot eat the shared budget. A brain restart resumes the open recording for a live pane by terminal id; against a pre-pane.attach Herdr the recorder degrades to seed-only capture (clankie recorder seed runs one such pass manually, e.g. right before a Herdr live-handoff upgrade truncates replay to 8 KB/pane).

Recording is per Herdr session. The persistent clankie up brain records the clankies session it owns; an ad hoc/manual Herdr session records only after a recorder is started for that session with clankie recorder start (inheriting the current HERDR_SOCKET_PATH) or clankie recorder start --session <name>. Recorder commands accept --session <name> for named-session reads, and recorder read resolves pane ids, labels, and agent names.

flowchart LR
  subgraph herdr["herdr session"]
    ev["events.subscribe<br/>pane created/closed/exited"]
    att["pane.attach<br/>raw PTY bytes"]
    rd["pane.read<br/>seed snapshots"]
  end
  subgraph brain["clankie brain (always on)"]
    recorder["pane recorder<br/>(CLANKIE_PANE_RECORDER=1)"]
  end
  store[("~/.clankie/herdr-transcripts/&lt;session&gt;/<br/>&lt;agent&gt;/&lt;run&gt;/ worker transcripts (in-path wrapper)<br/>panes/&lt;recording&gt;/ pane recordings (recorder)")]
  wrapper["clankie transcript-run<br/>(in-path, per worker spawn)"]
  readers["herdr_read auto · herdr_search<br/>clankie transcript/recorder CLI · relay read op"]

  ev --> recorder
  att --> recorder
  rd --> recorder
  recorder --> store
  wrapper --> store
  store --> readers

Readers page beyond Herdr's 1000-line cap with herdr_read source: recording (anchor: head|tail, skip), and herdr_search / clankie transcript search run full-text search across both planes including gzipped archives (ripgrep when installed, bounded node scan otherwise). The relay read op mirrors the same recording-first auto behavior, so the iOS window gets durable pane history — including panes it created — for free. clankie harvest <pane> is the operator-facing one-pane brief: it resolves a live pane, reads worker transcripts for clankie:* panes, reads harness-native session files for Claude/Codex/OpenCode panes, and falls back to pane recordings or live Herdr output.

Pi is not a performer. herdr can technically start any binary in a pane, so nothing stops a one-off pi pane, but Pi is not maintained as a performer — Clankie's only use of Pi is @earendil-works/pi-tui as the face UI toolkit (§1).

All performers coordinate through the current adapter's herdr skill (4.5). Whoever is orchestrating loads clankie-lead for the harvestable fan-out protocol when the active profile and task shape call for visible worker lanes.

4.4 The window — iOS app, supervisor lifecycle + eve relay channel

Remote access stays above the mux adapter. Interaction goes through a custom eve channel (defineChannel, raw WS route) that relays the active stage adapter to the network. With the current Herdr adapter, that means Herdr's local unix API socket for normal pane/workspace ops and the client terminal socket for durable Native terminal attach. But the relay lives inside the eve brain, so it cannot be what starts the brain. Cold-start is handled by the launchd-managed clankie supervisor, a small process below the brain that exposes only up/status/down over the tailnet. It authenticates with its own install-minted lifecycle token (CLANKIE_LIFECYCLE_TOKEN), separate from the brain-minted relay token. See docs/adr/0001-remote-lifecycle-cold-start.md.

flowchart LR
  phone["Clankie iOS"]
  subgraph mac["always-on host"]
    launchd["launchd"]
    sup["clankie supervisor<br/>tailnet HTTP · auth: lifecycle token"]
    relay["eve relay channel (WS)<br/>auth: bearer token"]
    sock["stage adapter sockets<br/>Herdr: herdr.sock API + herdr-client.sock terminal attach"]
    stage["stage panes<br/>(Herdr adapter today)"]
    launchd -->|KeepAlive| sup
    sup -->|ensure session + brain pane| stage
  end
  phone <-->|tailnet, lifecycle token: up/status/down| sup
  phone <-->|tailnet, bearer token: chat + pane steer| relay
  relay <-->|read scrollback / inject input / stream status / terminal attach| sock
  sock --- stage
  • Pairing (QR). The primary connect path: clankie pair prints a clankie://connect?relayUrl=…&token=…&mode=tailnet deep link as a terminal QR. When the supervisor is installed, the same link also carries supervisorUrl=…&lifecycleToken=…; older relay-only links remain valid. The app scans it once, stores the relay URL/token and optional lifecycle URL/token in Keychain, and auto-reconnects over Tailscale on every launch (restoreConnection). clankie pair --link prints just the link for AirDrop. Tokens stay the credentials; Tailscale is the transport.

  • Lifecycle (supervisor). The app calls the supervisor's authenticated tailnet HTTP surface for remote cold-start: POST /lifecycle/up, GET /lifecycle/status, and POST /lifecycle/down. The supervisor reuses the shared lifecycle core in agent/lib/lifecycle.ts to ensure the clankies session exists and Clankie's brain (eve dev --no-ui) runs as a pane. clankie supervisor install|status|uninstall|rotate-token manages the launchd plist and lifecycle token. scripts/clankie-up.ts remains the local/SSH-compatible JSON CLI shell around the same core.

  • Push (relay register-push + APNs/FCM). After pairing, mobile clients register their push token (register-push {token, platform, events?} where platform is ios or android; omitted platform defaults to ios for the existing iOS client), persisted as push-tokens.json under the Clankie data root (CLANKIE_HOME, default ~/.clankie; a legacy ~/.config/clankie registry is copied forward on first use); the relay returns {ok, registered, platform, apnsConfigured, fcmConfigured} so clients can distinguish token registration from send-ready sender configuration. A poll-and-diff watcher (pane.list every 5s) pushes an alert when an agent transitions to blocked/done/error, carrying the pane/workspace ids so a tap deep-links into that pane's live terminal; the watcher also starts at brain boot when the registry is non-empty, not only on the first register-push. iOS devices use APNs token-based auth (ES256 JWT over a .p8 key, node:crypto + http2), gated on CLANKIE_APNS_KEY_PATH / CLANKIE_APNS_KEY_ID / CLANKIE_APNS_TEAM_ID (+ CLANKIE_APNS_BUNDLE_ID, CLANKIE_APNS_ENV); Android devices use FCM HTTP v1 with service-account OAuth, gated on CLANKIE_FCM_SERVICE_ACCOUNT_PATH (or GOOGLE_APPLICATION_CREDENTIALS) plus optional CLANKIE_FCM_PROJECT_ID, or the env-only CLANKIE_FCM_PROJECT_ID / CLANKIE_FCM_CLIENT_EMAIL / CLANKIE_FCM_PRIVATE_KEY trio. Both senders no-op when unset. The face's /push command is the local APNs setup surface and push status/test surface: it stores only the .p8 path, shows masked registered device tokens by platform, and can send test notifications through configured senders.

  • Time-based follow-ups (ADR-0024). Follow-ups use the same durable schedule store as garden schedule CRUD (<CLANKIE_HOME>/schedules/schedules.json) with two row kinds: recurring rows carry a cron cadence, while once rows carry fire_at, optional context, and completion fields. Legacy rows without a kind normalize to recurring. The dispatcher is off by default and starts only when CLANKIE_FOLLOWUPS=1; the face's /followups command writes that flag, restarts the brain, shows store/retention status, and can enable, disable, or delete rows. The brain has both an authored eve schedule and a 30-second in-process ticker, but both call the same lease-based claimDueSchedules path. Completion records last_run, last_outcome, last_error, last_session_id, clears lease_until, disables completed one-time rows, and advances recurring rows from the completion time so missed fires coalesce. A fired row runs as a normal eve session on surface followup, mirrored as a clankie:followup-* presence pane when a session id exists. Its final reply is not delivered automatically; the prompt requires an outbound tool such as push_notify, Discord, worktracker, or a connected MCP. push_notify fans out through the same APNs/FCM registry with event kind followup. Completed one-time rows prune after CLANKIE_FOLLOWUP_RETENTION_DAYS (default 14).

  • Interaction (relay). The relay is a raw WS route, so it bypasses eve's session framing and carries terminal scrollback, status, and input injection faithfully. It adds explicit start/create-tab/close/orchestration ops alongside transcript-aware read/send/run/keys/subscribe and a raw api passthrough. chat-read/chat-tail/chat-untail (ADR-0016) serve a pane's conversation as normalized @volpestyle/clankie-contract pane-chat turns read from the harness's native session files (Claude/Codex jsonl, OpenCode sqlite via agent/lib/pane-chat/), degrading to plain-tail text from worker transcripts, pane recordings, or live pane.read, and carrying honest source/fallback facets. create-tab {workspace_id?, argv, cwd?, label?, focus?} applies a one-pane Herdr layout so the new tab's root pane runs the requested command; clients must not simulate new tabs by starting a pane with only workspace_id, because Herdr will place that pane in the workspace's current tab. Shared-session iOS chat panes (ADR-0009). Chat-with-Clankie uses eve's session routes (/eve/v1/session). The Lead chat adopts the visible face's current thread through face.status {session?}{attached, session_id?, continuation_token?, herdr_session?, title?} when the face publishes identity over face-attach. The published herdr_session must match the requested or relay-bound Herdr session, so a phone viewing another Herdr session does not adopt the wrong TUI's lead thread. Other native chats call chat.attach {session_id, continuation_token, slug, title?, tab_id?, pane_id?, workspace_id?, workspace_label?}, which places (or, by handle, reuses) a one-pane interactive face tab in the requested workspace (workspace_id existing, else workspace_label find-or-create) or the default "Clankie" workspace. The pane is the tab root, materialized via layout.apply with no orphan shell; its command is scripts/clankie.ts --session <id> --label <slug>, and the continuation token is delivered as CLANKIE_CHAT_TOKEN in the pane env rather than argv. Live remembered handles win over workspace targeting, so workspace fields affect only fresh materialization. chat.close {tab_id?, pane_id?, close_tab?} tears it down, closing the owning workspace when Herdr refuses to close its final tab. The chat list is device-persisted ({slug, sessionId, continuationToken, title, tabId, paneId}); per-chat status is reconciled against the workspace tree the app already streams, so there is no server-side chat registry. Discord and voice stay watch-only mirrors (§5.6).

  • Shared wire schemas (ADR-0006). The monorepo carries the shared zod/TypeScript schema package at ../clankie-contract for relay and Eve session wire shapes. The agent owns the API; the relay imports @volpestyle/clankie-contract from the top-level workspace package (agent/channels/relay.ts, implementation in agent/lib/relay/*) to validate inbound relay requests. ../clankie-ios consumes the same workspace package for local and EAS builds. Inbound relay frames are additionally capped at MAX_RELAY_INBOUND_MESSAGE_BYTES (~36 MB, sized above the 25 MiB upload limit after base64 expansion); an oversized frame is rejected with an error reply before it is ever parsed.

  • Orchestration graph (relay orchestration + subscribe). Clankie owns spawn/watch/steer provenance because the truth is created at Clankie's seams, not in Herdr (ADR-0010). The orchestration registry appends versioned JSONL events under <CLANKIE_HOME>/orchestration/events.jsonl, folds them into durable-name edges (spawned, watching, steered), run groups, pane summaries, per-pane activity, and lead run statuses, and resolves those names to live pane ids against pane.list only at serve time. The relay orchestration {session?} op returns a full snapshot: {type:"orchestration", edges, runs, summaries, activity, run_statuses}. A relay subscribe entry {type:"orchestration"} is handled locally and streams orchestration.edge_upserted, orchestration.edge_removed, orchestration.summary_updated, orchestration.activity_updated, orchestration.run_status_updated, orchestration.run_status_removed, and orchestration.reset frames on the same "events" stream; mixed Herdr subscriptions are still forwarded to Herdr, but the orchestration subscription itself is never sent to Herdr. Steered edges expire after CLANKIE_STEER_EDGE_TTL_MS (default 30 minutes), watching edges end when the worker settles, and closed panes remove live watch/steer edges while spawned edges remain as ended run history. Per-pane activity is always-on, non-LLM PaneActivity {pane_id, kind, intensity?, updated_at, revision?}. kind is an open string with known values thinking, reading, editing, running, searching, spawning, waiting-on-child, awaiting-input, and idle; clients treat activity older than 10 seconds as stale and fall back to pane status. The producer derives waiting-on-child from live watch edges, seam-spawned worker activity from transcript recency, and everything else from Herdr agent_status; ended edges and retained pane ids are not transcript identity for reused pane ids. Pane summaries are an LLM-generated 1–2 sentence gloss per pane (agent/lib/orchestration/summarizer.ts) and fill the iOS garden agent inspect pane before it is expanded. They are off by defaultCLANKIE_PANE_SUMMARIES (off|auto|always) opts in, configured from the face with /summaries (mode, plus a provider/model override via CLANKIE_SUMMARY_PROVIDER/CLANKIE_SUMMARY_MODEL that reuses the same subscription-OAuth logins as the lead agent). The spawn-time pane label is the always-on node name, so the graph and canvas stay legible without them.

  • Live terminal (relay attach/write/resize/scroll). For a true interactive terminal — the phone typing straight into a pane and seeing it live — the relay adds a held-open attach stream plus raw write, resize, and scroll ops:

    • attach {pane, terminal_id?, cols?, rows?, cell_width_px?, cell_height_px?, takeover?=true, source?="visible", format?="ansi", strip_ansi?=false, lines?, interval_ms?=180} opens a per-pane stream. When terminal_id is present, the relay connects to Herdr's client socket (herdr-client.sock), requests TerminalAnsi, and also sends a full ANSI-preserving pane.read snapshot of source first so remote clients can seed local scrollback: {id, ok:true, stream:true, body:{type:"pane.output", pane_id, source, format, full:true, text}}. Live chunks that arrive before that snapshot are buffered and replayed after it. The relay then sends AttachTerminal output as Herdr-rendered base64 ANSI bytes: {id, ok:true, stream:true, body:{type:"pane.output", pane_id, terminal_id, source:"terminal_attach", format:"ansi", full, encoding:"base64", data, seq, width, height}}. The client treats text snapshots as history replacement, while full byte redraws replace only the active screen and must preserve the seeded scrollback. cols/rows and cell pixel sizes are the viewing client's native terminal grid; reconnecting with a new grid resizes the server-owned terminal while the pane process stays durable. The iOS app exposes this as Terminal mode Native. A Native attach also subscribes to Herdr's terminal input-state pushes (fork ClientMessage::SubscribeInputState) and streams them as {id, ok:true, stream:true, body:{type:"pane.input_state", pane_id, terminal_id, wheel_routing}} with wheel_routinghost_scroll | mouse_report | alternate_scroll, sent on attach and whenever the pane app's scroll routing changes (entering or leaving the alt screen or mouse reporting). host_scroll means the client scrolls its locally seeded scrollback; the other two mean a fullscreen app owns scrolling and the client forwards gestures with the scroll op. Input-state frames bypass the backpressure guard and the pre-snapshot buffer. Every outgoing pane.output body also carries t_frame (server Date.now() at send) as an additive latency-instrumentation field; strict clients strip it. A slow or backgrounded client is guarded by server-side backpressure: when the peer's WS socket buffers past 4 MiB, live output frames for that attach stream are dropped instead of accumulating in the relay process, and once the socket drains below 512 KiB the stream resyncs itself with a fresh full snapshot (the same buffered-replay seam used at attach time). Initial snapshots and their replays are exempt; polling-mode full-text frames are simply skipped while backpressured and resent on the next healthy tick. (Relay WS sockets run with TCP_NODELAY: eve's Node WS stack — crossws over the vendored ws — calls socket.setNoDelay() on every accepted connection.)
    • Without terminal_id, or if the direct attach path is unavailable, the relay uses the compatibility path: it first sends a full ANSI-preserving snapshot of the requested source — {id, ok:true, stream:true, body:{type:"pane.output", pane_id, source, format, full:true, text}} — then uses native pane.attach byte chunks when available, otherwise snapshot polling. source:"full" requests all retained Herdr scrollback when that Herdr build supports it; vanilla builds without full fall back to capped recent_unwrapped output, while recent remains bounded by lines. The iOS app exposes this as Terminal mode Mirror. detach {pane?} ends one stream (or all). A peer may hold one events subscription plus one attach:<pane> stream per open pane concurrently.
    • write {pane, text, t0?} writes verbatim bytes to the PTY master with no trailing Enter (unlike run/send). Typed text, control sequences (Ctrl-C as \x03), and arrow-key escapes (\x1b[A) all pass through, so the client owns keystroke encoding and newlines. When the requesting peer holds a live Native attach stream for the pane, the write is injected as ClientMessage::Input on that persistent Herdr client socket — the low-latency keystroke path, no per-op API connection — and the reply carries result:{type:"ok", via:"stream"}; otherwise it falls back to Herdr's pane.send_text API. keys always stays on the API socket because pane.send_keys is terminal-mode-aware. The relay serializes write, keys, run, and pane-targeted send by target pane before forwarding to Herdr, so clients may pipeline live terminal input over a held-open WebSocket without racing characters or Enter ahead of prior text; an API-path input op that follows a stream write within a short ordering-barrier window is briefly delayed so the two Herdr injection channels cannot reorder input (design note in agent/lib/relay/ordered-input.ts). write and keys accept an optional numeric t0 (client-clock ms) that the relay echoes into its CLANKIE_RELAY_TRACE=1 latency trace log.
    • resize {pane, cols, rows}{id, ok:true} resizes the server-owned terminal of the peer's live Native attach stream in place (Herdr ClientMessage::Resize; attach-time cell pixel sizes are preserved), so a client geometry change does not tear down and reattach the stream. When the peer holds no live attach stream for the pane the op fails with the standard {id, ok:false, error} shape and the client falls back to reattaching.
    • scroll {pane, direction: "up"|"down", lines, column?, row?}{id, ok:true} forwards a scroll gesture to the live Native attach stream's terminal (Herdr ClientMessage::AttachScroll, wheel source). Herdr routes it by the pane's input state: SGR mouse-wheel reports for mouse-reporting apps, alternate-scroll arrow keys for alt-screen apps, host scrollback movement otherwise. lines is a batched step count (Herdr caps 128 per message); column/row are the 0-based gesture cell for mouse-report encoding. Clients send it only when the streamed pane.input_state routing is not host_scroll; same no-live-stream error shape as resize.
    • ping {}{id, ok:true, result:{t}} (server Date.now()), an app-level keepalive/RTT probe answered by the relay itself with no herdr round trip.
    • The relay also heartbeats every peer server-side: a ws protocol ping every 30s (auto-answered by any RFC 6455 client; where the runtime exposes no ws-level ping the relay sends an app-level {type:"ping", t} frame instead, and any client traffic — e.g. the ping op — counts as liveness). A peer silent for 90s is terminated and fully cleaned up (attach streams, poll loops, pending commands), so a phone dropping off the tailnet cannot leave the relay pumping frames into a dead socket. Failed peer.sends and WS error hooks are logged and feed the same cleanup path.
    • Native slash commands brokered to the command host (command/ face-command) carry a 60s inactivity deadline: every forwarded host event or client menu message re-arms it; on expiry the client gets the standard {id, ok:false, error} reply and the host receives menu.cancel so no orphaned menu session lingers.
    • upload {kind:"image", filename?, media_type?, data} saves a base64 image from a remote client into Clankie's private data dir on the host and returns {path, filename, media_type, bytes, directive}. directive is an @image <path> line for the Clankie TUI attachment parser. iOS inserts it through bracketed paste so the path lands in the terminal editor without submitting the prompt prematurely.
  • The brain is just another stage pane — the lead pane — which is why lifecycle (the supervisor) sits below it and interaction (the relay) sits inside it.

  • The active mux remains an adapter boundary; the glue is TypeScript inside the Clankie eve app.

  • The iOS app targets this relay + supervisor lifecycle contract.

4.4.1 Health snapshot (/relay/health)

Clankie exposes a cheap in-process health snapshot for local status, remote window checks, and future self-alerting (ADR-0012). The seam is agent/lib/health-registry.ts: subsystems register named, synchronous health providers, and collectHealthSnapshot() returns {at, sources, errors}. A throwing provider lands in errors; it does not break the whole health poll.

Registered sources:

  • voice — registered when the Discord-connected voice runtime attaches. It reports runtime/session state, provider settings, turn-buffer state, voice-session counters, turnLatency, and the latest ClankVox transportStats when present. transportStats is the last transport_stats IPC event with a receivedAt timestamp; its counters are cumulative inside the ClankVox process.
  • discordTurns — registered by the Discord presence host while the gateway is running. It reports accepted-turn reply latency through the shared LatencyWindow shape and reply outcomes (sent, empty, skip).
  • processVitals — started at brain boot. It samples process uptime, event-loop lag, RSS/heap/external memory, bounded history, and registered gauges such as relay peer heartbeat/input-queue sizes and face-presence ack maps.

agent/channels/relay.ts includes the snapshot as the health field of GET /relay/health. Authorized successful replies are {ok:true, herdr, face, commandHost, health}; if the Herdr ping fails, the route still returns {ok:false, error, health} with status 502 so clients can inspect the brain's own health while the stage adapter is degraded. Each health poll schedules a debounced write, and the brain also runs periodic persistence, to <CLANKIE_HOME>/health/status.json (default ~/.clankie/health/status.json).

flowchart LR
  voice["voice<br/>turnLatency + transportStats"]
  discord["discordTurns<br/>turnLatency + outcomes"]
  process["processVitals<br/>event-loop + memory + gauges"]
  registry["health registry<br/>collectHealthSnapshot()"]
  relay["GET /relay/health<br/>health field"]
  status[("&lt;CLANKIE_HOME&gt;/health/status.json")]

  voice --> registry
  discord --> registry
  process --> registry
  registry --> relay
  registry --> status

4.4.2 Usage ledger + stats (stats / /relay/stats)

Clankie persists per-step model usage and serves durable usage analytics (the Stats surface, ADR-0019, VUH-522). Two layers:

  • Raw ledger (write path). agent/hooks/usage-ledger.ts is a server-side eve hook on step.completed (and step.started for latency). Because each conversation is its own eve session, one hook captures every channel — face, Discord text/voice, Linear, iOS — without per-channel wiring. It appends one v1-enveloped line per completed model step to <CLANKIE_HOME>/usage/events.jsonl (agent/lib/usage/ledger.ts): {v:1, kind:"step", ts, sessionId, channel, source?, model, inputTokens, outputTokens, cacheReadTokens?, cacheWriteTokens?, turnId?, stepIndex?, latencyMs?, outcome?, runId?, worker?}. source preserves the normalized eve channel kind while channel is the analytics bucket; runId/worker are present when a producer can stamp run-scoped usage. Each step is a separately-billed call, so per-step lines are the accurate billing unit; turnId/stepIndex let a rollup regroup by turn. Append-only JSONL with an atomic retention + byte-cap compaction seam, mirroring the orchestration registry; retention defaults to 45 days (CLANKIE_USAGE_RETENTION_MS).

  • Rollups + snapshot (read path). agent/lib/usage/rollups.ts folds the raw log into tiny per-UTC-day rollups kept forever at <CLANKIE_HOME>/usage/rollups/YYYY-MM-DD.json — per-channel turn/token counts, per-model tokens/$, per-day worker_settled outcome counts (joined from the orchestration log) and run counts (from herdr-runs/ manifests). The agent/schedules/usage-rollup.ts handler runs daily (00:15 UTC): it is write-once per complete day (an existing file is never re-folded, so pruned raw data can't clobber a durable rollup), leaves today's open day to the read path, and calls compactUsageLog on the same cadence. buildStatsSnapshot(range) assembles {type:"stats", range, generatedAt, rangeStart?, tiles, previous?, dailyOutcomes, channelMix, modelSpend, runs} for range 7d|30d|all by reading rollup files and folding any uncovered day (including today) live from the raw logs — so it answers correctly before the first rollup exists and after retention prunes rolled-up days. previous carries the same tile fields for the preceding fixed-size window and is omitted for all. Each runs entry joins the run manifest with best-effort outcomes, run-scoped token/cost totals, per-worker usage buckets, and lead_turn_settled phase history. It is served both as the relay stats {range?, session?} op and the frontdoor-authed REST sibling GET /relay/stats?range=. Cost is derived downstream from MODEL_USAGE_COSTS in agent/lib/config-defaults.ts; tokens are the primary unit and $ is additive (subscription/local models are 0; API-billed rates land with VUH-528). The wire shapes live in @volpestyle/clankie-contract (src/stats.ts), consumed by the iOS Stats window.

Face-vs-iOS attribution is split when the eve auth context carries a surface: usageAttribution(ctx.channel.kind, ctx.session.auth.current) stores the raw source beside the analytics channel, mapping the local face to face and frontdoor/iOS sessions to ios. Direct Discord and voice sessions keep their normalized source bucket.

4.5 Skills model

Skills split on capability, protocol, and role — see also skills/clankie-lead, skills/clankie-worker, skills/clankie-work-tracker, and the current adapter's herdr skill. The active delegation profile is injected on every turn by agent/instructions/profile.ts; it decides which roles stay with the lead and which should route to visible workers. The bundled lead/coding/host-command skills are written against that injected profile rather than carrying an unconditional delegate-by-default rule.

SkillAudiencePurpose
herdr (stage adapter)every agent in a paneflat full-picture literacy: discover/read/message/wait/report-agent
clankie-leadcoordinator onlyopinionated harvestable fan-out: run dirs, manifest, sentinel files, spawn/harvest/cleanup
clankie-workerspawned workersworker-side stage coordination: identity, peer messaging, blocker escalation, result/sentinel discipline
clankie-work-trackerleads and workersprovider-neutral durable work protocol: lead-authored scope/verdicts, worker narrative, status lifecycle, review hygiene
  • The herdr skill is the non-rigid "every agent sees everyone" layer. Its contract is stage-control literacy: panes, messages, waits, and presence self-report (herdr pane report-agent) so agents publish status for peers.
  • Do not push clankie-lead onto workers; it is the hub-role protocol. Workers carry herdr plus clankie-worker for their side of the completion and reporting contract.
  • Keep work-tracker provider mechanics shared. clankie-work-tracker has lead and worker sections because both roles need the same provider-neutral lifecycle. Split it only when trigger and audience genuinely diverge.
  • Decision rule: new skill only when trigger and audience differ; otherwise extend the existing skill.

4.6 Model and provider auth — API-key default, subscription paths opt-in

The default lead-agent brain is an API-key-backed OpenAI Responses model: CLANKIE_MODEL_PROVIDER=openai (implicit when unset), CLANKIE_OPENAI_MODEL, and CLANKIE_OPENAI_API_KEY / OPENAI_API_KEY. This is the OSS happy path and the route /setup and /auth openai configure first. xAI, Gemini, and local OpenAI-compatible endpoints are also non-subscription brain providers.

flowchart LR
  setup["/setup or /auth openai"] --> env[".env.local<br/>CLANKIE_OPENAI_API_KEY"]
  env --> openai["openai provider<br/>default brain"]
  gate["CLANKIE_ALLOW_SUBSCRIPTION_AUTH=1"] --> codex["codex subscription OAuth"]
  gate --> claude["claude subscription OAuth"]
  usertoken["CLANKIE_DISCORD_CREDENTIAL_KIND=user-token"] --> discord["Discord user-token<br/>Go Live/private calls"]

The subscription-backed Codex and Claude conductor providers are ToS-gray advanced paths. They are off by default and fail closed unless CLANKIE_ALLOW_SUBSCRIPTION_AUTH=1 is set. /model codex and /model claude write that opt-in alongside CLANKIE_MODEL_PROVIDER; manually setting CLANKIE_MODEL_PROVIDER=codex or claude without the gate is rejected by agent/lib/model-selection.ts. Pane summaries use the same gate when CLANKIE_SUMMARY_PROVIDER=codex or claude.

Default API-key implementation. agent/lib/openai-model.ts builds the OpenAI API brain with @ai-sdk/openai and provider.responses(modelId). The model id defaults from agent/lib/config-defaults.ts; the API key resolves from CLANKIE_OPENAI_API_KEY or OPENAI_API_KEY. The factory keeps startup usable before auth is configured, but the first model call still fails with the provider's auth error until a real key is present.

Codex subscription implementation. agent/lib/codex-* keeps the prior OpenAI/Codex subscription route as an opt-in provider. The OAuth utility mints and refreshes { access, refresh, expires, accountId } credentials, and codex-model.ts builds a custom AI SDK LanguageModel by pointing @ai-sdk/openai at https://chatgpt.com/backend-api/codex and injecting the Codex CLI headers (chatgpt-account-id, OpenAI-Beta: responses=experimental, originator). The wrapper always supplies the Codex-required providerOptions.openai.instructions, store:false, and reasoning-effort defaults so callers cannot omit them. This route presents official-client identity to a subscription backend, may drift, and is for personal accounts that accept that risk.

Claude subscription implementation. agent/lib/claude-* uses Anthropic OAuth with the same opt-in gate. claude-auth.ts stores and refreshes anthropic-oauth credentials; claude-model.ts attaches the OAuth bearer and Claude Code identity headers, then prepends the required Claude Code system block. Anthropic has no sanctioned third-party subscription API for this shape, so this route is explicitly ToS-gray and personal-use only. For Claude performers (claude herdr panes), this is unnecessary because Claude Code in the pane already uses its own subscription natively.

Discord credential kind. Discord defaults to bot credentials. The user-token path for private calls and Go Live remains opt-in through CLANKIE_DISCORD_CREDENTIAL_KIND=user-token and /auth discord --user-token. Automating a Discord user account is against Discord's ToS; the bot-token path is the default OSS route.

5. Key flows

5.1 Always-on boot

  1. clankie supervisor install writes the launchd plist and launchd keeps scripts/clankie-supervisor.ts alive.
  2. The supervisor exposes the tailnet lifecycle surface and, on up, ensures the persistent herdr session exists: herdr --session clankies.
  3. A pane in clankies runs Clankie's headless command host (scripts/clankie.ts --command-host; also reachable through local clankie up). The host owns or attaches the headless eve Clankie brain (eve dev --no-ui; channels listening, schedules armed).
  4. Optional visible faces (clankie dev or pnpm dev) attach to the same brain over eve/client.
  5. The eve relay channel listens on the tailnet for the iOS app and forwards deterministic slash commands to the command host.

"Turning Clankie on" means the command host and eve service are up. The iOS app can run commands without a visible face.

5.2 Discord presence — text (free-will chat)

Clankie is present in Discord, not merely callable. He listens to whole channels, decides for himself when a message is for him, replies in natural conversation, and can jump in or stay quiet. This is the eve+herdr Discord presence model.

The gateway (the always-on ear). eve's stock discord.ts is HTTP Interactions only — slash commands, request/response. Presence needs to read every message, so the brain owns a persistent Discord Gateway WebSocket (agent/lib/discord/gateway.ts) with the GUILDS, GUILD_MESSAGES, DIRECT_MESSAGES, MESSAGE_CONTENT, and GUILD_VOICE_STATES intents. The gateway is a single connection per bot token (a singleton), so the brain owns it. Outbound replies use the stateless Discord REST API (agent/lib/discord/rest.ts), which any holder of the bot token can call — so a presence session can post its own replies without routing audio/text back through the gateway owner. The HTTP-interactions discord.ts stays for slash commands; the gateway is the free-will surface.

Addressing + free will. Every inbound message runs the pure decideDiscordInbound (agent/lib/discord/acceptance.ts). It accepts — i.e. spends a model turn — on any of: a DM, a platform @mention, a reply to one of Clankie's own messages, a wake-name address ("hey clankie", "yo clank", "clanker, …" — matched by agent/lib/discord/wake-names.ts), a bare wake-name mention, or a message inside the engagement window (recent active exchange with the same user, so follow-ups land without re-tagging). Everything else is ignored cheaply, before any model cost. Acceptance only decides whether to think; the model still has the last word: an accepted turn may answer, or output exactly [SKIP] to stay silent. That two-stage gate (cheap heuristic, then model judgment) is the "free will" to continue, ignore irrelevant chatter, or jump in. The engagement window only re-extends when Clankie actually replies, so a [SKIP] does not keep him latched onto a conversation that moved on.

The presence session (the "Discord subagent"). Accepted chat does not run on Clankie's main face-pane thread — that would clog the window you talk to and watch. It runs in a dedicated presence session: a separate eve session, keyed per Discord channel, of the same root agent. Because it is the same agent, it shares Clankie's durable memory store, persona (instructions.md), and full tool surface automatically (eve shares the memory store across sessions; only per-session conversation history is separate). So the Discord Clankie is the same Clankie — same character, same memory, same abilities — on a thread of his own. Bridge commands escape the subagent and address the main thread directly: /clankie <msg> / /clankie direct <msg> → main session, /clankie new → fresh main session, /clankie compact → compact main context.

sequenceDiagram
  participant D as Discord channel
  participant G as gateway (brain)
  participant S as presence session (per channel)
  participant P as pane clankie:discord-* (mirror)
  participant W as pane clankie:<slug> (delegated work)
  D->>G: MESSAGE_CREATE (every message)
  G->>G: decideDiscordInbound (addressed? engaged? else ignore)
  alt accepted
    G->>S: send(text, continuationToken=channel)
    S-->>P: stream reasoning + tools + reply (watchable)
    opt heavy/parallel work
      S->>W: herdr_spawn (web, code review, …)
      W-->>S: result
    end
    S->>D: reply via REST  (or [SKIP] → say nothing)
  else ignored
    G->>G: drop (no model turn)
  end

Watchable in herdr. Each presence session is mirrored into a herdr pane named clankie:discord-<channel suffix> by a viewer that tails the session NDJSON stream (GET /eve/v1/session/:id/stream) and renders reasoning, tool calls, and messages. You watch the Discord subagent think and act on the stage, exactly like any performer (§5.6).

Awareness + delegation. The presence session has the same herdr_* tools as the lead agent, so it has total awareness of what main Clankie is doing: it reads the live stage (herdr_status, herdr_read on the face pane and other panes) and shares Clankie's memory. To keep the conversation responsive it delegates heavy work (web browsing, code review, long builds) to clankie:<slug> performer panes via herdr_spawn rather than blocking the chat turn, and it can hand a matter to main Clankie directly (herdr_send to the face pane, or a /clankie direct style escalation) when the human is really asking the foreground agent.

The rule still holds: eve owns inbound + durability; the terminal stage owns visibility. The presence session is the durability; its mirror pane and any delegated performers are the visibility.

Gateway resilience. Start failures and session invalidation self-heal: the gateway retries on a 5s→5m backoff, and each health check re-probes under the per-lock guard so a wedged start cannot mask recovery. The gateway singleton is enforced cross-process by a token-keyed lock (discordGatewayLockPath): two brains resolving the same Discord credential contend for one lease regardless of which checkout they run from, so a second brain yields instead of double-connecting. Accepted presence turns are serialized per channel (DiscordPresenceHost per-channel op queue) because eve keeps no durable FIFO for competing sends on one continuation token — a same-channel burst runs in arrival order while other channels' turns proceed in parallel. Readiness derives live from client.isReady() rather than a cached flag. Gateway status-file writes are debounced (~2s) and flushed on exit. Outbound REST goes through discord.js's rate-limit-aware request manager (429 Retry-After and 5xx retries honored), and discord_recent_activity results include failedChannels so a partial fan-out is visible instead of silently incomplete.

5.3 Discord presence — voice

Voice is the same presence model with a live media plane. The brain's gateway already holds GUILD_VOICE_STATES, so "hop in vc" (a wake-addressed chat message, a voice op, or the existing discord_voice_join intent) makes Clankie join the caller's server voice channel. In user-token mode, the gateway also listens to raw private call events and answers scoped DM and group DM calls; the DM or group DM channel id is used as the private call voice server id and a agent/lib/discord/private-call.ts adapter rewrites ClankVox's guild-shaped OP4 payloads into private-call OP4 payloads (guild_id: null). The gateway handles voice intents before starting the normal text presence turn, so success/failure is reported directly instead of asking the text model to infer whether a voice tool exists. On startup, the gateway also fetches recent messages from scoped channels and replays only recent server voice intents, covering the restart window without replaying ordinary chat turns. The media path is the control plane in agent/lib/voice/* (ClankVox Rust transport for Discord RTP/Opus, a realtime voice client — OpenAI/xAI Realtime or the local stack — and Realtime or ElevenLabs TTS), attached to the gateway's Discord client via attachVoiceRuntime() on agent/channels/voice.ts.

  • Inbound: ClankVox emits the voice channel's PCM; the realtime voice client consumes it, and the conversation lands as text turns in a dedicated voice presence session (separate thread, same shared memory + persona, like §5.2).
  • Reasoning + free will: the realtime agent keeps a small, latency-friendly control surface and delegates real work — it routes substantive requests to the voice presence session / herdr_spawn performers rather than mirroring Clankie's whole tool set into the low-latency loop. Wake-name barge-in interrupts playback; floor control suppresses transcripts while Clankie speaks.
  • Outbound: the agent's reply is rendered to PCM (internal Realtime audio or ElevenLabs) and sent back through ClankVox to Discord.
  • Watchable in herdr: a clankie:voice-<channel> pane mirrors the durability eve session (§5.6) — the live transcript (who said what) plus the agent's reasoning and expandable tool calls — so a voice room is as inspectable on the stage as a text channel. (Distinct from the clankie:voice-<slug> performer panes the voice delegate op spawns for heavy work.)

Because the voice presence is the same agent on its own session, a thing said in VC and a thing said in chat reach the same memory and character — and neither clogs the main face-pane thread.

ClankVox setup. The Rust transport must be built before the first join: pnpm clankvox:setup is idempotent — it installs the Rust toolchain (unattended rustup) if missing, builds the release binary, and verifies it. The join path requires that prebuilt binary (resolved by resolveClankvoxBinaryLocation); it never compiles inline. If it is missing, the voice op faults the session with a "run pnpm clankvox:setup" message rather than crashing the brain. Override the source dir with CLANKIE_CLANKVOX_DIR or point at a prebuilt binary with CLANKIE_CLANKVOX_BIN.

Voice runtime resilience. Joins and leaves are serialized so overlapping ops cannot interleave transport state. A realtime send on a closed socket is dropped and faults the session as socket_closed instead of throwing into the audio loop. Brain exit kills the ClankVox child, and a stale-PID reap runs on rejoin via the voice/clankvox.pid file so an orphaned transport never blocks the next join. IPC frames are capped at 32 MiB; a desync (oversized or malformed frame) faults the session rather than buffering unbounded. The voice status op redacts the Go Live endpoint/token (exposing only hasCredentials). The voice fault log rotates at 1 MiB. Local-voice per-turn errors degrade that turn instead of faulting the whole session.

Credential kind (bot vs user/self token). CLANKIE_DISCORD_CREDENTIAL_KIND selects bot-token (default) or user-token. Bot tokens cover text presence and server voice audio. User tokens cover private DM / group DM call answering and Go Live publish/watch. Set a user token via the custom face's /token … --user-token slash command; the gateway applies the discord.js user-token patches (agent/lib/discord/user-token-patches.ts: strip the Bot REST prefix, identify as a desktop client, use /gateway, synthesize the READY application). Automating a user account is against Discord's ToS; it is opt-in, for a personal account, and off by default.

Go Live. With a user token, the gateway exposes a raw opcode seam (rawGatewayClient()) that drives Discord's screen-share opcodes through the discordStreamDiscovery control layer (watch op 20, publish op 18, stop 19, pause 22). agent/lib/discord/golive.ts wraps this as a GoLiveController (list / watch / go-live / stop / pause), registered active when Clankie is in a server vc or private call, and surfaced to him as the discord_golive tool. Discovered stream credentials (endpoint/token/rtc_server_id) are forwarded into ClankVox via streamWatchConnect / streamPublishConnect to decode others' streams or publish his own. Stream discovery supports both guild:<guild>:<channel>:<user> and call:<channel>:<user> keys. Live-gated: the actual decode/publish needs a user token + a running ClankVox + a real call, so the forwarding sink is wired but unexercised offline; the opcode/discovery/tool layer is verified by pnpm smoke:discord.

5.4 Continuity across surfaces

Text chat, voice, and the herdr face-pane TUI are three windows onto one Clankie: the same root agent, the same durable memory store, the same tool surface. They differ only in which session thread a turn lands on:

SurfaceSessionClogs main thread?Watchable as
custom face (scripts/clankie.ts)main— (it is the main thread)the face pane
Discord textper-channel presencenoclankie:discord-* mirror
Discord server voice / private callvoice presencenoclankie:voice-* mirror
iOS Lead chatface's main thread (session-scoped relay face.status)— (same as face)the face pane
iOS other chatsper-chat session (relay chat.attach)noan interactive face tab per chat in the "Clankie" workspace

This is what lets you talk to Clankie in the TUI and have him doing parallel work while, independently, Discord text and voice presences carry on their own conversations with full awareness of that work.

5.5 Fan-out / swarm orchestration

The active delegation profile decides how much Clankie uses fan-out. solo is the degenerate no-pool case for reasonable inline work; lead delegates the implementer, tester, and investigator roles by default; hands-off routes all bulky roles to workers while the lead keeps steering, synthesis, and review.

When Clankie (or any agent) acts as orchestrator, it loads clankie-lead, spawns one pane per task (clankie:<slug>), ends its turn, and acts on the [worker <outcome>] completion wakes the per-spawn watchers deliver (§4.3). For self-driving coding work, the spawn is goal-armed (goal: true or --goal) when the harness supports it, so the worker's own /goal loop stops only after the sentinel contract is satisfied or a blocked/budget escape is written. Sentinel files remain completion truth and harvest.sh --wait remains the polling safety net when a wake never arrives. The orchestrator inspects with herdr_read (auto for history, visible for current screen), unblocks by injecting into panes, and harvests results. The native worktracker store (§5.7) is the durable queue, DAG, and harvest ledger for tracker-backed pool work. pool_orchestrate plan reads native issues by default, maps store dependencies to the scheduler frontier, and parse_result writes worker result/settle claims back to the issue timeline before the lead adjudicates final status. herdr_spawn can bind a worker to a native issue (issueId), records assignment/spawn/start facts in the store, and briefs the worker to read that issue through clankie worktracker show <issue>. The lead skill still owns run directories, manifest files, sentinels, and cleanup as live-stage mechanics; those files are not the durable task ledger. eve decides when to invoke the lead protocol from the profile and task shape. The wake bridge is the supervisor's wake substrate in the pool operating model (docs/adr/0002-pool-orchestration-operating-model.md).

5.6 Self-subagents and pane mirrors

There are two distinct things called "subagents" here; keep them apart:

  • Performer panes — another clankie, claude, codex, or opencode process started with herdr agent start (via herdr_spawn). Its own process, its own pane, fully interactive and watchable, coordinating through the herdr skill. This is how Clankie (or a presence session) does profile-delegated, parallel, specialized, or otherwise watchable work.
  • eve child sessions — eve's native agent tool / declared subagents spin up a child session in the same service. They share the memory store but render only inside eve's own transcript, with no pane. Clankie's presence sessions (§5.2/§5.3) are this kind: separate session threads of the root agent, made watchable not by being a process but by a pane mirror that tails the session's NDJSON stream into clankie:discord-* / clankie:voice-*.

The mirror is the bridge between the two: it gives a session-only "subagent" the on-stage visibility that the project's "everything worth watching is a pane" rule requires, without spawning a redundant process. The delegation profile is the deciding input for whether ordinary work stays in the lead turn or becomes a performer pane; visibility and explicit user direction can still require a pane in any profile.

scripts/discord-pane-mirror.ts is a read-only pi-tui app that reuses the face's ClankieFaceRenderer (agent/lib/clankie-face-renderer.ts) and ClankieTranscriptViewport (agent/lib/clankie-transcript-viewport.ts), driven by the shared agent/lib/session-stream-view.ts seam, so the mirror renders the same collapsible blocks the face does — reasoning, messages, subagents, and tool calls expandable to their full args and output. It is watch-only: arrow keys select a block, Enter/Space (or Alt+Enter) or a left-click expand/collapse, PageUp/Down and the mouse wheel scroll; all interaction with the session happens over Discord.

The mirror script (scripts/discord-pane-mirror.ts) and its argv builder (agent/lib/pane-mirror.ts) are shared across presence kinds; only placement differs. Discord and voice spawn through one seam, spawnSessionPaneMirror(slug, sessionId, meta?) (agent/lib/discord/pane-mirror-spawn.ts), splitting near the face pane: text channels mirror their presence session as clankie:discord-<channel> from the gateway's onPresenceSession; voice mirrors its durability eve session (§5.3 — the silent turn that runs the same brain on voice activity) as clankie:voice-<channel>, bootstrapped from joinVoice with a silent start turn so the voice subagent is visible immediately after joining. Voice transcript turns surface as Voice <speaker>: … inbound lines.

iOS native chats are not mirrors (ADR-0009). They attach through the relay chat.attach op (agent/lib/ios/chat-attach-spawn.ts): each chat is the root pane of its own interactive face tab (labeled with the chat slug) in a dedicated "Clankie" workspace — one tab per chat, materialized with layout.apply so no orphan shell pane is left — addressed by its {workspace_id, tab_id, pane_id} handles rather than a named agent. The phone chat and pane face are peer writers/readers on the same eve session. Discord and voice remain watch-only session mirrors.

Presences are watchable from the phone, too (Proposed, ADR-0018). The meta on spawnSessionPaneMirror records each mirrored presence — slug → (kind, eve sessionId, label) — in a persisted presence registry (agent/lib/presence/registry.ts). The relay presences op returns the live set (listActivePresences = registry ⋈ pane.list, so a presence appears only while its mirror pane is on stage), giving the iOS app the one thing a HerdrPane cannot: the eve sessionId behind a mirror. The app lists them in a "Clankie is active in…" drawer section and renders each read-only by streaming its eve session (GET /eve/v1/session/:id/stream) through the same chat transcript as the native chat — watch-only, since the interactive surface stays Discord / voice / Linear. The lead face is not a presence here; the app already models it as the Lead chat.

5.7 Native work-tracker — conversational loop

Clankie's native durable store (ADR-0013, §9) is his conversational hub, not only a work ledger: leave a note on an issue and Clankie replies in-thread — the same brain, memory, persona, and tools as every other surface. This is a presence surface modeled on §5.6.

agent/channels/worktracker.ts is the boot seam. When the runtime sets CLANKIE_WORKTRACKER_PRESENCE=1 it subscribes to the store's after-commit emitter (WorkTrackerStore.subscribe). A comment from a human actor (owner:<user> or the iOS surface — not Clankie's own brain, nor a worker's clankie:<slug>) opens or continues a per-issue presence session, dispatched over loopback into this same eve server (eve exposes no in-process dispatch, so the loop self-calls its session API exactly as the Discord gateway does, §5.2), seeded with the issue and its full timeline. The reply is posted back to the store as a comment under Clankie's brain actor — agent-authored, so the same actor filter echo-suppresses it and it never re-triggers the loop.

Push, not poll. The store's emitter fires in the process that performs the write, so the human-note ingress writes inside the brain: POST /worktracker/comment (loopback-only, actor derived from the local user, used by clankie worktracker comment) does the store write in-process and the emitter fires the loop synchronously. Writes from other processes (a worker pane) are not on this bus; the relay/colony feed reads them through the store's listEventsSince cursor (§9) instead. So the native path needs no webhook and no polling — the webhook is only for the Linear inbound leg (§5.8, Phase 2).

The session is made watchable like any other presence: on its first turn Clankie mirrors it into a read-only clankie:worktracker-<issue> herdr pane via spawnSessionPaneMirror (presence kind worktracker), deduped once per eve session id, and the iOS presences roster lists it read-only (§5.6).

flowchart LR
  you["you — clankie worktracker comment"] --> ingress["POST /worktracker/comment<br/>loopback · in-brain write"]
  linear["Linear comment"] -. "webhook → reconcile (Phase 2, §5.8)" .-> store
  ingress --> store[("native store<br/>SQLite/WAL")]
  store -- "emit after COMMIT (push)" --> loop["worktracker loop<br/>filter: human comment"]
  loop -- "loopback dispatch" --> sess["per-issue eve session<br/>brain · memory · tools"]
  sess -- "reply comment (actor=brain, suppressed)" --> store
  sess -- "turn.started" --> mirror["clankie:worktracker-&lt;issue&gt; pane"]

One loop, two ingress paths: a native note (CLI/iOS) is a local event — instant; a Linear note arrives as a generic workspace webhook that reconciles into the store and lands on the same emitter (§5.8). Clankie acts in-thread through the built-in worktracker_* tools (list / get / search / comment / create / transition / assign) plus any connected MCP; owner-only status transitions stay with the issue owner (the store's single-writer rule, §9).

Outbound mirror (Phase 1, VUH-483). The store's mirrors[] role list (§9, integration-roles.ts) is projected outbound by the mirror engine (agent/lib/worktracker/mirror.ts, boot seam agent/channels/worktracker-mirror.ts, runtime-gated by CLANKIE_WORKTRACKER_MIRROR=1). It subscribes to the same after-commit emitter and write-throughs each committed event (create / status / assigned / comment / result / dep) to each bound mirror on a per-mirror serial queue, keyed by a durable native↔external id map (id-map.ts, in the store's SQLite DB). Native always wins: the emitter only enqueues, so a mirror being down never blocks the native write or the turn — a failed projection degrades to a mirror_sync_skipped timeline event and the mirror's cursor stalls, repairing by catch-up on the next successful sync. Writes go out under Clankie's bot OAuth (ADR-0005), and comment-shaped writes carry a hidden origin marker; inbound reconciliation recognizes both as echoes. Linear is the only concrete target (via the curated Linear MCP connection); GitHub follows the same seam.

Search + retention. The native store owns keyword recall directly: FTS5 search indexes issue titles/descriptions and timeline event text in the same SQLite/WAL database, with structural filters for status, actor, worker, run, archive state, and recency. worktracker_search exposes bounded BM25-ranked snippets for the agent orient phase. Retention is archival, not deletion: terminal issues (done/canceled) older than CLANKIE_WORKTRACKER_ARCHIVE_AFTER_DAYS (default 180) are marked archived on store open and hidden from active list/search by default, while remaining opt-in searchable for colony replay and audits.

Sequence. Phase 0 (native-only loop) is live behind CLANKIE_WORKTRACKER_PRESENCE; Phase 1 (outbound projection) is live behind CLANKIE_WORKTRACKER_MIRROR. Phase 2 generic Linear workspace webhook → native reconciliation is wired through agent/channels/linear.ts and agent/lib/worktracker/linear-reconcile.ts, so a mirrored Linear comment can drive the same loop in real time. Phase 3 is optional: the already-present ADR-0015 Agent-Session path remains available for first-class Clankie presence inside Linear's own agent UI when its OAuth app token is provisioned.

5.8 Linear inbound mirror + optional Agent Sessions

Linear is an optional mirror over the native hub (§5.7, ADR-0013), not the system of record — a two-way surface, not only an outbound ledger. Generic Linear workspace data webhooks POST to POST /eve/v1/linear (agent/channels/linear.ts, eve's linearChannel). eve verifies the Linear-Signature HMAC (rejecting stale webhookTimestamps); Clankie maps the external issue through the native↔Linear ID table, suppresses events authored by Clankie's configured Linear mirror actor, suppresses marked mirror-comment reflections, appends human comments as native comments, and records Linear status/title/description edits as external suggestions. If no mirror actor identity is configured, generic inbound imports fail closed instead of treating unknown authors as humans. Native state wins: inbound webhooks never overwrite a newer native snapshot.

The optional Agent Session path remains mounted on the same route. When the Clankie app actor (a Linear OAuth app installed with actor=app + app:mentionable / app:assignable, distinct from the member actor that authors Clankie's autonomous writes — ADR-0005) is @mentioned or delegated an issue, Linear opens an Agent Session and POSTs an AgentSessionEvent webhook. Clankie continues a session keyed agent-session:<id> seeded with the issue/comment context, and streams the reply back as native Agent Activities — an ephemeral thought on turn start, action per tool call, a durable response carrying the final text, error on failure, and elicitation for HITL input requests.

That per-thread session is a presence session, modeled on Discord/voice (§5.6): the moment its eve turn starts, Clankie spawns a read-only herdr pane mirror clankie:linear-<issue> through the shared spawnSessionPaneMirror seam (agent/lib/discord/pane-mirror-spawn.ts), deduped once per eve session id (ctx.session.id). The session is the sub-agent; the mirror is its watchable tail on the stage — inbound Linear work is a visible pane, exactly like an inbound Discord message, not a hidden in-process turn.

This is the inbound human-interaction surface only. It does not make Linear the work substrate: the durable system of record stays native (ADR-0013) and Clankie's autonomous status/comment writes stay on the member-actor MCP connection (ADR-0005). Generic data webhooks keep the mirror bidirectional; the Agent Session path is the optional richer form where a person hands Clankie a task or question in a Linear agent thread and gets a first-class agent reply in the same thread.

The webhook needs a public HTTPS ingress; the brain is otherwise loopback / tailnet-only. Ingress forwards only /eve/v1/linear to the brain (e.g. a path-scoped Tailscale Funnel), and the HMAC signature is the authenticator of last resort. See ADR-0013 and ADR-0015.

6. Decoupled swarm sessions (Clankie optional)

Because all coordination goes through the stage adapter (herdr today) and the herdr skill, the swarm does not depend on Clankie.

  • clankies session = stage + Clankie (the lead agent) present → the full product.
  • A bare swarm session = stage only. Agents self-coordinate with herdr agent list/read/send/wait; any one of them loads clankie-lead to become the orchestrator on demand.

The session is the primitive. Clankie is one optional lead-agent process you attach to whichever session you point him at. You may run multiple sessions.

7. Always-on / deployment

  • Host: a single always-on machine — a Mac today. The brain host is portable by design: the launchd supervisor and the Seatbelt host_command sandbox are the only macOS couplings, so a small Linux box or a hosted deployment is a planned shape, not a re-architecture. The phone already treats the brain as remote (§4.4), so relocating the host is a move, not a redesign.
  • Durability: the stage's persistent session (panes survive) + the eve service (durable sessions, memory). Both restart with the machine.
  • Reachability: tailnet only; the eve relay binds to a private address with a bearer token. No public exposure.
  • Cloud / host-off availability is explicitly deferred for v1. If pursued later, a separate deployed eve could cover headless chat only — pane access always requires the local terminal stage.

8. Substrate vs. build

External substrate:

  • terminal stage — persistent mux session and swarm CLI; clankie-herdr is the current/default adapter.
  • eve — brain, channels, schedules, durable sessions (headless eve dev --no-ui + eve/client).
  • @earendil-works/pi-tui — the face's terminal UI toolkit (rendering primitives).
  • clankie / claude / codex / opencode — performer agents (pi is not a performer).
  • the herdr skill.
  • Volpestyle's Herdr patch-stack fork when fork-carried provider capabilities are required (ADR-0017).

We build:

  1. The Clankie eve agent — instructions, persona, tools, memory lib, Discord and voice channels, schedules.
  2. The eve → herdr-pane spawn seam — the lead agent surfaces inbound and background work as herdr agent start panes (5.2). This is the core integration.
  3. The Clankie transcript layer — local append-only transcripts for Clankie-spawned workers, exposed through clankie transcript and transcript aware herdr_read.
  4. The eve relay channel — raw WS route bridging the iOS app to Herdr's local API and terminal attach sockets (4.4).
  5. The iOS client surface targets the relay contract.
  6. The presence self-report convention agents use through the herdr skill.

9. Repository and package layout

9.1 Target layout

clankie/
  agent/                     # the eve Clankie agent (the lead agent)
    instructions.md
    agent.ts
    channels/
      discord.ts             # Discord HTTP Interactions (slash commands)
      discord-gateway.ts     # free-will presence boot seam (Gateway WS owner)
      voice.ts               # voice channel (ClankVox-backed) + join/leave seam
      relay.ts               # raw WS relay to Herdr sockets (the iOS window)
      orchestration.ts       # agent-owned spawn/watch/steer graph boot seam + health
      linear.ts              # native Linear Agent inbound (Agent Sessions) -> /eve/v1/linear
    connections/             # curated third-party servers (Linear, Figma, GitHub): MCP/OpenAPI + brokered auth
    tools/                   # typed first-party tools (spawn seam, browser-bridge, web, discord, media, dynamic-MCP bridge)
    schedules/               # cron jobs
    skills/                  # agent-local skills (or symlinked)
    lib/
      discord/               # gateway, acceptance/free-will, wake-names, host
      voice/                 # Realtime/ClankVox control plane
      ...                    # memory, persona, herdr seam helpers
  scripts/
    clankie.ts                # custom face on eve/client (pnpm dev)
    discord-pane-mirror.ts   # tails a presence session into a herdr pane
  bin/
    clankie.ts                # installed as ~/.local/bin/clankie
  skills/
    clankie-lead/   # coordinator fan-out protocol
    clankie-*-operator/       # web/media/figma/work-tracker/etc.
  packages/
    clankie-browser-bridge/   # local browser control bridge
  branding/
  docs/
    spec.md                  # this document
  README.md

The shared wire contract lives in the top-level monorepo package ../clankie-contract. ClankVox stays a sibling Rust package (../clankvox) feeding the voice channel.

9.2 Integration model: connections vs. tools vs. dynamic MCP

Three tiers, separated by trust and auth shape. Pick by the rule below; do not blur them.

  1. Connections (agent/connections/<name>.ts) — the canonical path for curated third-party SaaS Clankie depends on: Linear and Figma first, then any other credentialed service (work trackers, design, finance). One file per server via defineMcpClientConnection (or defineOpenAPIConnection). eve brokers auth, keeps the URL and credentials out of the model's reach, exposes each remote tool as a typed tool through connection_search, and supports per-connection approval and tool allow/block. OAuth servers (Linear's and Figma's hosted MCP) use auth: defineInteractiveAuthorization — self-hosted, not the Vercel connect() helper, since Clankie does not adopt Vercel surfaces. Servers whose auth server cannot broker OAuth use auth: defineMcpTokenAuthorization (agent/lib/mcp-token-auth.ts): a static personal-access-token / API-key provider whose token the runtime sends as Authorization: Bearer <token>, read live per turn from a 0600 store (connections/mcp-tokens.json) or an env fallback, so a pasted token takes effect without a brain restart. Adding one is a small committed code change plus a dev-server reload; the model cannot add a connection at runtime, which is the point for credentialed services. GitHub is a curated connection alongside Linear and Figma (agent/connections/github.ts); it authenticates with a pasted GitHub personal access token (defineMcpTokenAuthorization) rather than OAuth, because api.githubcopilot.com/mcp's auth server does not support OAuth dynamic client registration (ADR-0021). Paste the token in the TUI via /mcp auth github (or set CLANKIE_GITHUB_TOKEN); it defaults to the version_control role for pull requests, diffs, branches, checks, releases, and code-host review. The Seatbelt-sandboxed host_command tool's gh stays the in-checkout local reader. Curated connections run a read/write approval split (gateWrites() in agent/lib/approvals.ts): read-shaped tool names (list_/get_/search_/ read_ prefixes, _read suffix) execute without a prompt, while writes and unknown shapes fail closed to a user-approval gate; CLANKIE_AUTO_APPROVE bypasses the whole split.

  2. First-party tools (agent/tools/) — capabilities we author and own: the herdr spawn seam, host_command (the Seatbelt-sandboxed host context shell, ADR-0003), browser_control (the custom browser-extension bridge), web_*, discord_*, memory_*, media, and mcp_export (bulk read-only MCP calls — curated connections via Clankie's own token stores, or dynamic servers — that spill the full result to a 0600 file under ~/.clankie/exports/ and return only path + preview, so board-wide reads never flood model context). Not third-party servers, just our code.

  3. Dynamic MCP bridge (mcp_list_tools / mcp_call / mcp_configure, agent/lib/mcp.ts) — runtime-added, no-auth or static-token MCP servers only: local tools, automations, a Minecraft MCP. Clankie or the user can add one on the fly via mcp_configure with no code change. Do not route OAuth or credentialed SaaS (Linear, Figma, finance) through this layer: its transports carry no OAuth provider, and storing long-lived tokens here is weaker than a connection's brokered auth. The spawned child-process env must be allowlisted, never the full process.env.

Decision rule: OAuth or a shared first-party credential → a connection. No-auth / runtime-throwaway / local → the dynamic MCP bridge. Something we wrote ourselves → a first-party tool.

Role bindings. Clankie's behavioral routing names logical roles, not vendors. Roles (work tracker, design tool, version control, and future ones like finance) map through a tri-state runtime binding: a role is bound to a concrete connection, explicitly none (the user chose no external provider for that role), or unconfigured (no decision yet). The persisted store is ~/.clankie/integration-roles.json; env overrides (CLANKIE_WORK_TRACKER, CLANKIE_DESIGN_TOOL, CLANKIE_VERSION_CONTROL) can name a connection or none. One connection can fill more than one role (GitHub serves work_tracker and/or version_control). The dynamic instruction (agent/instructions/integrations.ts, resolved on turn.started, read/written via agent/lib/integration-roles.ts) tells the model whether to use the bound connection, report the role as unconfigured, or honor the explicit none fallback (for work tracking, none means native-only because the native store is the unconditional floor). The custom face sets bindings with /integrations (lists roles, current bindings, available connections, and a none choice); the agent picks up the change on the next turn. instructions.md describes roles, so swapping Linear for another tracker, choosing no tracker, or Figma for another design tool is a binding change, not an edit to the persona.

PR sizing. The version-control settings also carry a preferred pull-request line limit (CLANKIE_PR_LINE_LIMIT, default 5,000 changed lines), set through /integrations version-control pr-line-limit <lines>. The dynamic integration instruction gives the model this target for PR planning while still allowing a larger cohesive PR when grouping is more effective.

Delegation profiles. The lead/worker role split is a manually selected behavior policy stored in ~/.clankie/delegation-profile.json, with one bit per role: implementer, designer, scribe, tester, and investigator, each assigned lead or worker. Presets are sugar over those five bits: solo sets all roles to lead; lead (the default) keeps designer and scribe with the lead and delegates implementer, tester, and investigator; hands-off sets all roles to worker. A bit pattern that matches no preset is shown as custom. Per-role env overrides (CLANKIE_DELEGATION_IMPLEMENTER, CLANKIE_DELEGATION_DESIGNER, CLANKIE_DELEGATION_SCRIBE, CLANKIE_DELEGATION_TESTER, CLANKIE_DELEGATION_INVESTIGATOR) layer over the store. The dynamic profile instruction (agent/instructions/profile.ts, backed by agent/lib/delegation-profile.ts) injects the resolved profile on every turn.started; /profile displays and writes the store. The same resolved profile is also threaded into herdr_spawn worker briefs so spawned workers see the active role split and the invariant that scribe changes drafting, not tracker authority. The relay profile.get and profile.set ops expose this store to the iOS settings UI, and include the profile-adjacent policy flags worker_subagents and auto_validation so the mobile surface can edit the whole delegation posture without parsing command-menu text.

Behavior policy flags. Runtime behavior policies live in ~/.clankie/policy-flags.json beside the delegation profile store, with env overrides layered by the resolver: CLANKIE_POLICY_PR_ONLY, CLANKIE_POLICY_MERGE_AUTHORITY, CLANKIE_POLICY_DESIGN_GATE, and CLANKIE_POLICY_WORKER_SUBAGENTS, CLANKIE_POLICY_AUTO_VALIDATION, CLANKIE_POLICY_PR_GRANULARITY, and CLANKIE_POLICY_PR_REVIEW_ESCALATION. Presets are sugar over per-flag truth: direct turns PR-only off, autonomous (the default) turns PR-only on and lets the lead merge after review/checks, and approval turns PR-only on, makes merges human-owned, and enables the advisory design gate. Non-preset combinations show as custom; contradictory combinations produce resolver warnings.

Each flag declares its enforcement class in agent/lib/policy-flags.ts. Guarantee-class flags have teeth only where the violating action crosses an ownable chokepoint: PR-only blocks git commit on, and git push to, the default branch through the host_command approval policy and threads the PR directive into worker briefs; merge authority blocks gh pr merge and the GitHub merge_pull_request MCP tool when set to human. A guarantee-class flag whose version_control platform layer is not bound renders as brain-enforced only. Design gate and PR granularity are advisory because no owned chokepoint can classify all design work or judge coherent PR grouping. Worker subagents, automated validation, and PR review escalation are also advisory: they shape worker briefs and lead instructions, keep large work stacked into size-capped PRs, wait briefly for external review, and fall back to lead-spawned review before merge. agent/instructions/policies.ts injects the resolved policy table on every turn.started; herdr_spawn adds the same policy context to worker briefs; /policies displays and writes the store.

Context sources. Clankie can optionally include host filesystem instruction files and installed skills in the same always-on context surface. The custom face exposes them through /context-sources (and /agent-md for the AGENTS.md compatibility commands). CLANKIE_AGENT_MD=1 keeps AGENTS.md ingestion opt-in: agent/instructions/agent-md.ts walks upward from CLANKIE_AGENT_MD_ROOT (or the brain working directory when unset), reads AGENTS.md / agent.md variants in parent-to-leaf order, and appends enabled files as a dynamic instruction on each turn.

Installed skills are discovered from CLANKIE_SKILL_ROOTS / CLANKY_SKILL_ROOTS, ~/.codex/skills, ~/.codex/skills/.system, ~/.claude/skills, and ~/.agents/skills, following symlinks with realpath. Enablement is stored in Clankie's own context-sources.json under CLANKIE_HOME; toggles never move, delete, copy, or rewrite source skill roots or instruction files. Built-in Clankie skills are locked, while installed skills and individual AGENTS.md files are optional. The enabled inherited inventory is exposed through /skills and relay list-skills; native clients can surface those as $skill invocations. If an inherited skill name collides with a Clankie-authored skill, the inherited skill is advertised with a non-colliding Eve runtime name while clients may still show the original $name.

10. Implementation status

  • Core runtime: eve agent, custom face, model selection, harness profiles, herdr spawn seam, the Seatbelt-sandboxed host_command context lane, transcript layer, relay channel, schedules, and dynamic MCP bridge are active surfaces.
  • Discord presence: Gateway-owned text presence, wake-name/free-will gate, per-channel presence sessions, pane mirror, bridge commands, voice join intent, user-token mode, and Go Live control layer are wired and verified offline.
  • Live-gated voice: the realtime voice loop, clankie:voice-* transcript pane, and ClankVox Go Live media forwarding require a Discord token, ClankVox, and OpenAI Realtime credentials for full live verification.

11. Open decisions

  • Host context access / two lanes — ACCEPTED (ADR-0003); host-command lane and GitHub connection role shipped. Clankie's own read_file/grep/bash stay sandbox-only (justbash, /workspace); host context flows through the Context lane's host_command tool (agent/tools/host_command.ts, agent/lib/host-command/*), adopting Codex's approval model (~/dev/codex): a raw command surface plus the host-command skill (not narrow schemas), full-disk read minus credential globs (.env*, ~/.ssh, ~/.aws) enforced by macOS Seatbelt (sandbox-exec), per-CLI network grants (pipelines of known reads + gh read subcommands), a classifier that auto-runs reads and gates mutating gh calls plus input-carried escalation (escalation{write|network, justification}) through agent/lib/approvals.ts. The approval ladder (/approvals: read-only → auto → yolo) tops out in a transient yolo — injected as CLANKIE_YOLO into the face-owned brain env, never persisted — and clamps to owner-driven turns: loopback eve clients self-declare via the x-clankie-surface header (face = owner; Discord presence/voice = not), unmarked surfaces fail closed to non-owner. The Work lane stays herdr_spawn. GitHub as a curated connection is bindable as work_tracker and defaults to version_control; /integrations also sets the preferred PR line limit for code-host planning. Full context in docs/adr/0003-context-access-two-lane.md.
  • Remote lifecycle / cold-start — ACCEPTED (ADR-0001). Remote cold-start uses the always-on supervisor below the brain with its own tailnet lifecycle op and install-minted credential, resolving the bootstrap paradox: the brain-minted relay token cannot boot the brain. Full context, options (keep-SSH-native-module vs supervisor vs hybrid), and the auth / install / Eve-down analysis are in docs/adr/0001-remote-lifecycle-cold-start.md.
  • Relay transport fit — RESOLVED (Phase 2): Herdr terminal attach with snapshot fallback. A raw eve WS channel carries live terminal screens via the attach op (§4.4). When the pane record includes terminal_id, the relay speaks Herdr's client-socket protocol directly, requests TerminalAnsi, and forwards rendered terminal frames to iOS. With older Herdr builds or panes that lack a terminal id, the relay falls back to the snapshot/native pane.attach compatibility path over supported read sources. Richer Herdr sources are provider capabilities, whether upstream or carried by the Volpestyle patch-stack fork (ADR-0017), while relay behavior stays in Clankie.
  • Shared-session iOS chat panes — SHIPPED (ADR-0009, supersedes ADR-0004). The Lead chat adopts the face's main thread through session-scoped relay face.status, and other native iOS chats materialize as one-pane interactive face tabs in the dedicated "Clankie" workspace via chat.attach / chat.close (§4.4, §5.6). The face pane is the tab root (no orphan shell, layout.apply), the continuation token is delivered out-of-argv, and the chat list remains device-persisted with per-chat status reconciled from the live workspace tree. Backend and iOS move together with no chat.mirror compatibility shim. Full context in docs/adr/0009-shared-session-chat-interactive-face-panes.md.
  • Face surface — RESOLVED: custom face. eve's stock TUI has a fixed, non-extensible slash-command set, so the face is scripts/clankie.ts (pnpm dev) on the public eve/client: it renders eve's event stream through pi-tui (@earendil-works/pi-tui), matching eve's look only where useful, and adds the slash commands eve can't (the live list is /help). It attaches to a running eve server or spawns/owns a headless one (eve allows one dev server per agent).
  • Memory store — RESOLVED: eve session context + a Clankie memory lib. Durable memory is owned by the eve agent store — eve carries per-session conversation history and shares it across a root agent's sessions — and a Clankie memory lib (agent/lib/memory.ts, agent/lib/runtime-memory.ts) persists cross-session facts to <CLANKIE_HOME>/memory/facts.json under the agent's resolved data paths.
  • Performer selection — RESOLVED: explicit coding harness profiles. /harness allow selects the allowed coding pane profiles; each spawn chooses a harness explicitly. /harness configures each launchable profile's native-vs-Ollama model preference without importing those runtimes as core dependencies.
  • Codex provider route (§4.6) — RESOLVED: route (a). A live spike confirmed the stock @ai-sdk/openai .responses() model works against the Codex backend with the subscription OAuth token, given instructions + store:false + streaming. No custom LanguageModelV2 needed; route (b) is fallback-only. Remaining risk is backend drift, not feasibility.