[ Brigade Master Class ]
Brigade, end to end
One canonical source for the MCP servers, tools, skills, and memory your AI coding agents share across 18 writer harnesses, merged into each tool’s native config with a review gate and a receipt for every change. A local operator-system CLI - not a runner, not a daemon, not a hosted service. Memory is one pillar of it, not the whole story. This is how every piece works, from the decide() memory guardrail to the MCP merge engine to the cross-model run loop.
What Brigade Is - and Is Not
Brigade’s own one-liner: "Your agents run loops. Brigade keeps the receipts." It is one canonical source for the MCP servers, tools, skills, and memory your agents share across Codex, Claude Code, OpenCode, and over a dozen other harnesses - merged into each tool’s native config with a review gate and a receipt for every change. Shared memory is where it started and is one pillar of it, not the whole surface. It runs on the machine you control: local by default, loud about the exceptions. Holding both halves of this panel in your head is the whole mental model.
What it is
- A shared MCP catalogOne catalog of MCP servers merged by key into every tool’s native config. Dry-run by default; servers you added by hand are never touched.
- A shared tool + skill catalogReviewed portable tools and SKILL.md packs, installed across the harness fleet with per-harness receipts and rollback.
- Shared memory across harnessesOne handoff note format every tool writes; one canonical owner files the good notes into durable memory.
- Local guardrailsLints handoffs, scrubs content, and security-scans agent workspaces before anything leaves the machine.
- A receipt for everythingEvery consequential action lands a plain-file receipt you can grep, diff, and prune.
- File-firstYour memory and catalogs are files in your repo, reviewable in git, readable without Brigade installed.
- Deliberately boundedSafe, targeted changes apply; ambiguous or risky ones wait for your review.
What it is not
- A hosted service or daemonDoes not run in the background or install schedulers.
- A publisherNever pushes to GitHub or publishes packages.
- NoisySends no notifications by default.
- Blindly automaticDoes not save every note or write tool configs without your yes.
- A silent ingesterNever turns ingest into a background process, and never skips review for risky or failed notes.
Why It Exists: Two Incidents
Brigade was hand-rolled one incident at a time around an always-on OpenClaw agent plus daily Codex and Claude Code sessions. Two failures shaped the design more than anything planned. First, a nightly "dreaming" job promoted raw session fragments straight into memory and bloated MEMORY.md past the bootstrap budget; every session then started with truncated memory and nobody noticed for weeks. Blind auto-promotion died that day. Second, 195 handoff notes sat unread across 35 repos because the ingester had a hardcoded three-repo allowlist and nothing warned about the gap. Silence is the failure mode. Every part of Brigade that lints, warns, or writes a receipt exists because something once failed in silence.
Brigade’s Surface Area
Memory is where Brigade started, but it is one capability among several the CLI keeps in one reviewed source: MCP servers, tools, skills, and memory. Each is its own command family with its own review gate and receipts; none of them publishes, runs a daemon, or changes anything without your explicit command. Here is the whole surface before we go deep on each piece - keep it in mind, because the rest of this dive is one chapter per card.
The Kitchen Glossary
Brigade borrows the vocabulary of a professional kitchen brigade (a brigade de cuisine). The terms are load-bearing, not decoration: internalize them and the rest of the system reads cleanly. Definitions are verbatim from docs/technical-guide.md.
- harnessthe line cook
- An AI agent program: Claude Code, Codex, OpenCode, Antigravity, Pi, Cursor, OpenClaw, Hermes, and more.
docs/technical-guide.md - operatorthe chef
- You, the human running the agents and making the explicit decisions about what gets saved, run, or published.
- handoffthe ticket
- A memory note an agent writes to be saved long-term, held in its own inbox until linted and classified.
- ingest / ingesterexpediting to the pass
- Reading handoff notes from every inbox and filing them into permanent memory. Conservative by design.
- stationa kitchen station
- A subsystem of Brigade (memory, security, tokens, pantry, ...) with its own commands and a health check.
- mise en placeeverything in its place
- Rules, memory, tools, inboxes, and guards laid out before the session gets expensive. The core idea.
- aboyeurthe expediter
- The kitchen expediter who calls out orders. In Brigade, the orchestrator behind a cross-model `brigade run`.
- receiptthe docket
- A local file logging that something happened, kept for audit and proof. Brigade writes them but never acts on them.
- gatethe chef’s yes
- A manual approval checkpoint; nothing risky happens without your yes.
- dogfoodstaff meal
- Brigade used on itself or another trusted repo.
The Memory Loop
The memory loop is the original core; the rest of the surface orbits it. In five steps: (1) each writer harness drops a handoff note into its own inbox (e.g. .claude/memory-handoffs/) as it works; (2) Brigade lints, guards, and classifies every note before it can become memory; (3) safe, targeted notes file themselves into durable memory automatically; (4) anything ambiguous, unsafe, or failed is kicked to a review inbox for the memory owner to decide; (5) future sessions start with better context and a plain-file receipt records exactly what happened. Drawn, it looks like this - writer harnesses hand off, Brigade lints and classifies, safe notes reach durable memory through the owner, the risky few branch to review, and memory feeds the next session.
Mise en Place: Two-Layer Memory
The 41KB incident produced the central design decision: memory has two layers. Knowledge cards under memory/cards/ hold the detail; MEMORY.md stays a slim, one-line-per-card index that loads every session and is guarded against the 12KB budget. Brigade never edits canonical memory itself - the owner does the writing.
The decide() Guardrail
This is the single most important piece of Brigade’s memory loop. decide() (ingest.py) is the pure function that turns a parsed handoff into one of four outcomes: promoted, routed, inboxed, or skipped. Every branch that cannot prove a note is safe routes it to the review inbox. Conservative by default - that pause is the point.
- any unknown ## section?inboxed outcomeA section key not in KNOWN_SECTIONS means the parser may have split on internal text. Bail to review.
- action = create/update-card AND promote_cards▸ Card safety checksSAFE_CARD_NAME_RE filename allowlist → YAML frontmatter present → scan_untrusted for injection.
- any check failsinboxed outcomeUnsafe name, missing frontmatter, or injection signal.
- all passpromoted outcomeWritten to memory/cards/<card>.md.
- any check fails
- action names a target document AND route_documents▸ Document safety checksTarget in SAFE_SPECIAL_TARGETS or matching SAFE_RULE_PATH_RE → non-empty → no ## headings → dedupe → budget guard → injection scan.
- any check failsinboxed outcomeUnsafe target, dup, over-budget, or injection signal.
- all passrouted outcomeAppended to the target document (TOOLS.md, rules/*, .learnings/*).
- any check fails
- no recommended actioninboxed outcomeMissing "Recommended memory action".
- anything elseskipped outcomeAction not auto-handled; left for a human.
decide(), in the Source
The same guardrail in code. Notice that every failure path constructs an Outcome("inboxed", ...) instead of writing - the safe default is always review.
The card-promotion branch of decide(). The document-routing branch below it follows the same shape.
src/brigade/ingest.py:209
def decide(sections, target, promote_cards, route_documents) -> Outcome:
action = sections.get("recommended memory action", "").strip().lower()
stray = [s for s in sections if s not in KNOWN_SECTIONS]
if stray:
return Outcome("inboxed",
reason=f"unknown sections present (parser may have split content): {stray}")
if action in ("create-card", "update-card") and promote_cards:
card = sections.get("target card", "").strip()
content = sections.get("suggested card content", "")
if not SAFE_CARD_NAME_RE.match(card):
return Outcome("inboxed", reason=f"target card name unsafe: {card!r}")
if not content.lstrip().startswith("---"):
return Outcome("inboxed", reason="card content missing YAML frontmatter")
if scan_untrusted(content).flagged:
return Outcome("inboxed", reason="injection signal in card content ...")
return Outcome("promoted", dest=target / "memory" / "cards" / card)stray = [s for s in sections if s not in KNOWN_SECTIONS]An unrecognized ## heading means the parser likely split the note wrong. Refuse to guess; inbox it.and promote_cardsAuto-promotion is opt-in. Off by default, you choose to enable it per ingest.SAFE_CARD_NAME_RE.match(card)A strict filename allowlist (^[A-Za-z0-9._-]+\.md$) blocks path traversal into other dirs.scan_untrusted(content).flaggedPrompt-injection scan runs before anything is allowed to become durable memory.
The Eleven Stations
Under the command families, Brigade is partitioned into eleven builtin stations (the _BUILTIN tuple in registry.py). Each is a frozen Station dataclass with a name, a summary, kitchen aliases, an optional doctor() health check, and the managed tools attached to it. Each managed tool now also declares machine-readable surfaces - a live doctor JSON, a bounded brief, a verify command - that brigade stations list --json and brigade profiles surface before you install a sidecar. The station is the unit of the system, and the bridge from the memory loop to everything else.
One MCP Catalog, Every Tool
Every agent tool reads its MCP servers from a different file in a different shape. Brigade keeps one canonical catalog at .brigade/mcp.json and merges it into each tool’s native config for you. It is dry-run by default and never runs from doctor or brief; it merges by server key, so servers you added by hand are never touched; and secrets are written as ${VAR} references (or VS Code ${input:VAR}), never inlined. brigade mcp plan previews; brigade mcp sync --write actually merges.
| Tool | Native config it writes | Shape |
|---|---|---|
| Claude Code | .mcp.json | JSON · mcpServers |
| Cursor | .cursor/mcp.json | JSON · mcpServers |
| Codex CLI | .codex/config.toml | TOML · [mcp_servers.*], merged surgically |
| VS Code | .vscode/mcp.json | JSON · servers + inputs[] |
| OpenCode | opencode.json | JSON · mcp |
| Antigravity | ~/.gemini/config/mcp_config.json | JSON · user-global |
| OpenClaw | ~/.openclaw/openclaw.json | JSON · mcp.servers |
The Merge State Machine
Brigade owns only the server keys recorded in the sidecar .brigade/mcp/state.json. For each (tool, server) it compares the canonical definition, what it last wrote, and the live value in the file, then picks one of seven outcomes. A server you added yourself is foreign and never modified; one Brigade manages but you then edited is a conflict, left alone unless you pass --force. This is what makes the sync safe to run on a config you also hand-edit. (Verbatim from docs/mcp-sync.md.)
| State | Situation | Default action |
|---|---|---|
| missing | Not in the file | create |
| foreign | In the file, not owned by Brigade | skip (--adopt to take over) |
| current | Owned, matches the catalog | skip |
| stale | Owned, canonical changed | update |
| conflicted | Owned, edited outside Brigade | skip (--force to overwrite) |
| orphan | Dropped from the catalog, still pristine | skip (--prune to remove) |
| orphan-edited | Dropped from the catalog, edited in place | leave untouched |
Shared Tools: The Portable Catalog
brigade tools describes local callable tools, slash commands, skills, superpowers, scripts, and MCP configs across every harness. It inspects local files, reports health, and can explicitly write reviewed projections or supervise approved runtimes. It does not start servers, fetch schemas, store auth, or auto-run anything. A tool call walks a gated path before it ever executes, and projection parity tracks every harness file so config drift surfaces as a reviewable issue rather than a surprise.
- Discover & contracttools list / search / describe surface portable entries and their JSON-Schema call contracts.
- Plantools call plan validates arguments and renders a safe execution plan without invoking anything.
- Queue & approvePlans wait in an approval queue; approve / reject / hold change status only, never run code.
- Run & receipttools call run executes one approved call and writes a local receipt with args, exit code, and fingerprints.
- ParityProjection fingerprints track each harness file; a parity closeout quiets unchanged issues, but a changed fingerprint resurfaces as drift to review.
Shared Skills: One Pack, Twenty Targets
Skills are treated as versioned workflow code: a reviewed SKILL.md pack with metadata, provenance, a trust level, and a stable fingerprint. One import lands it in a local registry; one install materializes it into every harness’s native skills folder with a receipt and a rollback snapshot. The supported targets are twenty in all - nineteen harnesses (Codex, Claude, OpenCode, Antigravity, Pi, Cursor, Aider, Goose, Continue, GitHub Copilot CLI, Qwen, Kimi, AdaL, OpenHands, Grok, Amp, Crush, OpenClaw, Hermes) plus a read-only MCP resource.
Friction Logs: Mining Pain Into Work
brigade friction scan reads recent work artifacts, notes, handoffs, and .learnings (optionally local Codex and Claude Code session logs) for buried workflow pain, and writes .brigade/friction/latest.json and latest.md. It is review-first: it does not create GitHub issues, edit memory, publish reports, or promote findings. Candidates only become tasks when you say so. It pattern-matches nine kinds of friction:
The Repo Fleet and Release Trains
brigade repos inspects the local repositories listed in a gitignored .brigade/repos.toml. It never clones, pushes, or mutates a remote: it collects safe health evidence, redacts private paths, and turns reviewed reports into a release train. A train classifies each repo as ready, blocked, needs-review, needs-dispatch, in-progress, stale-evidence, no-release-candidate, or deferred, then writes FLEET_RELEASE_TRAIN.md, FLEET_RELEASE_EVIDENCE.json, and a MANUAL_PUBLISH_PLAN.md of checklist steps. Brigade does not execute any publish step.
Execution: brigade run & the Aboyeur
brigade run "<task>" is a bounded cross-model orchestration. The aboyeur (the kitchen expediter) plans the task into staged JSON assignments; workers run in parallel within a stage through their own CLIs; later stages receive earlier results; the orchestrator synthesizes. It is intentionally bounded: two orchestrator calls plus the planned worker calls. No infinite loop. Each run also carries brigade-computed ground truth - a git diff --stat, the changed-file list, and parsed verify exit codes folded into synthesis, so the aboyeur reasons over facts, not worker narration - plus fail-open code-graph and upstream-drift briefs, ranked and clipped by a run-level brief budget, that hand workers blast-radius context before they edit.
orchestratorcli=codexcli=claudecli=...orchestratorThe Dispatch Loop
How "serial across stages, parallel within a stage, later stages see earlier results" is actually implemented.
src/brigade/aboyeur.py:756
all_results: list[WorkerResult] = []
stages = sorted({a.stage for a in assignments})
for stage in stages:
stage_assignments = [a for a in assignments if a.stage == stage]
prior_results = list(all_results)
max_workers = min(roster.max_workers, len(stage_assignments))
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_index = {
executor.submit(run_one, a, prior_results): i
for i, a in enumerate(stage_assignments)
}
for future in as_completed(future_to_index):
i = future_to_index[future]
try:
stage_results_by_index[i] = future.result()
except Exception as exc:
a = stage_assignments[i]
stage_results_by_index[i] = WorkerResult(
worker=a.worker, task=a.task, text="", ok=False, detail=str(exc)[:200])
all_results.extend(stage_results_by_index[i] for i in range(len(stage_assignments)))for stage in stages:Stages are serial; ordering is preserved so dependent work runs after its inputs.prior_results = list(all_results)Each stage receives a snapshot of every earlier worker’s output as context.min(roster.max_workers, len(stage_assignments))The pool never over-provisions: cap is the smaller of the roster limit and the stage size.except Exception ... ok=FalseA worker that crashes becomes a failed result, not an exception that kills the run.
The CLI Adapter Ring & Sandbox Enforcement
Brigade reaches models with no SDKs and no API keys: it builds subprocess argv for the user’s own authenticated CLIs. Because not every CLI can truly enforce read-only, Brigade is honest about it. hard = a native sandbox the model cannot escape; soft = read-only is only a prompt instruction; none = read_only is not applied at all. A writable --sandbox override downgrades even a hard CLI to prompt-only, with a loud advisory.
| hard | soft | none | |
|---|---|---|---|
| codex | |||
| antigravity | |||
| pi / cursor / aider | |||
| continue / qwen / kimi | |||
| goose / copilot / adal | |||
| openhands / grok | |||
| amp / crush | |||
| claudeargv ignores read_only entirely | |||
| opencode |
8 hard · 7 soft · 2 none. From READ_ONLY_ENFORCEMENT in agents.py. ollama:<model> refs always resolve to "none".
How read-only Is Enforced
The codex adapter is the cleanest example of the "build argv, no shell" pattern. Every external command is an argv list run through proc.run, which captures output and normalizes failure to exit codes.
src/brigade/agents.py:22 · src/brigade/proc.py:30
def _codex_argv(prompt, read_only, sandbox):
if sandbox:
return ["codex", "exec", "--sandbox", sandbox, prompt] # writable override
if read_only:
return ["codex", "exec", "--sandbox", "read-only", prompt] # hard sandbox
return ["codex", "exec", prompt]
# proc.run: no shell, bounded, failures become exit codes
cp = subprocess.run(args, capture_output=True, text=True,
timeout=timeout, check=False, stdin=subprocess.DEVNULL)
# FileNotFoundError -> Result(code=127, ...) # missing tool, not a raise
# TimeoutExpired -> Result(code=124, ...) # hung tool, bounded["codex", "exec", "--sandbox", "read-only", prompt]codex gets a real OS sandbox flag, which is why its enforcement is "hard".sandbox: ... return [..., "--sandbox", sandbox, ...]A writable --sandbox override is honored, downgrading enforcement to prompt-only with a loud advisory.subprocess.run(args, ... ) # no shell=Trueargv list, never a shell string: there is no shell-injection surface.stdin=subprocess.DEVNULLTools can never block waiting on input; a hung tool is killed by timeout (code 124).
Installation as Declarative Composition
brigade init does not run a script of imperative steps. It composes manifests (a depth manifest + one per selected harness + includes), dedupes them by destination so a harness can override a baseline file, renders the templates, and atomically rewrites a marker-delimited block in .gitignore. A repo-depth install now writes only AGENTS.md and SAFETY_RULES.md plus gitignored state by default; the full kit - workflow rules, the inactive pre-push hook, the default tool packs - moves behind --full.
src/brigade/install.py:265
# Dedupe files by dst (last-wins): a harness manifest can override a baseline file.
seen: dict[str, dict] = {}
for entry in files:
seen[entry["dst"]] = entry
deduped_files = list(seen.values())
# Per selected harness, write a managed, marker-delimited gitignore block:
for h in selection.harnesses:
inbox = WRITER_INBOXES.get(h)
if inbox:
lines += [f"{inbox}/*", # ignore session-local handoffs
f"!{inbox}/TEMPLATE.md", # but keep the template
f"!{inbox}/.gitkeep"]seen[entry["dst"]] = entryLast-wins by destination path: later (harness-specific) manifests override earlier (baseline) files.GITIGNORE_BEGIN ... markersRe-running brigade init only rewrites the content between the markers, never your edits outside them.f"{inbox}/*" + f"!{inbox}/TEMPLATE.md"Handoffs are private session context, so they are gitignored - but the shared template is kept.
Brigade as a Read-only MCP Server
Distinct from the MCP catalog sync above (brigade mcp, which writes server config into your tools), this is Brigade serving its own skills and memory cards back to agents over a zero-dependency, read-only MCP server: a single line-oriented JSON-RPC loop on stdin/stdout that only ever initializes, lists resources, and calls read tools. Managed tools follow the same humility: an absent or unconfigured fleet tool is advisory (WARN/MANUAL), never a hard failure of the workspace doctor.
src/brigade/mcp_server.py:50
for line in sys.stdin:
request = json.loads(line) # newline-delimited JSON-RPC 2.0
method = str(request.get("method") or "")
if method == "initialize":
_emit(response(id, result={"protocolVersion": "2024-11-05",
"capabilities": {"resources": {}, "tools": {}}}))
elif method == "resources/list":
_emit(response(id, result={"resources": list_resources()}))
elif method == "tools/call":
payload, failed = call_tool(name, arguments) # station-supplied callback
_emit(response(id, result={"content": [{"type": "text", "text": text}],
"isError": failed}))for line in sys.stdin:No framework: a plain newline-delimited JSON-RPC loop, which is why it has zero dependencies.initialize / resources/list / tools/callThe only methods it answers. There is no mutating method - the server is read-only by construction.call_tool(name, arguments)Each station supplies a read callback; the loop owns the JSON-RPC envelope.
The CLI Surface: 43 Commands, 5 Families
Everything above is reachable through one CLI. COMMAND_GROUPS (cli/_common.py) organizes all 43 top-level commands into five families; a test enforces that every command appears in exactly one group, so nothing can be silently un-grouped. Eighteen of the groups are an operator extras surface that registers only after brigade extras on (or BRIGADE_EXTRAS=1); the core stays twenty-five. The daily operator loop now centers on a verified work loop - work brief, then work verify run --capture <id>, then a handoff - so outcomes are recorded, not narrated. This is the map of where each capability lives.
inithandoffhandoff-templateingestmemorydoctorstatusprofiles
operatordailyworkfrictioncenterrunbookbudgetsnotifications
addstationsskillstoolsmcppantryrosterrunrunsdogfood
securityscrubuntrustedresearchlearnoutcomechatcontextprojects
releaseroadmapreposreconfigurecompletionsextrasopenclaw-fragmentshermes-fragments
From the Brigade to the Whole Kitchen
Brigade is the brigade de cuisine itself - the executive chef and the line - but it does not work alone. The stations you saw attach to managed tools (agentpantry, code-search, miseledger, and more) and the embedded content guard, each owning one job and handing off cleanly to the next, while the catalogs, friction lane, and fleet keep the whole line in sync.