ADR 0208: Agents carry a role; the world reads it
Status: accepted (James, 2026-10-02). Applies ADR 0203 and stays inside ADR 0188.
Context
The app's second version shows the swarm as a pixel world where agents work at
role stations. It needs three facts the host did not carry: what each agent is
for, whether it is fanning out into native subagents inside its own TUI, and
which work items belong at each station. The persona already had an
appearance.accessory with role-like names, but that field is cosmetic and
chosen for variety by a hash; a station cannot be keyed on it.
Under ADR 0203 this passes on both counts. The world is part of Clankie's character, and neither Claude Code nor Codex gives the owner one view of a mixed fleet's roles, fan-out and backlogs. Better models would not absorb it: the facts belong to the owner's arrangement, not to any one harness.
Decision
A persona carries an optional semantic role: planner, designer,
builder, tester, reviewer or researcher. Absent means unassigned. It
persists in the persona store with the character, survives seat moves, and is
independent of appearance.accessory. The owner sets it with the
set_persona_role operator op (steer grant), clankie agents role, or /agents role in the TUI; Clankie or the owner can set it at hire time with the
optional role on hire_agent and spawn_seat. Nothing infers a role.
Custom roles are allowed (amendment below).
A fleet seat may carry subagents: a running count and up to eight recent
labels. It is derived from the harness's own transcript through
@clankie/agent-transcript. For Claude Code, an Agent/Task call with no
result is running; a background call answers at once with async_launched and
finishes when its <task-notification> lands in the parent journal; a
foreground call is treated as ended once the main thread speaks again in a new
message. The read is incremental, so a fleet read costs the append. A cold read
covers at most the last 2 MiB.
Codex is covered by the same reader and seat path (VUH-1531, 2026-10-04).
Its rollout header names the child's own id, parent_thread_id, nickname
and task path; session_id can name the parent. Discovery processes only
headers in the addressed parent's Codex home, then tails only direct children.
Labels combine nickname and task path. The parent journal's native
FINAL_ANSWER envelope, targeted wait/close results, and per-child
list_agents statuses settle children within that fleet read. A generic
wait_agent acknowledgement identifies no child and settles none. Successful
followup or a newer child task_started resets completion; interruption is
ended. When no current parent status exists, five minutes without a child file
write means done as a heuristic; a quiet running tool may be misclassified.
A current explicit running status takes precedence over idleness.
Recent entries also carry a stable native id, startedAt, and endedAt
when available (VUH-1607, 2026-10-04). Claude uses the native Agent/Task
call ID; Codex uses the child's own thread UUID and rollout creation time.
Claude ends at a parent result, background notification or the next main
assistant message; Codex ends at its parent collection/status record. An idle
Codex ending uses the last file-write time plus the five-minute idle threshold,
so it is an estimate. Running entries omit endedAt; reopening preserves the
native ID. Missing/invalid native timestamps remain absent. These fields are
optional in the protocol so entries from older hosts and journals still validate.
OpenCode is covered through the same seat enrichment path (VUH-1586,
2026-10-04). Its registered local worker profile supplies a bounded SQLite v1
projection through the existing native history reader; no owner-wide database
discovery is added. A task part uses its native call ID for identity, its
subagent_type and description for the label, and state.time.start/end for
timing. A pending part can use its native creation time before a start is
recorded. Continuations into the same child session have distinct call IDs.
Child references must name a direct child in the same registered profile and
working directory. Completed/error foreground calls are done. A background
tool return whose native envelope says state="running" remains running until
the parent's synthetic completion/error notification; its message creation
time supplies the ending. There is no SQLite-file idleness heuristic.
OpenCode retains the history adapter's schema check, with 1.18.18 as a release floor since VUH-2417 (later columns and migrations are admitted). Each read selects at most 500 recent parent messages, 2,000 parts and 4 MiB of payload, then retains 64 task calls and emits eight recent entries. Older tasks outside that window and v2-only history remain unsupported. The inspected installed 1.18.34 store contained historical 1.17.13 task records, not fresh 1.18.34 subagents; the sanitized shapes and source-pinned background semantics are documented in the inspection record.
To respect ADR 0188, discovery alone never reads a transcript. The host reads only seats it already has an address for: one it hired or whose chat the owner opened. It reads only local seats. It stores nothing and imports no entries, and the derived summary contains labels, not content. Absent means unknown, not zero. Remote fleets are left absent because each read would cost an SSH round trip per seat. The projection is bounded to 64 recent children per parent, 32 parent sessions, 4,096 recent rollout headers and 64 KiB per header. Older children or parent completion records outside these windows are not guaranteed.
The app can select a recent entry with a native id as a tray subject
(VUH-1532/1533). subagent_replay reads it through the parent's existing
seat/persona conversation and the chat grant. It returns the ordinary normalized
replay page without creating a child conversation or granting send, resume or
terminal control. Count-only entries from older hosts remain display-only.
Unknown optional response fields are discarded by the client's tolerant reader;
known fields and requests retain strict validation.
Concurrent roster readers share one refresh. Read-only callers may reuse its completed projection for one second while the fleet change cursor is unchanged. An authority-sensitive or post-mutation read forces a census started after any older refresh; native admission never relies on the cached roster.
Codex child discovery uses asynchronous filesystem reads and a bounded cache per parent session. Parent rollout and discovery-directory identity, size and modification time invalidate it. Known child journals still detect appended activity and idle transitions. Unchanged sessions avoid repeated tree walks; an unrelated existing date folder is rechecked within two seconds of another read, covering a child that appears without a parent append. The cache supplies display observations, never child-history authority.
The same native readers resolve the selected child. Claude maps the parent call
ID through its matching toolUseResult.agentId, then checks the derived nested
child journal's parent and agent IDs. A call without that recorded locator is
unavailable; ordinary parent reads still exclude sidechains. Codex reuses the
roster's direct-child header discovery and checks both headers before each byte
read. OpenCode resolves the task call inside the same registered profile and
checks its direct-child database relation again after reading. Labels and list
positions never select files. Parent changes, foreign cursors and escaped paths
refuse instead of reading another child. A remembered native source allows
history after a parent pane closes. Remote child history remains unsupported.
Expanded trays lazily read a bounded native window (up to 500 entries and the existing 4 MiB file/database cap), page within it and refresh while the child is running. Collapsing stops those reads. Missing source history has a read-only unavailable explanation, never a writable fallback.
A work item may carry labels from its backend: Linear labels, GitHub
labels (minus the status: … labels the GitHub backend writes), or Markdown
labels: front matter. Listing accepts one label filter, matched
case-insensitively against any label. A station shows the items labelled with
its role. Labels are read-only through this contract. An owner labels work in
the tracker itself.
Alternatives
- Reuse
appearance.accessory: it is cosmetic, hash-assigned and has a different vocabulary. Overloading it would turn every avatar edit into a reassignment. - Push subagent state from Claude Code hooks: this would need new hook events, a service route and per-seat state, and it would cover only plugin-launched workers. The transcript already records the facts.
- Read subagents for every roster seat: this violates ADR 0188 for panes that are not working for Clankie.
- A separate role-to-backlog mapping: labels already exist in every tracker, and the role name is the label.
Consequences
- The app places agents by
persona.roleand reads a station's backlog withwork_items { label: role }. - An unaddressed or remote seat shows no subagents. The world treats that as unknown.
- A Claude subagent started before the 2 MiB cold-read window is not counted. Codex children remain discoverable by header within the bounds above; parent completion outside the cold-read window falls back to child file idleness.
Amendment: custom roles (James, 2026-10-02)
James asked for custom roles. The role is now an open, validated string. The six
built-ins stay as suggestions (OPERATOR_AGENT_ROLES). Any role is 1–24
letters, digits, spaces and hyphens, and starts with a letter or digit.
Parsing trims the role and collapses inner whitespace. A built-in in any casing
folds to its lowercase name. A custom role keeps the owner's casing for display
and compares case-insensitively (operatorAgentRoleKey), so Sound Designer
and sound designer are one role and one station. Personas stored with an
enum role remain valid, because every built-in is a valid string.
A read-only roles operator op lists roles as { role, builtIn, count }. It
returns the six built-ins first, always and with counts, then the custom roles
personas hold, most held first. A custom role is labelled with the casing of
its most recently updated holder. clankie agents roles prints it. It is an op
rather than a fleet field so the long-polled snapshot does not grow. The app's
picker reads it on open.
A custom role's station reads work items labelled with that role, using the same case-insensitive label match.
Amendment: roles are not a pipeline (James, 2026-10-14)
James asked whether planner, builder and tester roles are backed by agent coordination research. As a relay of personas handing work down a chain, they are not. What the research does support is splitting work into separable results under one coordinator, verification that brings new information, and review that does not share the producer's context or model.
- Roles stay what this ADR made them: a station, a backlog label and, under
ADR 0216, a hire profile.
They define no coordination protocol. The
leadskill splits work by separable result, not by phase, and thehire_agentrole description says so. - Project proposals no longer add
testerbecause a repository has a test suite. The suite is the builder's own check; independent review is the reviewer's.testerremains a built-in for an owner who wants one, for example live play-testing that needs its own seat or device. - The lead's review guidance defines independence by mechanism: brief the reviewer from the acceptance and the artifact rather than the producer's transcript, settle behavior by running the real thing, and prefer another model family for a consequential change when usage allows.
Evidence as of this amendment:
- Persona prompts alone do not improve accuracy (Zheng et al., Findings of EMNLP 2024).
- Across 1,600+ traces from seven multi-agent frameworks, including role-based MetaGPT and ChatDev, failures came mostly from system design and inter-agent misalignment rather than model limits (Cemri et al., NeurIPS 2025). In multi-agent code generation, 75.3% of failures trace to the planner-to-coder handoff (arXiv 2510.10460).
- Across 180 configurations, a central orchestrator gained 80.8% on parallelizable tasks, while every multi-agent variant degraded sequential reasoning by 39–70%. Independent agents amplified errors 17.2×, against 4.4× with an orchestrator checking outputs (Google Research, arXiv 2512.08296). Anthropic reports its orchestrator-worker research system beat a single agent by 90.2% at about 15× the tokens of a chat, and that most coding tasks are less parallelizable than research (2025).
- Verification helps when it adds information: tests written apart from the code and actually executed (AgentCoder). Self-correction without external feedback does not (Huang et al., ICLR 2024), and LLM evaluators favor their own generations (Panickssery et al., 2024).
Most of these results come from function-level benchmarks or the authors' own evaluations. Long-running repository work by frontier-model fleets is barely studied, so Clankie's own delivery outcomes (rework, missed defects, accepted results) should revise this when they disagree.
