| name | mesh-builder |
| description | Build TX V4 meshes - agent configs, prompts, routing. Use for new meshes, agent roles, or multi-agent workflows. Triggers - mesh, routing, agents, multi-agent, config.yaml |
Mesh Builder
Build meshes (agent workflows) for TX V4.
Quick Start
tx prompt <mesh> <agent>
tx prompt narrative-engine narrator
tx prompt dev --raw
Documentation
| Topic | Location |
|---|
| Config fields | docs/mesh-config.md |
| FSM (state tracking) | .ai/docs/mesh-fsm-config.md |
| Available meshes | docs/meshes.md |
| Message format | docs/message-format.md |
Minimal Config
mesh: example
description: "What this mesh does"
agents:
- name: worker
model: sonnet
prompt: prompt.md
entry_point: worker
Command Agents
Agents can invoke slash commands instead of (or in addition to) prompt files. The command is prepended to the user prompt when processing messages.
agents:
- name: builder
model: opus
command: "/know:build"
prompt: builder/prompt.md
- name: reviewer
model: sonnet
command: "/know:review"
Precedence:
- Message frontmatter
command: (highest)
- Agent config
command: (default)
- No command (just prompt as system prompt)
Commands are prepended to the user prompt at dispatch time — no special SDK options required.
Command Template Interpolation
Commands support {key} template tokens that resolve from the message payload at runtime. Use this to pass dynamic values (like feature names) through the mesh pipeline.
agents:
- name: prebuild
model: haiku
command: "/know:prebuild {feature}"
- name: builder
model: opus
command: "/know:build {feature}"
Resolution rules:
{key} matches msg.payload[key] — if present, replaced with the string value
- Unresolved tokens stay as literal text (no silent failures, no crashes)
- Payload values come from message frontmatter (e.g.,
feature: auth-system)
Propagation: Upstream agents must include the key in their completion message frontmatter for downstream agents to receive it. The consumer maps frontmatter fields to payload automatically.
User message: feature: auth → prebuild gets "/know:prebuild auth"
Prebuild msg: feature: auth → builder gets "/know:build auth"
Writing Prompts
Focus on workflow only.
System Auto-Injects (DO NOT WRITE IN PROMPTS):
- ❌ Message protocol (frontmatter schema, message types, paths format)
- ❌ Routing instructions (how to write messages to other agents)
- ❌ Rearmatter format (success_signal, grade, confidence fields)
- ❌ Workspace structure and paths (auto-injected from config.yaml)
- ❌ Message file naming conventions
- ❌ Tool availability and usage instructions (system provides)
Write ONLY:
- ✅ Agent role and mandate
- ✅ Workflow steps (what to do, when)
- ✅ Decision trees and logic
- ✅ Domain-specific guidance
- ✅ Quality gates and success criteria
# {Agent Name}
You are the {role} agent.
## Workflow
1. Read incoming task
2. {Work steps}
3. Signal completion when finished
Prompt Template Tokens
Prompts can embed {key} template tokens that are replaced with resolved values at runtime, before any section injection. This lets agents reference dynamic paths inline rather than relying on injected context sections.
Built-in tokens (always available when workspace is resolved):
{workspace} → absolute path to the resolved workspace directory
Example usage in prompt:
## Phase 0: Inventory
ls {workspace}/prose-draft.md
cat {workspace}/context.yaml
At runtime, if workspace resolves to /project/.ai/games/my-game/campaigns/campaign-1/turns/turn-35, the prompt becomes:
## Phase 0: Inventory
ls /project/.ai/games/my-game/campaigns/campaign-1/turns/turn-35/prose-draft.md
cat /project/.ai/games/my-game/campaigns/campaign-1/turns/turn-35/context.yaml
Rules:
- Tokens that don't appear in the prompt are no-ops (safe for all meshes)
- Unresolved tokens (no matching key) are left as-is (no silent failures)
- Replacement happens via
PromptInjector.replaceTemplateTokens() before workspace section injection
- The
injectWorkspace() method automatically replaces {workspace} — no caller changes needed
Dynamic workspace resolution via workspace.variables + workspace.locations:
When the workspace config declares variables and locations, the dispatcher resolves template variables from a source file (e.g., session.yaml) and uses the resolved workspace location as the workspace directory. This enables per-turn or per-session dynamic paths.
workspace:
path: ".ai/games/"
variables:
source: ".ai/tx/my-mesh/session.yaml"
mapping:
game-id: game_id
campaign-id: campaign_id
N: turn
locations:
session: ".ai/tx/my-mesh"
game: ".ai/games/{game-id}"
campaign: ".ai/games/{game-id}/campaigns/{campaign-id}"
workspace: ".ai/games/{game-id}/campaigns/{campaign-id}/turns/turn-{N}"
Resolution priority (dispatcher):
- FSM context
$workspace variable (highest — gates use this)
- Resolved
workspace location from manifest variables (per-turn path)
- Static workspace config (
workspace.path)
- Default:
.ai/tx/workspaces/<mesh-name>
Falls back gracefully: if the source file is missing or variables don't resolve, unresolved {tokens} remain and the static fallback is used instead.
Agent Boundaries (CRITICAL for Coordinators)
Haiku agents are eager helpers. Without explicit boundaries, they'll do work meant for other agents. Use <boundaries> blocks to constrain behavior.
Problem: A haiku coordinator sees domain context (file formats, workflow goals) and decides to "help" by doing the creative work itself instead of routing.
Solution: Explicit DO NOT / ONLY lists that name WHO does each task.
<role>
Route tasks. Validate state. Forward to specialists.
You are a ROUTER. You do NOT create content.
</role>
<boundaries>
DO NOT:
- Write output files (worker does that)
- Analyze input data (analyst does that)
- Make domain decisions (specialist does that)
- Read file contents beyond checking existence
ONLY:
- Read session state for routing decisions
- Check file EXISTENCE (ls), never CONTENTS (cat)
- Write routing messages to other agents
- Write ask-human when blocked
</boundaries>
Key principles:
- State WHO does the forbidden work: "(worker does that)"
- Separate existence checks from content reads
- Add "If you find yourself doing X, STOP" guardrails
- Keep domain knowledge minimal - coordinators route, they don't understand
Phase Coordinators Pattern
For complex pipelines, use one haiku coordinator per phase instead of one monolithic coordinator.
Problem: A single coordinator managing many phases accumulates too much context and state. It becomes complex, error-prone, and harder to debug.
Solution: Split into discrete phase coordinators, each with single responsibility.
Before (monolithic):
agents:
- name: coordinator
model: haiku
prompt: coordinator/prompt.md
After (phase-based):
agents:
- name: entry
model: haiku
prompt: coordinator/entry.md
- name: init-coord
model: haiku
prompt: coordinator/init-coord.md
- name: prep-coord
model: haiku
prompt: coordinator/prep-coord.md
- name: work-coord
model: haiku
prompt: coordinator/work-coord.md
Benefits:
- Each coordinator has ~50-80 lines (vs 400+)
- Single responsibility per agent
- Easier to debug (which phase failed?)
- State validation at phase boundaries
- Boundaries are clearer per-phase
Pattern:
entry → phase-1-coord → phase-2-coord → ... → completion-coord
↓ ↓
specialists specialists
Each phase coordinator:
- Receives task from previous coordinator
- Does its ONE job (setup, dispatch, validate, etc.)
- Updates shared session state
- Routes to next coordinator
Shared state: Use session.yaml that all coordinators read/write. Each coordinator preserves ALL fields when updating.
Multi-Agent Routing
routing:
agent-a:
complete:
agent-b: "Handoff reason"
blocked:
core: "Need intervention"
See docs/mesh-config.md for full routing reference.
Dispatcher Routing (Opt-in)
Centralized routing where agents write to a sentinel address and the dispatcher resolves targets from config.
routing_mode: dispatcher
routing:
agent-a: agent-b
agent-b:
approved: agent-c
needs_work: agent-a
default: agent-c
Fan-out / Fan-in: Array value with trailing options object for parallel dispatch:
routing_mode: dispatcher
routing:
planner: [reviewer-a, reviewer-b, reviewer-c, { discuss: true, complete: synthesizer, fan_in: batch }]
complete: agent — join agent, gated until all fan-out members send outcome: complete
discuss: true — members can peer-message via outcome: discuss + route_to: peer
fan_in: batch|queue|drain — controls how messages are delivered to the join agent (default: batch)
transform: summarize — optional haiku pre-pass to compress responses before delivery
- Fan-out members get implicit routing (no individual entries needed)
- Members send
outcome: complete to signal done, outcome: discuss + route_to: for peer chat
Fan-in delivery modes (fan_in):
| Mode | Behavior |
|---|
batch (default) | Gate until all complete, deliver all responses in one combined message |
queue | Current OAOM serial delivery (N cold worker starts) |
drain | Deliver immediately; inject into running join worker via session resume |
Transform (transform):
| Value | Behavior |
|---|
summarize | Haiku pre-pass compresses response(s) before delivery to join agent |
| fan_in | transform | Result |
|---|
| batch | — | Gate until all complete, deliver all in one worker |
| batch | summarize | Gate, haiku-compress all responses into one, deliver |
| queue | — | Serial OAOM (N cold starts) |
| queue | summarize | Each message haiku-compressed before its worker run |
| drain | — | Inject each response into running join worker |
| drain | summarize | Each response haiku-compressed then injected |
Agents receive prompt instructions to write to: mesh/dispatch with outcome: in frontmatter. Override with route_to: for explicit targeting. Reserved outcome: escalate routes to human.
Fan-out members with discuss: true receive a peer list in their prompt. They use outcome: discuss + route_to: peer-name for peer-to-peer messaging within the group.
Type detection: string value = linear, object value = branch, array value = fan-out, absent = terminal.
Static Routing Mode
Ordered agent chain — each agent runs sequentially, worker exit fires the next agent automatically. No message passing between agents.
routing_mode: static
routing:
- preprocessor
- analyzer
- reporter
routing must be an ordered array of agent names
- First agent in array is the entry point (receives initial task)
- Last agent in array is the completion agent (routes to core/core on completion)
- No
entry_point or completion_agents needed (derived from routing chain)
- Each agent's completion triggers the next agent's spawn automatically
- Agents do NOT write messages to mesh addresses — chain advances on exit
- Error in any agent halts the chain; error surfaces to core/core
- Not compatible with FSM or free routing (warned if mixed)
Common Patterns
Session reuse (default behavior): continuation: true is the default — sessions persist naturally. Set continuation: false to force cold starts (needed for checkpoint/fork_from isolation).
Persistent mesh (no shutdown on complete): For meshes that loop perpetually and report status without dying:
completion_agents:
- weaver
stop_on_first_complete: false
check_queue_on_complete: true
| stop_on_first_complete | check_queue_on_complete | Behavior |
|---|
| true (default) | true (default) | Stop on complete, wait for queue to drain first |
| true | false | Stop immediately on complete (legacy behavior) |
| false | true | Informational complete, mesh continues running |
| false | false | True daemon mode, mesh never stops on complete |
MCP tools only: toolRestriction: mcp-only
Quality hooks: Use explicit lifecycle: hooks for quality evaluation:
lifecycle:
pre:
- quality:preflight
post:
- quality:checklist
- quality:rubric
FSM state tracking: fsm: block for system-managed state variables and logic. Only use if needed, linear workflows generally don't need fsm.
Parallel execution: parallelism: block for fork/join semantics (see Parallel Execution section below), or ensemble: { type: parallel } for FSM states
CRITICAL - FSM Entry Routing: Entry agents in FSM ensemble meshes MUST fan out to ALL ensemble workers. FSM observes these messages to track state, but explicit routing triggers the workers.
routing:
entry:
complete:
worker-1: "Spawn worker 1"
worker-2: "Spawn worker 2"
worker-3: "Spawn worker 3"
Original task injection: injectOriginalMessage: true - Injects original task into downstream agents
Design documentation: playbook_notes: - Embed architectural rationale in config (replaces separate READMEs)
Self-assessment metadata: rearmatter: - Agent outputs self-assessment fields (grade, confidence, status) for FSM routing decisions
Lifecycle hooks: Auto-commit, brain insights, quality gates
lifecycle:
post:
- commit:auto
- brain-update
Available hooks: worktree:create, commit:auto, brain-update, quality:*. See docs/mesh-config.md.
File Preload
Dump files into agent context before execution. Useful for preloading context without manual reads.
agents:
- name: preloader
model: haiku
prompt: prompt.md
load:
- "package.json"
- "*.md"
- "src/**/*.ts"
Behavior:
- Files matched by glob patterns are read and injected into system prompt
- Files over 200KB are skipped with warning
node_modules/ and .git/ are auto-excluded
- Model defaults to
haiku when load is set (cheap preloaders)
Use cases:
- Virtual "setup" agents that preload project context
- Checkpoint entry points that establish shared context
- Cheap haiku agents that read files before expensive opus agents work
Session Forking
Share conversation context between agents via checkpoints.
agents:
- name: setup
model: haiku
prompt: setup.md
load: ["package.json"]
checkpoint: true
- name: worker-a
model: sonnet
prompt: worker.md
fork_from: setup
- name: worker-b
model: opus
prompt: worker.md
fork_from: setup
Behavior:
checkpoint: true saves the agent's sessionId on completion
fork_from: agent-name loads that checkpoint as the starting session
- Forked agents continue from the checkpoint's conversation history
- Works across models (haiku checkpoint → opus fork)
Use cases:
- Skip redundant prework (preload once, fork many)
- Share established context across parallel workers
- Model escalation with preserved context
Parallel Execution
Fork from entry, run agents concurrently, join at exit.
agents:
- name: preload
model: haiku
prompt: preload.md
load: ["package.json"]
- name: analyst
model: sonnet
prompt: analyst.md
- name: reviewer
model: sonnet
prompt: reviewer.md
- name: critic
model: sonnet
prompt: critic.md
- name: synthesizer
model: sonnet
prompt: synthesizer.md
parallelism:
- agents: [analyst, reviewer, critic]
entry: preload
exit: synthesizer
timeout: 300000
on_partial: continue
Flow:
preload (entry)
│ checkpoint
├─────┼─────┐
▼ ▼ ▼
analyst reviewer critic (parallel, forked from preload)
│ │ │
└─────┼─────┘
▼
synthesizer (exit, gated until all complete)
Auto-wiring:
- Entry agent gets
checkpoint: true automatically
- Parallel agents get
fork_from: entry automatically
- Exit agent is gated until ALL parallel agents complete
Routing: Parallel agents must route to exit agent:
routing:
preload:
complete:
analyst: "Ready for analysis"
analyst:
complete:
synthesizer: "Analysis done"
reviewer:
complete:
synthesizer: "Review done"
critic:
complete:
synthesizer: "Critique done"
synthesizer:
complete:
core: "Synthesis complete"
vs FSM Ensemble:
| Feature | parallelism: | FSM ensemble: |
|---|
| Fork context | Yes (checkpoint) | No |
| Result aggregation | No (just sync) | Yes (concat/vote/etc) |
| Gating | Exit gated | FSM state transition |
| Use case | Parallel work, shared context | Same task, multiple perspectives |
FSM (State Tracking)
Add fsm: block to track state and provide context to agents.
IMPORTANT: If you use FSM, you must also define routing: configuration. Routes can exist without FSM, but FSM cannot exist without routes.
Sequential workflow:
fsm:
initial: init
context:
turn: 0
workspace: null
states:
init:
agents: [coordinator]
entry:
set:
turn: "$((turn + 1))"
workspace: "/path/to/turn-$turn"
exit:
default: awaiting_work
awaiting_work:
agents: [worker]
exit:
when:
- condition: signal == "PASS"
target: complete
default: awaiting_work
scripts: {}
Parallel workflow (ensemble):
routing:
rev-1:
complete:
synthesizer: "Review 1 complete"
rev-2:
complete:
synthesizer: "Review 2 complete"
rev-3:
complete:
synthesizer: "Review 3 complete"
fsm:
initial: parallel_review
states:
parallel_review:
ensemble:
type: parallel
agents: [rev-1, rev-2, rev-3]
aggregation: concat
exit:
set:
results: "$ENSEMBLE_OUTPUT"
default: synthesize
scripts: {}
Agents receive injected context:
## FSM Context
state: awaiting_work
turn: 5
workspace: /path/to/turn-5
See docs/mesh-fsm-config.md for:
- Exit-based routing (when/run/default)
- Ensemble states (parallel execution)
- Self-loops and iteration tracking
- Gates and validation
Documentation
playbook_notes in config.yaml (for maintainers)
- Design rationale and architectural decisions
- WHY the mesh is built this way
- Alignment with methodologies/patterns
- Not injected into prompts
Example:
playbook_notes: |
This mesh implements the Ralph pattern from ClaytonFarr/ralph-playbook.
Uses layered quality refinement: haiku drafts, sonnet reviews, opus finalizes.
Task Distribution Pattern
Alternative to ensemble for splitting work across agents:
task_distribution:
spawner: coordinator
subagents: [worker-1, worker-2, worker-3]
reviewer: synthesizer
distribution_strategy: equal
subtask_count: 5
timeout_ms: 300000
allow_partial_failure: true
When to use task_distribution vs ensemble:
| Pattern | Task Distribution | Ensemble |
|---|
| Task | Split into parts | Same task |
| Agents | Different subtasks | Same analysis |
| Output | Combined portions | Aggregated views |
Aggregation Strategies
For ensemble aggregation field:
| Strategy | Description | Use Case |
|---|
concat | Join all outputs | Comprehensive review |
deduplicate | Remove duplicate findings | Code analysis |
voting | Majority opinion wins | Consensus decisions |
consensus | Require agreement | High-stakes choices |
custom | Use custom prompt | Domain-specific |
Deprecated Patterns
AVOID these patterns:
| Pattern | Replacement | Reason |
|---|
state.type: ensemble | state.ensemble: { type: parallel } | Old FSM syntax |
state.subtask: true | Explicit ensemble routing | Implicit behavior |
workspace: "string" | workspace: { path: "..." } | Object format preferred |
Agent Config Fields
| Field | Type | Required | Description |
|---|
name | string | yes | Agent identifier |
model | string | yes* | opus / sonnet / haiku (*defaults to haiku if load set, else sonnet) |
prompt | string | one of prompt/command | Path to prompt file |
command | string | one of prompt/command | Slash command (e.g., /know:build). Supports {key} interpolation from payload. |
workspace | object | no | Per-agent workspace config |
mcpServers | object | no | MCP server configurations |
description | string | no | Agent documentation |
load | array | no | Files to preload into context (globs supported) |
checkpoint | boolean | no | Save session state on completion for forking |
fork_from | string | no | Fork from another agent's checkpoint |
thinking | boolean | no | Extended thinking (default: true). Set false to disable. |
max_turns | number | no | API round-trip limit per invocation |
max_messages | number | no | Outbound message limit per invocation |
Additional Config Fields
| Field | Type | Description |
|---|
brain | boolean | Enable brain-update insights |
capabilities | array | Agent capability tags |
config | object | Custom mesh-specific settings |
idle_timeout_minutes | number/false | Idle timeout (false=disabled) |
clear-before | boolean | Clear state before run |
turn_workspace | object | Turn-based game workspace |
parallelism | array | Parallel execution blocks (see Parallel Execution) |
persistence | boolean/array | Session persistence across mesh runs |
routing_fallback | string | DEPRECATED — use guardrails.routing_error.routing_fallback |
routing_retry_max | number | DEPRECATED — use guardrails.routing_error.routing_retry_max |
manifest_enforcement | object | Artifact validation settings |
max_mesh_messages | number/object | Mesh-wide message cap (guardrail) |
autoInjectManifestFiles | boolean | Auto-preload manifest reads (default: true) |
load_claude_md | boolean | Load project CLAUDE.md into agent system prompt (default: true) |
Route Validation
Verify all ask relationships have matching ask-response routes back.
Rule: If agent A asks agent B, then B must have an ask-response route back to A.
Manual check:
- List all
ask relationships: A → asks → B
- List all
ask-response routes: B → responds-to → [X, Y, Z]
- For each ask, verify the target can respond to the sender
Common mistakes:
- Coordinator asks worker, but worker only responds to a different coordinator
- Agent added to
ask list but ask-response not updated
- Indirect flows (A → B → C → A) mistaken for direct flows
Example mismatch:
validator:
ask:
fixer: "Fix issues"
fixer:
ask-response:
reviewer: "Fixes complete"
Intentional indirection (not a bug):
Document intentional indirections in playbook_notes.
Prompt-to-Config Validation
Verify all agent references in prompts match agents defined in config.yaml.
Rule: Every to: mesh/agent in prompt examples must reference an agent that exists in the mesh's config.yaml.
Manual check:
yq '.agents[].name' meshes/{mesh}/config.yaml | sort > /tmp/agents.txt
rg "to: {mesh}/[a-z-]+" meshes/{mesh} --type md -o --no-filename \
| sed 's/to: {mesh}\///' | sort | uniq > /tmp/targets.txt
comm -23 /tmp/targets.txt /tmp/agents.txt
Common mistakes:
- Generic
coordinator when mesh has phase coordinators (init-coord, render-coord, etc.)
- Outdated agent names after refactoring
- Copy-paste from other meshes with different agent names
Architectural principle:
Prompts should reference responsibilities, not agent names. Routing decisions (who handles what) belong in config.yaml, not prompts.
| Pattern | Guidance |
|---|
to: mesh/specific-agent in examples | Acceptable for illustrating message format |
to: {from: field} dynamic routing | Preferred for ask-response patterns |
| Prose describing "send to agent X" | Move WHO to config, keep WHAT in prompt |
Anti-pattern:
# BAD: Hardcoded routing in prompt
When done, send ask-response to COORDINATOR.
Better:
# GOOD: Reference responsibility, config handles routing
When done, send ask-response to the coordinator that sent the ask.
# Config routing section defines which coordinator that is.
Guardrails
Unified runtime enforcement with strict/warning mode on every guardrail. Config: .ai/tx/data/config.yaml under guardrails:.
Mode (applies to all guardrails):
| strict | warning | Result |
|---|
| false | true | Default — Allow + inject feedback |
| true | true | Block/kill + reason |
| true | false | Block/kill silently |
| false | false | Disabled |
- Write gate: Intercepts Write/Edit/NotebookEdit and Bash redirects to undeclared paths.
- Read gate: Intercepts Read/Glob/Grep to undeclared paths.
- Routing error: Corrective injection on bad targets (max retries: 3) + per-edge message caps (
routing_retry_max / routing_fallback).
- Artifact validation: Pre/post validation of agent outputs. Default: enabled, 2 retries.
- Max messages/turns: Global or per-agent caps. Accept bare number or
{strict, warning, limit} object.
- Max mesh messages: Mesh-wide cap on total messages across all agents in a mesh run.
- Max turns (warning mode): SDK limit bypassed, turns tracked manually, event emitted at threshold.
- Parity: Always-on, non-configurable.
guardrails:
write_gate:
strict: false
warning: true
kill_threshold: null
read_gate:
strict: false
warning: true
kill_threshold: null
routing_error:
strict: false
warning: true
max_retries: 3
artifact:
strict: false
warning: true
post_validation: true
pre_validation: true
max_retry: 2
max_messages:
strict: false
warning: true
limit: null
max_turns:
strict: false
warning: true
limit: null
max_mesh_messages:
strict: false
warning: true
limit: null
meshes:
my-mesh:
write_gate:
strict: true
kill_threshold: 5
agents:
my-agent:
write_gate:
strict: false
warning: true
kill_threshold: 10
Override chain: agent > mesh > global > hardcoded default. strict and warning resolve independently.
Gates activate automatically when manifest entries exist — no additional mesh config needed.
Full reference: docs/guardrails.md
Debugging
tx status
tx trace
tx spy
tx logs