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-clankie →
scripts/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. TheStageProviderseam 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, oropencode) 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-herdris 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 aStageProviderseam 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:
soloallows reasonable inline work, whileleadandhands-offbias 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-herdris 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; theStageProvidercapability 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/clientpi-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, oropencodeagents started through the active stage adapter. (Herdr starts them withherdr 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…>
- discovery:
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 need | eve primitive |
|---|---|
| Discord text in/out | agent/channels/discord-gateway.ts + agent/channels/discord.ts |
| Voice in/out | agent/channels/voice.ts + ClankVox media |
| Linear delegation in/out | agent/channels/linear.ts (native Agent Sessions, ADR-0015) |
| Durable session state | eve sessions + continuationToken |
| Scheduled/autonomous runs | agent/schedules/*.md (cron frontmatter) |
| Slash-command execution | headless command host (scripts/clankie.ts --command-host) registered with the relay |
| Visible face | custom face on eve/client (clankie dev while editing, pnpm dev direct) |
| Memory | eve 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.
| Need | Source |
|---|---|
| Running, idle, blocked, done | Herdr |
| 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 spawn | herdr_watch |
| Send text or keys | Herdr |
| Current TUI screen | Herdr visible |
| Recent terminal screen buffer | Herdr recent / recent_unwrapped |
| Full history of a wrapped worker | Clankie worker transcript |
| Full history of any other pane (iOS-created, manual, mirrors) | Clankie pane recording (§4.3.1) |
| Historical worker output | Clankie transcript when capture is enabled |
| Cross-agent audit trail | Clankie transcript + pane recordings |
| Full-text search across all of the above | herdr_search / clankie transcript search |
| One-pane harvest brief | clankie 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/<session>/<br/><agent>/<run>/ worker transcripts (in-path wrapper)<br/>panes/<recording>/ 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 pairprints aclankie://connect?relayUrl=…&token=…&mode=tailnetdeep link as a terminal QR. When the supervisor is installed, the same link also carriessupervisorUrl=…&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 --linkprints 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, andPOST /lifecycle/down. The supervisor reuses the shared lifecycle core inagent/lib/lifecycle.tsto ensure theclankiessession exists and Clankie's brain (eve dev --no-ui) runs as a pane.clankie supervisor install|status|uninstall|rotate-tokenmanages the launchd plist and lifecycle token.scripts/clankie-up.tsremains 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?}whereplatformisiosorandroid; omitted platform defaults toiosfor the existing iOS client), persisted aspush-tokens.jsonunder the Clankie data root (CLANKIE_HOME, default~/.clankie; a legacy~/.config/clankieregistry 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.listevery 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 onCLANKIE_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 onCLANKIE_FCM_SERVICE_ACCOUNT_PATH(orGOOGLE_APPLICATION_CREDENTIALS) plus optionalCLANKIE_FCM_PROJECT_ID, or the env-onlyCLANKIE_FCM_PROJECT_ID/CLANKIE_FCM_CLIENT_EMAIL/CLANKIE_FCM_PRIVATE_KEYtrio. Both senders no-op when unset. The face's/pushcommand is the local APNs setup surface and push status/test surface: it stores only the.p8path, 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:recurringrows carry a croncadence, whileoncerows carryfire_at, optionalcontext, and completion fields. Legacy rows without a kind normalize torecurring. The dispatcher is off by default and starts only whenCLANKIE_FOLLOWUPS=1; the face's/followupscommand 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-basedclaimDueSchedulespath. Completion recordslast_run,last_outcome,last_error,last_session_id, clearslease_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 surfacefollowup, mirrored as aclankie:followup-*presence pane when a session id exists. Its final reply is not delivered automatically; the prompt requires an outbound tool such aspush_notify, Discord, worktracker, or a connected MCP.push_notifyfans out through the same APNs/FCM registry with event kindfollowup. Completed one-time rows prune afterCLANKIE_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/orchestrationops alongside transcript-awareread/send/run/keys/subscribeand a rawapipassthrough.chat-read/chat-tail/chat-untail(ADR-0016) serve a pane's conversation as normalized@volpestyle/clankie-contractpane-chat turns read from the harness's native session files (Claude/Codex jsonl, OpenCode sqlite viaagent/lib/pane-chat/), degrading toplain-tailtext from worker transcripts, pane recordings, or livepane.read, and carrying honestsource/fallbackfacets.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 onlyworkspace_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 throughface.status {session?}→{attached, session_id?, continuation_token?, herdr_session?, title?}when the face publishes identity overface-attach. The publishedherdr_sessionmust 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 callchat.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_idexisting, elseworkspace_labelfind-or-create) or the default "Clankie" workspace. The pane is the tab root, materialized vialayout.applywith no orphan shell; its command isscripts/clankie.ts --session <id> --label <slug>, and the continuation token is delivered asCLANKIE_CHAT_TOKENin 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-contractfor relay and Eve session wire shapes. The agent owns the API; the relay imports@volpestyle/clankie-contractfrom the top-level workspace package (agent/channels/relay.ts, implementation inagent/lib/relay/*) to validate inbound relay requests.../clankie-iosconsumes the same workspace package for local and EAS builds. Inbound relay frames are additionally capped atMAX_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 againstpane.listonly at serve time. The relayorchestration {session?}op returns a full snapshot:{type:"orchestration", edges, runs, summaries, activity, run_statuses}. A relaysubscribeentry{type:"orchestration"}is handled locally and streamsorchestration.edge_upserted,orchestration.edge_removed,orchestration.summary_updated,orchestration.activity_updated,orchestration.run_status_updated,orchestration.run_status_removed, andorchestration.resetframes 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 afterCLANKIE_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-LLMPaneActivity {pane_id, kind, intensity?, updated_at, revision?}.kindis an open string with known valuesthinking,reading,editing,running,searching,spawning,waiting-on-child,awaiting-input, andidle; clients treat activity older than 10 seconds as stale and fall back to pane status. The producer deriveswaiting-on-childfrom live watch edges, seam-spawned worker activity from transcript recency, and everything else from Herdragent_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 default —CLANKIE_PANE_SUMMARIES(off|auto|always) opts in, configured from the face with/summaries(mode, plus a provider/model override viaCLANKIE_SUMMARY_PROVIDER/CLANKIE_SUMMARY_MODELthat 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-openattachstream plus rawwrite,resize, andscrollops: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. Whenterminal_idis present, the relay connects to Herdr's client socket (herdr-client.sock), requestsTerminalAnsi, and also sends a full ANSI-preservingpane.readsnapshot ofsourcefirst 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 sendsAttachTerminaloutput 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/rowsand 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 (forkClientMessage::SubscribeInputState) and streams them as{id, ok:true, stream:true, body:{type:"pane.input_state", pane_id, terminal_id, wheel_routing}}withwheel_routing∈host_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_scrollmeans the client scrolls its locally seeded scrollback; the other two mean a fullscreen app owns scrolling and the client forwards gestures with thescrollop. Input-state frames bypass the backpressure guard and the pre-snapshot buffer. Every outgoingpane.outputbody also carriest_frame(serverDate.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 withTCP_NODELAY: eve's Node WS stack — crossws over the vendoredws— callssocket.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 nativepane.attachbyte chunks when available, otherwise snapshot polling.source:"full"requests all retained Herdr scrollback when that Herdr build supports it; vanilla builds withoutfullfall back to cappedrecent_unwrappedoutput, whilerecentremains bounded bylines. The iOS app exposes this as Terminal mode Mirror.detach {pane?}ends one stream (or all). A peer may hold oneeventssubscription plus oneattach:<pane>stream per open pane concurrently. write {pane, text, t0?}writes verbatim bytes to the PTY master with no trailing Enter (unlikerun/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 asClientMessage::Inputon that persistent Herdr client socket — the low-latency keystroke path, no per-op API connection — and the reply carriesresult:{type:"ok", via:"stream"}; otherwise it falls back to Herdr'spane.send_textAPI.keysalways stays on the API socket becausepane.send_keysis terminal-mode-aware. The relay serializeswrite,keys,run, and pane-targetedsendby 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 inagent/lib/relay/ordered-input.ts).writeandkeysaccept an optional numerict0(client-clock ms) that the relay echoes into itsCLANKIE_RELAY_TRACE=1latency trace log.resize {pane, cols, rows}→{id, ok:true}resizes the server-owned terminal of the peer's live Native attach stream in place (HerdrClientMessage::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 (HerdrClientMessage::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.linesis a batched step count (Herdr caps 128 per message);column/roware the 0-based gesture cell for mouse-report encoding. Clients send it only when the streamedpane.input_staterouting is nothost_scroll; same no-live-stream error shape asresize.ping {}→{id, ok:true, result:{t}}(serverDate.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. thepingop — 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. Failedpeer.sends and WSerrorhooks 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 receivesmenu.cancelso 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}.directiveis 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 ClankVoxtransportStatswhen present.transportStatsis the lasttransport_statsIPC event with areceivedAttimestamp; 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 sharedLatencyWindowshape 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[("<CLANKIE_HOME>/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.tsis a server-side eve hook onstep.completed(andstep.startedfor 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?}.sourcepreserves the normalized eve channel kind whilechannelis the analytics bucket;runId/workerare 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/stepIndexlet 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.tsfolds 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-dayworker_settledoutcome counts (joined from the orchestration log) and run counts (fromherdr-runs/manifests). Theagent/schedules/usage-rollup.tshandler 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 callscompactUsageLogon the same cadence.buildStatsSnapshot(range)assembles{type:"stats", range, generatedAt, rangeStart?, tiles, previous?, dailyOutcomes, channelMix, modelSpend, runs}forrange7d|30d|allby 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.previouscarries the same tile fields for the preceding fixed-size window and is omitted forall. Eachrunsentry joins the run manifest with best-effort outcomes, run-scoped token/cost totals, per-worker usage buckets, andlead_turn_settledphase history. It is served both as the relaystats {range?, session?}op and the frontdoor-authed REST siblingGET /relay/stats?range=. Cost is derived downstream fromMODEL_USAGE_COSTSinagent/lib/config-defaults.ts; tokens are the primary unit and$is additive (subscription/local models are0; 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.
| Skill | Audience | Purpose |
|---|---|---|
herdr (stage adapter) | every agent in a pane | flat full-picture literacy: discover/read/message/wait/report-agent |
clankie-lead | coordinator only | opinionated harvestable fan-out: run dirs, manifest, sentinel files, spawn/harvest/cleanup |
clankie-worker | spawned workers | worker-side stage coordination: identity, peer messaging, blocker escalation, result/sentinel discipline |
clankie-work-tracker | leads and workers | provider-neutral durable work protocol: lead-authored scope/verdicts, worker narrative, status lifecycle, review hygiene |
- The
herdrskill 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-leadonto workers; it is the hub-role protocol. Workers carryherdrplusclankie-workerfor their side of the completion and reporting contract. - Keep work-tracker provider mechanics shared.
clankie-work-trackerhas 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
clankie supervisor installwrites the launchd plist and launchd keepsscripts/clankie-supervisor.tsalive.- The supervisor exposes the tailnet lifecycle surface and, on
up, ensures the persistent herdr session exists:herdr --session clankies. - A pane in
clankiesruns Clankie's headless command host (scripts/clankie.ts --command-host; also reachable through localclankie up). The host owns or attaches the headless eve Clankie brain (eve dev --no-ui; channels listening, schedules armed). - Optional visible faces (
clankie devorpnpm dev) attach to the same brain overeve/client. - 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_spawnperformers 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 theclankie:voice-<slug>performer panes the voicedelegateop 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:
| Surface | Session | Clogs main thread? | Watchable as |
|---|---|---|---|
custom face (scripts/clankie.ts) | main | — (it is the main thread) | the face pane |
| Discord text | per-channel presence | no | clankie:discord-* mirror |
| Discord server voice / private call | voice presence | no | clankie:voice-* mirror |
| iOS Lead chat | face's main thread (session-scoped relay face.status) | — (same as face) | the face pane |
| iOS other chats | per-chat session (relay chat.attach) | no | an 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, oropencodeprocess started withherdr agent start(viaherdr_spawn). Its own process, its own pane, fully interactive and watchable, coordinating through theherdrskill. This is how Clankie (or a presence session) does profile-delegated, parallel, specialized, or otherwise watchable work. - eve child sessions — eve's native
agenttool / 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 intoclankie: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-<issue> 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.
clankiessession = 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 loadsclankie-leadto 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_commandsandbox 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-herdris 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
herdrskill. - Volpestyle's Herdr patch-stack fork when fork-carried provider capabilities are required (ADR-0017).
We build:
- The Clankie eve agent — instructions, persona, tools, memory lib, Discord and voice channels, schedules.
- The eve → herdr-pane spawn seam — the lead agent surfaces inbound and
background work as
herdr agent startpanes (5.2). This is the core integration. - The Clankie transcript layer — local append-only transcripts for
Clankie-spawned workers, exposed through
clankie transcriptand transcript awareherdr_read. - The eve relay channel — raw WS route bridging the iOS app to Herdr's local API and terminal attach sockets (4.4).
- The iOS client surface targets the relay contract.
- The presence self-report convention agents use through the
herdrskill.
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.
-
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 viadefineMcpClientConnection(ordefineOpenAPIConnection). eve brokers auth, keeps the URL and credentials out of the model's reach, exposes each remote tool as a typed tool throughconnection_search, and supports per-connection approval and tool allow/block. OAuth servers (Linear's and Figma's hosted MCP) useauth: defineInteractiveAuthorization— self-hosted, not the Vercelconnect()helper, since Clankie does not adopt Vercel surfaces. Servers whose auth server cannot broker OAuth useauth: defineMcpTokenAuthorization(agent/lib/mcp-token-auth.ts): a static personal-access-token / API-key provider whose token the runtime sends asAuthorization: Bearer <token>, read live per turn from a0600store (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, becauseapi.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 setCLANKIE_GITHUB_TOKEN); it defaults to theversion_controlrole for pull requests, diffs, branches, checks, releases, and code-host review. The Seatbelt-sandboxedhost_commandtool'sghstays the in-checkout local reader. Curated connections run a read/write approval split (gateWrites()inagent/lib/approvals.ts): read-shaped tool names (list_/get_/search_/read_prefixes,_readsuffix) execute without a prompt, while writes and unknown shapes fail closed to a user-approval gate;CLANKIE_AUTO_APPROVEbypasses the whole split. -
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, andmcp_export(bulk read-only MCP calls — curated connections via Clankie's own token stores, or dynamic servers — that spill the full result to a0600file under~/.clankie/exports/and return only path + preview, so board-wide reads never flood model context). Not third-party servers, just our code. -
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 viamcp_configurewith 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 fullprocess.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_commandcontext 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/bashstay sandbox-only (justbash,/workspace); host context flows through the Context lane'shost_commandtool (agent/tools/host_command.ts,agent/lib/host-command/*), adopting Codex's approval model (~/dev/codex): a raw command surface plus thehost-commandskill (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 +ghread subcommands), a classifier that auto-runs reads and gates mutatingghcalls plus input-carried escalation (escalation{write|network, justification}) throughagent/lib/approvals.ts. The approval ladder (/approvals: read-only → auto → yolo) tops out in a transient yolo — injected asCLANKIE_YOLOinto the face-owned brain env, never persisted — and clamps to owner-driven turns: loopback eve clients self-declare via thex-clankie-surfaceheader (face = owner; Discord presence/voice = not), unmarked surfaces fail closed to non-owner. The Work lane staysherdr_spawn. GitHub as a curated connection is bindable aswork_trackerand defaults toversion_control;/integrationsalso sets the preferred PR line limit for code-host planning. Full context indocs/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
lifecycleop 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 indocs/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
attachop (§4.4). When the pane record includesterminal_id, the relay speaks Herdr's client-socket protocol directly, requestsTerminalAnsi, 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/nativepane.attachcompatibility 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 viachat.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 nochat.mirrorcompatibility shim. Full context indocs/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.jsonunder the agent's resolved data paths. - Performer selection — RESOLVED: explicit coding harness profiles.
/harness allowselects the allowed coding pane profiles; each spawn chooses a harness explicitly./harnessconfigures 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, giveninstructions+store:false+ streaming. No customLanguageModelV2needed; route (b) is fallback-only. Remaining risk is backend drift, not feasibility.
