| name | mesh-builder |
| description | Use when creating, editing, or debugging any mesh — config.yaml, agent prompts, routing, FSM, guardrails, dispatcher, fan-out, ensemble, workspace, lifecycle hooks, parallelism, or permissions. Activate for any work in meshes/ directory or multi-agent workflow design. |
Mesh Builder
Build meshes (agent workflows) for TX V4.
Core Philosophy: Earn Your Keep
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.
Config Options and When They're Warranted
| 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.
Common Over-Engineering Patterns to Avoid
- Orchestrator doing implementation work — add
orchestrator: true to enforce routing-only (Read + Write msgs-only). System-level enforcement beats prompt instructions.
- FSM + orchestrator that handles all routing — pick one. If orchestrator routes, drop FSM.
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.
- Ensemble with 1 agent — that's just a regular agent.
- Working in
.ai/tx/msgs/ instead of workspace — agents MUST use {workspace} token for all intermediate files, analysis, outputs. msgs/ is the message queue only.
Workspace Isolation — Critical Design Pattern
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.
Two Patterns
Pattern A: Developer Meshes (dev, dev-full, etc.)
- Agents write directly to project:
src/, lib/, root config files, etc.
- No workspace config needed
- Messages are coordination only, actual work lives in codebase
Example:
echo "export function foo() { ... }" > src/foo.ts
Pattern B: Analysis/Reasoning Meshes (lens, research, etc.)
- Agents write to dedicated workspace defined in config
- Workspace isolates intermediate analysis from codebase
- Useful for multi-agent workflows, game turns, reasoning loops
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.
Anti-Pattern: Working in msgs/
❌ WRONG — Pollutes message queue:
echo "analysis" > .ai/tx/msgs/my-work-123.md
This breaks the clean separation:
- Transport:
.ai/tx/msgs/ (messages between agents)
- Work: Project files OR dedicated workspace (agent output, analysis, code)
Result: Message queue fills with intermediate files, becomes unreadable, routing breaks.
Best Practice: Parallel Isolation (Pattern B)
When multiple analysis agents run in parallel, each writes to its own subdirectory within the shared workspace:
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:
- Creates its subdirectory:
mkdir -p {workspace}/runs
- Writes to it:
{workspace}/runs/{agent-id}-output.md
- Routes completion back to coordinator
- Synthesizer reads all files and combines results
Result: Clean isolation, no message pollution, work visible and debuggable.
Dev Mode — Cheap Workflow Testing
dev_mode: true
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:
- First run of any new mesh (always test with dev_mode first)
- Debugging routing issues (agent A → B → C flow)
- Validating FSM state transitions
- Testing ask-human and HITL flows
- Any mesh with 4+ agents where a full run would be expensive
Disable when:
- The workflow is confirmed working and you need quality output
- Haiku's reasoning is insufficient for the task (complex synthesis, conflict resolution, architectural decisions)
- Running in production
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.
dev_mode: true
agents:
- name: synthesizer
model: opus
agents:
- name: synthesizer
model: opus
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 |
| Manifest routing | docs/manifest-routing.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.
Reliability front-matter fields (used by core agent for recovery, not in mesh configs):
recover: true — triggers DLQ recovery for the target mesh
rewind-to: <state> — override recovery session with checkpoint from named FSM state
session-id: <id> — resume a specific SDK session
resume-mesh: true — preserve mesh state instead of clearing on new entry
User message: feature: auth → prebuild gets "/know:prebuild auth"
Prebuild msg: feature: auth → builder gets "/know:build auth"
Message File Path Format — CRITICAL
Every agent prompt that writes messages must explicitly specify the file path format.
Format Rules
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)
Examples
✅ 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
Prompt Template
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 name
coordinator/worker with actual agent names
task with action (dispatch, complete, response, etc.)
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)
Dispatcher-Mode Prompt Examples (CRITICAL)
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}
---
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
Free Routing Mode
Agents 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
completion_agents:
- writer
- No
routing: block needed (warned if present)
- Agents set
to: agent-name directly in frontmatter (same as agent mode)
completion_agents controls who sees core/core as destination. Omit = all agents can complete
entry_point omitted → all agents receive the initial task simultaneously
- Not compatible with FSM (warned if present)
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)
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.
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).
Completion agents: Define which agents sit at the mesh boundary and can message core/core. Accepts array form (preferred) or deprecated singular string:
completion_agents:
- reviewer
- evaluator
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
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 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"
worker-2: "Spawn worker 2"
worker-3: "Spawn worker 3"
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 one
mesh-id: <name> — Unique identifier for this instance
- Each instance is isolated with its own state and session
- Use
mesh-id in follow-up messages to route to the same instance
- Instance marked complete when completion agent sends
status: complete
- View instances:
tx status shows running and completed instances
Isolation guarantees:
- Session isolation: Each instance gets unique session key (
meshName:meshId) that persists across follow-up messages
- State isolation: Independent worker tracking, metrics, and FSM state per instance
- Workspace isolation: Agents see same filesystem but track state separately
- Cross-instance communication: Not supported - instances cannot message each other directly
Cleanup and lifecycle:
- Instances persist in SQLite (
parallel_instances table) with status running or completed
- No automatic garbage collection - completed instances remain queryable via
tx status
- No instance limit enforced (guardrails planned but not yet implemented)
- Routing to completed instances returns error to sender
When to use:
- Multiple features being built in parallel by the same mesh
- Concurrent tasks that shouldn't share state
- Same workflow applied to different inputs (e.g.,
dev mesh building feature-a and feature-b simultaneously)
Example workflow:
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
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
- 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.
When to Use FSM — Decision Guide
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:
- Routing decisions depend on computed state, counters, or file presence — not agent judgment
- State must survive between completely separate TX sessions (cold restart)
- You need arithmetic operations to drive routing (e.g.,
turn: turn + 1, loop N times then exit)
- Or you need deterministic gate conditions independent of agent output (e.g., "only proceed if file X exists")
Real examples that warrant FSM:
- Narrative engine tracking turn number across days-long campaigns
- Loop mesh that runs exactly N iterations before exiting (counter drives routing)
- Ensemble with a gate that only fires when ALL N output files are present on disk
- Multi-session workflow that must resume in the exact right state after a cold restart
Do NOT use FSM when:
- An orchestrator agent already handles all routing decisions — if every state routes back to orchestrator anyway, the orchestrator IS the state machine. Drop the FSM.
- The workflow is linear or near-linear:
A → B → C → D
- Loops can be tracked by the agent itself (validator counting its own attempts in the message body)
- Fan-out/fan-in is the need — use dispatcher routing with fan-out array syntax instead
- HITL is the need — use
ask-human messages directly, no FSM state required
- "What phase are we in?" can be answered by reading the last message — that's not a state machine problem
The 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:
- Every FSM state has only one non-error exit that always fires (just use routing)
- All states route back to an orchestrator agent anyway (orchestrator is doing the state management, FSM is redundant)
- The FSM context tracks values the orchestrator could just pass in message bodies
- You added FSM because the workflow "felt complex" — complexity alone is not a reason
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: {}
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
count: $parallelism
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:
- 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
distribution_prompt: "..."
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 |
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 |
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 / 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.
Chrome Agents (Browser Access)
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.
Permissions (Tool Access Control)
Control which tools an agent can access using the permissions field.
Default behavior (no permissions block):
- Allowed:
Read, Write, Edit, Glob, Grep
- Denied:
Bash, 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
Example: Read-only reviewer
agents:
- name: reviewer
model: sonnet
prompt: reviewer.md
permissions:
allowedTools:
- Read
- Glob
- Grep
Available tools:
- File operations:
Read, Write, Edit, Glob, Grep
- Execution:
Bash (denied by default)
- Advanced:
Task (subagent spawning), TaskOutput, LSP, WebFetch, WebSearch, TodoWrite, NotebookEdit, Skill, EnterPlanMode, ExitPlanMode, KillShell
- Never grant to mesh agents:
AskUserQuestion (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.
Postconditions
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"
exit_code: 0
min_calls: 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:
- Validation runs after agent completion, before routing to next agent
- All postconditions must pass for the agent to complete successfully
- Guardrail mode (strict/warning) controlled via
guardrails.postcondition override chain
- Strict mode: kills worker, routes error to core/core
- Warning mode: injects corrective feedback into agent session
- Disabled mode (
strict: false, warning: false): skips validation entirely
Pattern matching:
- Bash: substring match on
input.command
- Write/Read/Edit: substring match on
input.file_path
- Other tools: substring match on any string value in
input
- Omit
pattern to match all calls to that tool type
Examples:
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
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
agents:
gravity:
postcondition:
strict: true
Additional Config Fields
| 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) |
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 invocations: Per-agent spawn cap. Counts how many times a worker is spawned for an agent in a mesh run. Ask/respond cycles don't count (worker stays alive). Prevents unbounded iteration loops. Warning at limit injects "final invocation" feedback. Strict past limit blocks spawn and notifies core.
- 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
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
Debugging
tx status
tx trace
tx spy
tx logs