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
Build meshes (agent workflows) for TX V4.
Start with the minimum. Add only what the mesh actually needs.
Every config option added is complexity that can break. The default question for each field is: "Does this mesh fail without it?" If no ā leave it out.
| Option | Warranted when... | Default |
|---|---|---|
dev_mode: true |
Testing a new mesh end-to-end before committing to full model costs | Omit |
fsm: |
Routing depends on computed state/counters/file presence ā NOT agent judgment | Omit |
parallelism: |
Agents truly run in parallel and need a sync gate | Omit |
routing_mode: dispatcher |
Fan-out to N parallel workers is the core mechanic | Omit |
routing_mode: free |
Agents self-organize ā each sees full roster, decides routing autonomously | Omit |
routing_mode: static |
Sequential agent chain ā worker exit fires next agent, no messaging | Omit |
routing_mode: manifest |
Workflow is a file pipeline ā agents produce files that unlock downstream agents | Omit |
type: persistent / auto_despawn: false |
Mesh must survive indefinitely (daemon pattern) | Omit |
continuation: false |
You explicitly need cold starts for isolation | Omit (continuation is default-on) |
lifecycle: hooks |
Quality gates or auto-commits are genuinely required | Omit |
workspace: |
Agents need structured file workspace management | Omit |
checkpoint: / fork_from: |
Multiple agents need shared prior context | Omit |
load: |
Files must be in context before any work starts | Omit |
ensemble: |
Same task, multiple perspectives, aggregated output | Omit |
injectOriginalMessage: |
Downstream agents truly need the original task | Omit |
rearmatter: |
FSM routing depends on self-assessment scores | Omit |
guardrails: |
Custom limits differ from system defaults | Omit |
brain: true |
Agents need project context they can't get from preloaded files alone | Omit |
load_claude_md: false |
Mesh agents should not inherit project CLAUDE.md instructions | true |
The minimal working mesh:
mesh: my-mesh
description: "What it does"
agents:
- name: worker
model: sonnet
prompt: worker.md
entry_point: worker
This is complete. Everything else is optional and should be justified.
orchestrator: true to enforce routing-only (Read + Write msgs-only). System-level enforcement beats prompt instructions.type: persistent on a mesh that runs once ā persistent is for daemons only.lifecycle: hooks "just in case" ā add when quality gates are actually required.parallelism: on 2 agents ā just route them sequentially, parallelism overhead isn't worth it.checkpoint:/fork_from: when agents don't share context ā preloading context agents don't need wastes tokens..ai/tx/msgs/ instead of workspace ā agents MUST use {workspace} token for all intermediate files, analysis, outputs. msgs/ is the message queue only.The rule: Agents read from .ai/tx/msgs/ (message queue). They write to their intended output location ā either project files (src/, lib/, etc.) or a dedicated workspace. .ai/tx/msgs/ is message transport only ā never do agent work there.
Pattern A: Developer Meshes (dev, dev-full, etc.)
src/, lib/, root config files, etc.Example:
# ā
CORRECT ā implementer writes to src/
echo "export function foo() { ... }" > src/foo.ts
Pattern B: Analysis/Reasoning Meshes (lens, research, etc.)
Config:
workspace:
path: ".ai/tx/workspaces/my-mesh"
create_on_init: true
Agent prompt:
## Output Location
All analysis goes to `{workspace}/`.
## Workflow
1. Create workspace subdirectories as needed: `mkdir -p {workspace}/analysis`
2. Write working files: `{workspace}/analysis/draft-1.md`
3. Include output content in completion message to next agent
At runtime, {workspace} resolves to .ai/tx/workspaces/my-mesh.
ā WRONG ā Pollutes message queue:
# Agent does this ā BAD
echo "analysis" > .ai/tx/msgs/my-work-123.md
This breaks the clean separation:
.ai/tx/msgs/ (messages between agents)Result: Message queue fills with intermediate files, becomes unreadable, routing breaks.
When multiple analysis agents run in parallel, each writes to its own subdirectory within the shared workspace:
# General parallel analysis pattern
coordinator: ā {workspace}/request.md (shared)
analyst-1: ā {workspace}/runs/analysis-1.md
analyst-2: ā {workspace}/runs/analysis-2.md
analyst-3: ā {workspace}/runs/analysis-3.md
synthesizer: ā {workspace}/synthesis.md (reads all, synthesizes)
Each agent:
mkdir -p {workspace}/runs{workspace}/runs/{agent-id}-output.mdResult: Clean isolation, no message pollution, work visible and debuggable.
dev_mode: true # Forces ALL agents to haiku regardless of config
Enable when: You've built a new mesh and want to test the routing, workflow, and agent coordination end-to-end before paying for sonnet/opus runs. Haiku is fast and cheap ā use it to validate the plumbing works before the real thing.
Enable for:
Disable when:
Never commit dev_mode: true ā it's a testing flag. Remove it before the mesh is considered production-ready. If you see it in a mesh config, that mesh hasn't been signed off yet.
# ā
Testing a new mesh
dev_mode: true
agents:
- name: synthesizer
model: opus # ignored ā all agents become haiku in dev_mode
# ā
Production ā remove dev_mode entirely
agents:
- name: synthesizer
model: opus # now respected
# Test prompt output before deploying
tx prompt <mesh> <agent> # View built prompt with injected protocol
tx prompt narrative-engine narrator # Example
tx prompt dev --raw # Raw output, no metadata
| Topic | Location |
|---|---|
| Config fields | docs/mesh-config.md |
| FSM (state tracking) | .ai/docs/mesh-fsm-config.md |
| Manifest routing | docs/manifest-routing.md |
| Available meshes | docs/meshes.md |
| Message format | docs/message-format.md |
mesh: example
description: "What this mesh does"
agents:
- name: worker
model: sonnet # opus | sonnet | haiku
prompt: prompt.md
entry_point: worker
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 # optional extra context
- name: reviewer
model: sonnet
command: "/know:review"
# no prompt needed - command expands to full workflow
Precedence:
command: (highest)command: (default)Commands are prepended to the user prompt at dispatch time ā no special SDK options required.
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}" # {feature} replaced from payload
- name: builder
model: opus
command: "/know:build {feature}" # same token, resolved per-message
Resolution rules:
{key} matches msg.payload[key] ā if present, replaced with the string valuefeature: 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.
Reliability front-matter fields (used by core agent for recovery, not in mesh configs):
recover: true ā triggers DLQ recovery for the target meshrewind-to: <state> ā override recovery session with checkpoint from named FSM statesession-id: <id> ā resume a specific SDK sessionresume-mesh: true ā preserve mesh state instead of clearing on new entryUser message: feature: auth ā prebuild gets "/know:prebuild auth"
Prebuild msg: feature: auth ā builder gets "/know:build auth"
Every agent prompt that writes messages must explicitly specify the file path format.
Message files go to .ai/tx/msgs/ with this format:
{timestamp}-{from}-{to}-{action}-{id}.md
Where:
{timestamp} = Unix seconds (date +%s){from} = Agent's qualified name with slashes replaced by hyphens (e.g., lens-coordinator for lens/coordinator){to} = Target agent/mesh with slashes replaced by hyphens (e.g., lens-historical for lens-historical){action} = What the message does (e.g., dispatch, complete, response){id} = Random or sequential ID (e.g., 12345 or abc123)ā CORRECT:
File: 1774562950-lens-framer-coordinator-frame-99999.md
---
to: lens/coordinator
from: lens/framer
msg-id: frame-1774562950
---
ā ALSO CORRECT:
File: 1740362400-lens-coordinator-lens-historical-dispatch-47392.md
---
to: lens-historical
from: lens/coordinator
msg-id: dispatch-historical
---
ā WRONG (creates directory):
File: /Users/god/projects/tx/tx-core/.ai/tx/msgs/1774562400-lens/coordinator--lens-historical.md
ā WRONG (slashes in agent name):
File: 1774562400-lens/coordinator--lens/historical-dispatch.md
Include this in every agent prompt that writes messages:
## Message Format
Write messages to `.ai/tx/msgs/` with this filename structure:
\`{timestamp}-{from}-{to}-{action}-{id}.md\`
Example:
\`1774562950-my-mesh-coordinator-my-mesh-worker-task-12345.md\`
File content:
\`\`\`markdown
---
to: my-mesh/worker
from: my-mesh/coordinator
msg-id: task-12345
headline: Brief description
---
Message body...
\`\`\`
Replace:
my-mesh with your actual mesh namecoordinator/worker with actual agent namestask with action (dispatch, complete, response, etc.)Focus on workflow only.
In routing_mode: dispatcher meshes, prompt examples must use the sentinel address (mesh/dispatch), never direct agent addresses. The system auto-injects routing instructions, but if your prompt includes message examples they must match the dispatcher protocol or agents will bypass the sentinel and trigger routing errors.
Fan-out discuss examples ā always use sentinel + outcome: discuss + route_to::
---
to: my-mesh/dispatch
from: my-mesh/reader-a
outcome: discuss
route_to: reader-b
msg-id: discuss-{timestamp}
headline: Question for reader-b
timestamp: {iso-timestamp}
---
Never write to: my-mesh/reader-b directly ā this bypasses the dispatcher and the message gets dropped with a routing error nudge.
Completion examples ā same pattern:
---
to: my-mesh/dispatch
from: my-mesh/reader-a
outcome: complete
msg-id: report-{timestamp}
headline: Domain report
timestamp: {iso-timestamp}
---
# {Agent Name}
You are the {role} agent.
## Workflow
1. Read incoming task
2. {Work steps}
3. Signal completion when finished
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 directoryExample 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:
PromptInjector.replaceTemplateTokens() before workspace section injectioninjectWorkspace() method automatically replaces {workspace} ā no caller changes neededDynamic 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/" # Static fallback
variables:
source: ".ai/tx/my-mesh/session.yaml" # Fixed path (no chicken-and-egg)
mapping:
game-id: game_id # {game-id} ā session.game_id
campaign-id: campaign_id # {campaign-id} ā session.campaign_id
N: turn # {N} ā session.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):
$workspace variable (highest ā gates use this)workspace location from manifest variables (per-turn path)workspace.path).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.
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:
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 # 400 lines, manages 6 phases
After (phase-based):
agents:
- name: entry
model: haiku
prompt: coordinator/entry.md # Routes based on state
- name: init-coord
model: haiku
prompt: coordinator/init-coord.md # Sets up workspace, routes to prep
- name: prep-coord
model: haiku
prompt: coordinator/prep-coord.md # Fan-out/fan-in for prep agents
- name: work-coord
model: haiku
prompt: coordinator/work-coord.md # Dispatches workers, routes to validate
Benefits:
Pattern:
entry ā phase-1-coord ā phase-2-coord ā ... ā completion-coord
ā ā
specialists specialists
Each phase coordinator:
Shared state: Use session.yaml that all coordinators read/write. Each coordinator preserves ALL fields when updating.
routing:
agent-a:
complete:
agent-b: "Handoff reason"
blocked:
core: "Need intervention"
See docs/mesh-config.md for full routing reference.
Centralized routing where agents write to a sentinel address and the dispatcher resolves targets from config.
routing_mode: dispatcher
routing:
agent-a: agent-b # linear ā always routes to agent-b
agent-b: # branch ā outcome determines target
approved: agent-c
needs_work: agent-a
default: agent-c
# agent-c: (absent) = terminal agent ā routes to core/core on complete
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: completediscuss: true ā members can peer-message via outcome: discuss + route_to: peerfan_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 deliveryoutcome: complete to signal done, outcome: discuss + route_to: for peer chatAgents self-organize from full roster. No routing table ā each agent sees all peers and decides where to route next.
routing_mode: free
agents:
- name: planner
prompt: planner.md
- name: researcher
prompt: researcher.md
- name: writer
prompt: writer.md
entry_point: planner # Optional: omit to fan-out to all agents
completion_agents: # Optional: omit to let any agent complete
- writer
routing: block needed (warned if present)to: agent-name directly in frontmatter (same as agent mode)completion_agents controls who sees core/core as destination. Omit = all agents can completeentry_point omitted ā all agents receive the initial task simultaneouslyOrdered agent chain ā each agent runs sequentially, worker exit fires the next agent automatically. No message passing between agents.
# Static routing ā sequential chain, no agent messaging
routing_mode: static
routing:
- preprocessor
- analyzer
- reporter
# routing[0] = entry_point (preprocessor)
# routing[last] = completion agent (reporter)
# Each agent runs, exits, next fires automatically
# On error: chain halts, error surfaces to core/core
routing must be an ordered array of agent namesentry_point or completion_agents needed (derived from routing chain)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.
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).
Completion agents: Define which agents sit at the mesh boundary and can message core/core. Accepts array form (preferred) or deprecated singular string:
# Preferred: array form
completion_agents:
- reviewer
- evaluator
# Deprecated: singular form (backward compatible, array takes precedence if both set)
completion_agent: reviewer
Persistent mesh (no shutdown on complete): For meshes that loop perpetually and report status without dying:
completion_agents:
- weaver
stop_on_first_complete: false # Completion signal is informational, mesh continues
check_queue_on_complete: true # (default) Queue-aware for future use
| 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 when routing depends on computed state, counters, or file presence ā not agent judgment. If an orchestrator handles all routing anyway, FSM is redundant. See FSM decision guide below.
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" # ā
CORRECT - Fan out to all workers
worker-2: "Spawn worker 2"
worker-3: "Spawn worker 3"
# core: "..." # ā WRONG - Workers never spawn!
Parallel Mesh Instances: Spawn isolated, named instances of the same mesh for concurrent execution:
---
to: dev/worker
from: core/core
parallel: true
mesh-id: auth-system
---
Implement user authentication.
parallel: true ā Spawn new instance or route to existing onemesh-id: <name> ā Unique identifier for this instancemesh-id in follow-up messages to route to the same instancestatus: completetx status shows running and completed instancesIsolation guarantees:
meshName:meshId) that persists across follow-up messagesCleanup and lifecycle:
parallel_instances table) with status running or completedtx statusWhen to use:
dev mesh building feature-a and feature-b simultaneously)Example workflow:
# Core sends task to dev with unique mesh-id
echo "---
to: dev/worker
from: core/core
parallel: true
mesh-id: feature-auth
---
Build authentication feature." > .ai/tx/msgs/$(date +%s)-core-core--dev-worker-$(date +%s%N | tail -c 6).md
# Later, send follow-up to the same instance
echo "---
to: dev/worker
from: core/core
mesh-id: feature-auth
---
Update authentication to use JWT." > .ai/tx/msgs/$(date +%s)-core-core--dev-worker-$(date +%s%N | tail -c 6).md
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 # Auto-commit changes
- brain-update # Document insights
Available hooks: worktree:create, commit:auto, brain-update, quality:*. See docs/mesh-config.md.
Dump files into agent context before execution. Useful for preloading context without manual reads.
agents:
- name: preloader
model: haiku # Model defaults to haiku when load is set
prompt: prompt.md
load:
- "package.json" # Exact file
- "*.md" # Glob pattern
- "src/**/*.ts" # Recursive glob
Behavior:
node_modules/ and .git/ are auto-excludedhaiku when load is set (cheap preloaders)Use cases:
Share conversation context between agents via checkpoints.
agents:
- name: setup
model: haiku
prompt: setup.md
load: ["package.json"]
checkpoint: true # Save session for forking
- name: worker-a
model: sonnet
prompt: worker.md
fork_from: setup # Fork from setup's checkpoint
- name: worker-b
model: opus
prompt: worker.md
fork_from: setup # Same checkpoint, different agent
Behavior:
checkpoint: true saves the agent's sessionId on completionfork_from: agent-name loads that checkpoint as the starting sessionUse cases:
Fork from entry, run agents concurrently, join at exit.
agents:
- name: preload
model: haiku
prompt: preload.md
load: ["package.json"]
# checkpoint: true auto-added
- name: analyst
model: sonnet
prompt: analyst.md
# fork_from: preload auto-added
- 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 # Fork point (gets checkpoint: true)
exit: synthesizer # Sync gate (waits for all)
timeout: 300000 # Optional: 5 min timeout
on_partial: continue # continue | abort on partial failure
Flow:
preload (entry)
ā checkpoint
āāāāāāā¼āāāāāā
ā¼ ā¼ ā¼
analyst reviewer critic (parallel, forked from preload)
ā ā ā
āāāāāāā¼āāāāāā
ā¼
synthesizer (exit, gated until all complete)
Auto-wiring:
checkpoint: true automaticallyfork_from: entry automaticallyRouting: 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 |
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.
Default assumption: do NOT use FSM. Pure message routing handles the majority of meshes cleanly. Only add FSM when the routing itself cannot be handled by agent judgment.
Use FSM when ALL of these are true:
turn: turn + 1, loop N times then exit)Real examples that warrant FSM:
Do NOT use FSM when:
A ā B ā C ā Dask-human messages directly, no FSM state requiredThe key test: If you'd trust an orchestrator agent to route correctly based on incoming messages, you don't need FSM. FSM is for when routing must be mechanical and cannot rely on agent judgment.
Red flags that you're over-engineering with FSM:
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:
# Ensemble agents need explicit 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 # Required: type inside ensemble block
agents: [rev-1, rev-2, rev-3]
aggregation: concat
exit:
set:
results: "$ENSEMBLE_OUTPUT"
default: synthesize
scripts: {}
Ensemble shorthand (agent + count): Instead of listing agents individually, spawn N copies of one agent. Supports variable references for dynamic parallelism:
fsm:
context:
parallelism: 3
states:
parallel_review:
ensemble:
type: parallel
agent: reviewer # Single agent template
count: $parallelism # Spawns 3 instances (variable reference)
aggregation: concat
FSM context_descriptions: Document context variables for maintainability:
fsm:
context:
turn: 0
workspace: null
context_descriptions:
turn: "Current iteration number, incremented each cycle"
workspace: "Resolved workspace path for this turn"
Agents receive injected context:
## FSM Context
state: awaiting_work
turn: 5
workspace: /path/to/turn-5
See docs/mesh-fsm-config.md for:
playbook_notes in config.yaml (for maintainers)
Example:
playbook_notes: |
This mesh implements the Ralph pattern from ClaytonFarr/ralph-playbook.
Uses layered quality refinement: haiku drafts, sonnet reviews, opus finalizes.
Alternative to ensemble for splitting work across agents:
task_distribution:
spawner: coordinator # Required: agent that splits the task
subagents: [worker-1, worker-2, worker-3] # Required: agents that do the work
reviewer: synthesizer # Required: agent that combines results
distribution_strategy: equal # Required: equal | weighted | adaptive | custom
distribution_prompt: "..." # Required when strategy is 'custom'
subtask_count: 5 # Optional fixed count
timeout_ms: 300000 # 5 minute timeout
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 |
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 |
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 |
completion_agent: "name" |
completion_agents: [name] |
Array form takes precedence if both set |
routing_fallback / routing_retry_max |
guardrails.routing_error.* |
Moved to guardrail config |
| 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 / string | no | Save session state on completion for forking. true (normalized to 'start'), 'start', or 'end'. |
fork_from |
string | no | Fork from another agent's checkpoint |
thinking |
boolean / object | no | Extended thinking. true (default), false to disable, or { budget_tokens: number } to set token budget. |
max_turns |
number | no | API round-trip limit per invocation. Also configurable via guardrails.max_turns with strict/warning modes. |
max_messages |
number | no | Outbound message limit per invocation. Also configurable via guardrails.max_messages with strict/warning modes. |
fragments |
array | no | Prompt fragment names to inject (string[]). Fragments are reusable prompt snippets shared across agents. |
load_claude_md |
boolean | no | Control CLAUDE.md injection into agent system prompt (default: true). Set false to prevent project instructions from leaking into mesh agents. |
orchestrator |
boolean | no | Restrict to Read + Write(msgs only). For coordinator agents that route, not implement. |
permissions |
object | no | Tool access control. See Permissions section below. |
postconditions |
object | no | Tool call postconditions. See Postconditions section. |
chrome |
boolean | no | Use claude --chrome CLI instead of Agent SDK. Enables browser access. Fire-and-forget: no HITL, no resume, no checkpoint. |
Incompatible combinations:
fork_from + continuation: true ā fork requires cold start isolation. Set continuation: false explicitly when using fork_from.Set chrome: true to spawn claude --chrome --print CLI instead of SDK. Fire-and-forget: no HITL, no resume, no checkpoint. SIGTERM ā SIGKILL after 5s.
agents:
- name: browser
model: sonnet
prompt: browser.md
chrome: true
Full docs: docs/chrome-agents.md ā behavior differences, incompatible fields, chrome vs Playwright decision matrix, troubleshooting.
Control which tools an agent can access using the permissions field.
Default behavior (no permissions block):
Read, Write, Edit, Glob, GrepBash, Task (must be explicitly allowed)Example: Allow Bash for an implementer
agents:
- name: implementer
model: sonnet
prompt: implementer.md
permissions:
allowedTools:
- Read
- Write
- Edit
- Glob
- Grep
- Bash # Explicitly allowed
Example: Read-only reviewer
agents:
- name: reviewer
model: sonnet
prompt: reviewer.md
permissions:
allowedTools:
- Read
- Glob
- Grep
# No Write, Edit, or Bash
Available tools:
Read, Write, Edit, Glob, GrepBash (denied by default)Task (subagent spawning), TaskOutput, LSP, WebFetch, WebSearch, TodoWrite, NotebookEdit, Skill, EnterPlanMode, ExitPlanMode, KillShellAskUserQuestion (no user session ā use messaging to core/core instead)Preamble behavior: Multi-agent meshes tell agents "not the Task tool" by default to prevent subprocess chaos. When Task or TaskOutput appear in allowedTools, the preamble switches to encourage Task tool usage for parallel subprocesses within the session, while still routing cross-agent work via messages. The preamble also recognizes Agent as a legacy alias.
Security principle: Only grant tools an agent actually needs. Start restrictive, add permissions as required.
AskUserQuestion ā never grant to mesh agents. Mesh agents have no interactive user session. AskUserQuestion is an SDK tool for CLI sessions only. When agents need human input, they write a message to core/core (which suspends the session until the human responds via core). Prompts that need HITL steps should instruct agents to send a message to core, not use AskUserQuestion.
God mode: Run tx start dev --god-mode to bypass all permissions (unrestricted tool access). Use only when you need it.
Validate that required tool calls occurred during agent execution. Prevents agents from hallucinating results instead of using tools.
Agents sometimes describe what they would do instead of doing it ā generating output inline rather than calling Bash, Write, or Task tools. Postconditions catch this by checking the actual tool call record after the agent completes.
agents:
- name: gravity
model: sonnet
prompt: gravity/prompt.md
postconditions:
tool_calls:
- tool: Bash
pattern: "campaign.sh" # Substring match in command
exit_code: 0 # Required exit code (default: 0)
min_calls: 1 # Minimum matching calls (default: 1)
- tool: Write
pattern: "collisions.yaml"
min_calls: 1
Fields per entry:
| Field | Type | Default | Description |
|---|---|---|---|
tool |
string | required | Tool name: Bash, Write, Read, Edit, TaskOutput, etc. |
pattern |
string | ā | Substring match in command (Bash), file_path (Write/Read/Edit), or any string input value (other tools). Omit to match any call to that tool. |
exit_code |
number | 0 | Required exit code. Bash only. Omit to use default (0 = no error). |
min_calls |
number | 1 | Minimum number of matching calls required. |
Behavior:
guardrails.postcondition override chainstrict: false, warning: false): skips validation entirelyPattern matching:
input.commandinput.file_pathinputpattern to match all calls to that tool typeExamples:
Ensure agent runs a script successfully:
postconditions:
tool_calls:
- tool: Bash
pattern: "campaign.sh"
exit_code: 0
Ensure agent uses parallel Tasks (not inline generation):
postconditions:
tool_calls:
- tool: TaskOutput
min_calls: 3 # Must read results from at least 3 parallel Tasks
Ensure agent writes required output file:
postconditions:
tool_calls:
- tool: Write
pattern: "report.md"
Override chain for mode (strict/warning):
guardrails:
postcondition:
strict: false
warning: true
meshes:
narrative-engine:
postcondition:
strict: true # Kill gravity if it hallucinates
agents:
gravity:
postcondition:
strict: true
| Field | Type | Description |
|---|---|---|
dev_mode |
boolean | Force all agents to haiku for cheap workflow testing. Remove before production. |
brain |
boolean | Inject brain access prompt into all agents. Agents learn they can message brain/brain for project questions (architecture, dependencies, design rationale). Skipped for the brain mesh itself. |
capability |
object | Structured capability declaration for catalog matching (domain, input, output, tools, interaction ā enum arrays). See src/mesh/capability/schema.ts for valid values. |
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) |
max_invocations |
number/object | Per-agent spawn cap ā counts worker spawns, not messages. Caps iteration loops while allowing ask/respond (no new spawn). Same {strict, warning, limit} shape. |
autoInjectManifestFiles |
boolean | Auto-preload manifest reads (default: true) |
load_claude_md |
boolean | Load project CLAUDE.md into agent system prompt (default: true) |
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:
ask relationships: A ā asks ā Bask-response routes: B ā responds-to ā [X, Y, Z]Common mistakes:
ask list but ask-response not updatedExample mismatch:
# validator asks fixer
validator:
ask:
fixer: "Fix issues"
# fixer responds to reviewer, NOT validator - BUG!
fixer:
ask-response:
reviewer: "Fixes complete" # ā validator missing!
Intentional indirection (not a bug):
# narrator ā lint-coordinator ā editor ā narrator
# lint-coordinator responds to editor, not narrator (by design)
Document intentional indirections in playbook_notes.
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:
# Extract agents from config
yq '.agents[].name' meshes/{mesh}/config.yaml | sort > /tmp/agents.txt
# Extract to: targets from prompts
rg "to: {mesh}/[a-z-]+" meshes/{mesh} --type md -o --no-filename \
| sed 's/to: {mesh}\///' | sort | uniq > /tmp/targets.txt
# Find mismatches
comm -23 /tmp/targets.txt /tmp/agents.txt
Common mistakes:
coordinator when mesh has phase coordinators (init-coord, render-coord, etc.)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.
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 |
routing_retry_max / routing_fallback).{strict, warning, limit} object.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
max_invocations:
strict: true
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
tx status # Workers, queue
tx trace # Message trace viewer
tx spy # Real-time activity
tx logs # System logs