CRUMB a card from devarno-cloud

AGENTS.xml Subagent Fleet

petrova advanced 9 min read

ELI5

AGENTS.xml is the wiring diagram for the agent fleet. Each subagent is a worker with a job, a list of events that wake them up, files they read and write, what they hand off, and a list of conditions under which they refuse to proceed. There are no inferred workers — if you need a new one, you add it to the XML.

Technical Deep Dive

Class diagram of a subagent

classDiagram
class Subagent {
+name: string (1)
+description: paragraph
+spawns_on: Event[] (1..n)
+reads: Path[] (1..n)
+writes: Path[] (1..n)
+handoff_to: Subagent? (0..1, may be empty for terminal)
+output_contract: Contract (1)
+refusal_conditions: Condition[] (1..n)
}
class Event {
+id: string (must exist in event_catalog)
}
class Contract {
+format: string
+required_sections: string[]
+session_log_path?: string
}
class Condition {
+text: paragraph
}
class Capability {
+id: string
+verbs: Verb[]
+preconditions: string[]
+refusal_recovery: ErrorCode[]
}
Subagent "1" --> "1..n" Event : spawns_on
Subagent "1" --> "1" Contract : output_contract
Subagent "1" --> "1..n" Condition : refusal_conditions
Subagent --> "0..1" Subagent : handoff_to

The event catalog

Triggers are not free-text — they must come from <event_catalog>:

<event_catalog>
<event id="phase.open"/>
<event id="phase.close"/>
<event id="milestone.start"/>
<event id="milestone.acceptance_check"/>
<event id="decision_doc.created"/>
<event id="audit.fail"/>
<event id="friction.surfaced"/>
<event id="drift.suspected"/>
<event id="schema.changed"/>
<event id="human.invocation"/>
</event_catalog>

The CI validator catches typos: “event ‘mileston.start’ referenced but not in event_catalog”. Adding a new event requires updating both the catalog and the orchestrator prompt that interprets it — keep this list small and meaningful.

The default fleet

flowchart LR
H["human.invocation"] --> P[planner]
PO["phase.open"] --> P
FS["friction.surfaced"] --> P
P -->|milestone.start| I[implementer]
AF["audit.fail"] --> I
I -->|milestone.acceptance_check| A[auditor]
AC["milestone.acceptance_check"] --> A
DC["decision_doc.created"] --> A
A -->|phase.close| R[reviewer]
A -. "audit.fail" .-> I
PC["phase.close"] --> R
R -. "phase.close" .-> P
DS["drift.suspected"] --> DW[drift-watcher]
DW -->|drift.suspected| P
SC["schema.changed"] --> MIG[migrator]
MIG -->|milestone.acceptance_check| A
MS["milestone.start"] --> FD[frontend-designer]
FD -->|milestone.acceptance_check| A

Six core subagents:

  • planner — sequences work, owns MILESTONES.md edits, classifies friction. Does NOT write code.
  • implementer — writes code in 5–15-line safe windows; refuses larger windows without milestone waiver.
  • auditor — runs acceptance checks, never modifies code; verdicts pass / block / advisory.
  • reviewer — terminal, owns phase-close decision docs; refuses if >3 deferred items or unaddressed invariant violations.
  • migrator (full-stack option) — owns DB schema migrations; refuses forward-only without rollback strategy.
  • frontend-designer (full-stack option) — owns design tokens / a11y; refuses divergence from tokens without a decision doc.
  • drift-watcher — invoked on drift.suspected; refuses when change touches surfaces in >3 phases at once.

Refusal conditions are mandatory (MR-6)

Every <subagent> must declare at least one <refusal_conditions> clause. Subagents that “never refuse” are a smell — they cannot resist drift, cannot block invariant violations, cannot escalate. Examples:

SubagentSample refusal
planner”Phase has >5 deferred items from the prior phase. Stop and ask whether the prior phase should reopen.”
implementer”Change exceeds the safe window (15 contiguous lines) AND no waiver is in the milestone’s note. Stop and propose a decomposition.”
auditor”Acceptance check is unfalsifiable as written. Stop and ask the planner to rewrite it.”
reviewer”Verification round produced >3 deferred items. Stop and ask the human whether to reopen the phase.”
migrator”Migration is forward-only with no rollback strategy AND the data is not trivially reproducible.”
frontend-designer”Component lacks keyboard-navigation support and the spec doesn’t waive it.”
drift-watcher”The change touches surfaces in >3 phases at once.”

Escalation rules

When refusal fires:

  1. Write docs/roles/<role>/outputs/refusal-YYYYMMDD-HHMM-*.md with reason + recommended re-scope + alternatives considered.
  2. Do NOT proceed.
  3. Surface to the human with the refusal context.
  4. 3 refusals in one milestone → route to drift-watcher; the milestone is architecturally ambiguous.
  5. Refusal mentions an invariant violation → STOP unconditionally; not even human approval clears it until either the invariant is re-grounded (decision doc) or the change is dropped.

MR-6 — read the XML, never improvise

“If an orchestrating Claude session needs to delegate but the right subagent isn’t in AGENTS.xml, the correct response is add it to the XML, not improvise a delegation. Improvised delegation is how the delegation grammar rots. Six weeks later nobody can reproduce who handled what.”

Adding a subagent is a small commit. Doing it the right way preserves traceability — every action ends up traceable to a named role with declared reads/writes/output_contract.

Capabilities — external action surfaces

The <capabilities> block (added 2026-04-29 with the petrova control plane) describes typed verb layers the fleet may invoke. Different from a subagent: a capability is an external action surface with schema-validated PR-emitting verbs.

<capabilities>
<capability id="petrova-act">
<verbs>
<verb name="open_decision" upholds="MR-4 MR-7"/>
<verb name="update_milestone" upholds="MR-2 MR-7"/>
<verb name="start_phase" upholds="MR-2 MR-7 MR-10"/>
<verb name="close_phase" upholds="MR-2 MR-7 MR-10"/>
<verb name="verify_round" upholds="MR-10"/>
<verb name="propose_fix" requires="recent diagnose run (≤24h)"/>
...
</verbs>
<preconditions>...</preconditions>
<refusal_recovery>...</refusal_recovery>
</capability>
</capabilities>

Each <verb> declares which MRs it upholds — every PR a verb emits cites them. This is how MR-7 stays mechanically true even when an agent fleet writes verbs across the petrova line.

Key Terms

  • spawns_on — declared events that wake a subagent. Must come from <event_catalog>.
  • refusal_conditions — paragraphs describing when a subagent must stop and ask. Empty list is a CI failure.
  • <capabilities> — block describing external typed-verb surfaces. Differs from a subagent: a capability is invokable on demand, not event-driven.

Q&A

Q: What six children does every element require, and why is refusal_conditions one of them? A: name, spawns_on, reads, writes, output_contract, refusal_conditions. Refusal conditions are mandatory (MR-6) because a subagent without them cannot block drift, cannot escalate invariant violations, and silently absorbs work that should have stopped — the failure mode of “everyone agreed and the system rotted”.

Q: How does the <event_catalog> constrain spawn triggers? A: Every <event> referenced under <spawns_on> must have an id matching a catalog entry. CI rejects unknown events. This stops typos and stops new events from creeping in without simultaneously updating the orchestrator prompt that knows how to react to them.

Q: What does the block describe, and how is it different from a subagent? A: A capability is an external action surface (e.g. petrova-act with verbs open_decision, start_phase, etc.). A subagent is event-driven and wakes on triggers; a capability is invoked on demand by humans or other subagents. Capabilities carry preconditions, refusal-recovery error codes, and explicit MRs each verb upholds.

Examples

KAHN’s Phase 6 surfaces a friction item that needs a new subagent role: migration-archiver, owning archival of schema-migration outputs. MR-6 says: don’t improvise. The team opens a decision doc justifying the addition, edits AGENTS.xml.tmpl in core/templates, commits the submodule + bumps the parent pointer, and adds: <spawns_on><event>milestone.start</event></spawns_on>, <refusal_conditions><condition>migration touches a frozen archive without a thaw decision doc</condition></refusal_conditions>. CI re-runs; handoff-graph still a DAG; merge.

neighbors on the map