tmux-pilot Role Authoring
A role is two artifacts:
- A YAML entry under
subagent-roles.<name>in the entry config file —$PI_CODING_AGENT_DIR/tmux-pilot.config.yaml, falling back to~/.pi/agent/tmux-pilot.config.yaml. - An optional agent-definition Markdown document (YAML frontmatter +
instruction body) referenced by the role's
agent-definitionkey.
Config is read at spawn time — edits apply to the next spawn, never to a running agent (widget-style is captured on the record at spawn; startup-scoped widget settings apply next session). Role names are matched case-insensitively at spawn.
The canonical, exhaustively commented schema reference is
tmux-pilot.config.example.yaml in the tmux-pilot repo. Templates:
resources/role-config-template.yaml and
resources/agent-definition-template.md.
Naming and schema rules (hard cutover — no legacy keys)
Every tmux-pilot-owned key is lower-kebab-case; controlled enum values too
(overlay, above-editor). External identifiers keep their spelling (model
IDs, provider IDs like opencode-go, harness IDs, role names, tmux keys like
C-c). Legacy camelCase keys (agentDef, guards, new-worktree,
config-dir, maxTurns, stopKeyCombo, …) are not recognized — they
warn and are ignored. Unknown keys at known scopes warn; role names, model
IDs, harness IDs stay open and never warn.
Part 1 — the YAML role entry
Inheritance model (memorize this first)
Every defaultable field resolves model-level > role-level > subagent-global-defaults > hardcoded:
thinking,provider,harness— all three layers.agent-definition,models— role > global only (no per-model form).modelsis a full replacement: a role with its own list loses all global entries. Nomodels:key → inherit the global list.lifecycle-guardsis a shallow per-field merge (role field > global field > hardcoded), EXCEPTturn-thresholds, which is replaced wholesale — defining any role thresholds discards the global list.worktree-enabled— role > global >false. Not allowed per-model.
Role keys
subagent-roles:
reviewer:
thinking: high # off|minimal|low|medium|high|xhigh|max
provider: opencode-go # pi provider id (default for bare-string models)
harness: pi # pi (default) | cmd | claude-code | codex | hermes
agent-definition: reviewer.md
worktree-enabled: false
models: # ordered fallback — first available wins
- deepseek-v4-flash # bare string: inherits role/global
- claude-sonnet-4-20250514: # object: per-model overrides
provider: anthropic
thinking: high
- mimo-v2.5:
harness: cmd # per-model harness override
lifecycle-guards:
max-turns: 25 # 0 = unlimited (hardcoded default)
grace-turns: 5 # extra turns past max before forced stop
session-file-timeout-ms: 60000
max-runtime-ms: 0 # 0 = disabled (default) — recommended
staleness-timeout-ms: 0 # 0 = disabled (default) — recommended
turn-thresholds: # REPLACES the global list wholesale
- at-turns-remaining: 5
action:
type: steer
prompt: >
You have {{turnsRemaining}} turns remaining. Wrap up.
# cleanup-linger-ms: 30000 # keep finished window visible
# harness-config-directory: { path: ~/curated/reviewer, mode: overlay }
# widget-style: { role-color: "#c98f5f", portrait: { glyph: "⚖" } }
# webui-theme: role-pack:inquisitor # web-console presentation only
# sandbox: { enabled: true, filesystem: { write: ["."] } }
Notes that prevent real mistakes:
session-poll-interval-msis global-only (undersubagent-global-defaults.lifecycle-guards) — a role-level value is ignored.- Leave
max-runtime-msandstaleness-timeout-msat 0 unless you specifically want wedge detection; completion is driven by the per-harness lifecycle-end sentinel, and staleness kills agents mid-long-tool-call. - Threshold prompts get
{{turnsRemaining}}and{{agentId}}template variables; the only actiontypeissteer. sandboxmerges per-leaf over the globalsandboxblock; arrays replace wholesale. Linux x64 only, opt-in.
Harness-specific constraints
| Harness | Constraint to respect in the role entry |
|---|---|
pi |
Default; full routing support. |
cmd |
Full routing; config dir redirect is HOME-based. |
claude-code |
Anthropic model ids only (haiku/sonnet/opus/…); provider is ignored — never write one expecting a gateway. |
codex |
OpenAI-only; provider ignored with a warning; thinking maps to model_reasoning_effort (off→minimal, xhigh→high); account-global ChatGPT rate limits — max-concurrent-agents is the lever. |
hermes |
model/provider map verbatim to -m/--provider; no harness-config-directory support (warned-and-ignored); a user-set hermes max_concurrent_sessions can fail parallel spawns. |
Harness-scoped defaults (stop-key-combo, cleanup-linger-ms,
harness-config-directory) live under harness-global-defaults.<id>, not in
the role; the role-level harness-config-directory / cleanup-linger-ms win
over them when both are set.
Imports
The entry file may imports: other YAML files (paths relative to the
declaring file; globs sorted; maps deep-merge, lists replace, later wins,
entry file wins last). Duplicate role names warn, later declaration wins.
Relative agent-definition paths always resolve from the ENTRY file's
directory, never from the imported file that declared them.
Part 2 — the agent-definition Markdown document
What it is
Runtime role-instruction input. The body is delivered to each new
matching subagent ahead of its task; frontmatter contributes only
description (surfaced in role listings and the web console — every other
frontmatter key is ignored). All current harnesses use prompt-context
delivery: the composed spawn prompt is
[worktree context, if any]
[parent conversation context, if inherited]
# Role Definition
Role: <role>
Source: role|global
--- BEGIN ROLE INSTRUCTIONS ---
<your body, verbatim>
--- END ROLE INSTRUCTIONS ---
# Your Task
<the spawn prompt>
Write the body knowing it lands between those markers: address the agent in second person, define scope/outputs/constraints, and don't restate the task — the task follows it.
Shape
---
description: One-line summary shown in role listings and the web console.
---
You are the <role> agent. <instructions…>
Frontmatter is optional — a plain body with no --- block is valid. If the
file DOES start with ---, the frontmatter must be well-formed:
- Opening
---on its own line, closing---on its own line. - Content between them must parse as a YAML mapping (not a list/scalar).
Validation (spawn-rejecting — fail closed)
A configured definition that is missing, unreadable, not a regular file,
not valid UTF-8, larger than 64 KiB, has malformed frontmatter, or has an
empty body rejects the spawn before any tmux window is created, with a
RoleDefinitionError naming role, source, reference, and resolved path. There
is no fall-through to "spawn without instructions." The document is captured
immutably (content + sha256) at spawn — editing the file never affects a
running agent.
When debugging a rejected spawn, check in order: file exists at the resolved path → regular file → ≤ 64 KiB → UTF-8 → frontmatter delimiters/YAML-mapping → non-empty body after frontmatter.
Path resolution
agent-definition values resolve as:
~/…or~— home-expanded, used as-is.- Absolute path — used as-is (ignores
agent-definition-directory). - Relative path — resolved against
subagent-global-defaults.agent-definition-directory, which itself may be absolute,~-prefixed, or relative (relative resolves against the entry config file's directory /PI_CODING_AGENT_DIR). Unset → the entry config directory itself.
A role without agent-definition inherits the global
subagent-global-defaults.agent-definition-directory + global
agent-definition if one is set; multiple roles may share one document.
Workflow
- Read the current entry config (
~/.pi/agent/tmux-pilot.config.yamlor$PI_CODING_AGENT_DIR/tmux-pilot.config.yaml) and anyimports:it pulls in before editing — respect existing global defaults instead of restating them per-role, and check the role name isn't already declared in an import. - Write/edit the role entry with only the fields that differ from inherited defaults. Kebab-case; no legacy keys.
- If the role needs standing instructions, create the
.mdunder theagent-definition-directoryand reference it by bare filename; otherwise omitagent-definitionentirely. - Sanity-check: valid YAML; model entries either bare strings or single-key maps; thinking values from the enum; harness constraints from the table above; definition file passes the validation checklist.
- Remind the user: changes apply on the next spawn (no restart needed for
routing/guards; widget placement/toggle keys need a new session). The
/tmux-pilot:webuiconsole edits the same entry file with validation, and is the recommended place to repair a definition a spawn rejected.