This guide is for humans and AI coding agents contributing to Brigade. It covers the layout, the commands, the conventions you must follow, and the recipes for common changes. Read it before you touch the tree.
For user-facing docs see README.md. For the release flow see docs/RELEASING.md.
A single-operator, multi-agent AI-crew runtime written in TypeScript. It runs as a
headless WebSocket gateway (the state-holding daemon) with thin clients (a chat
TUI, connect, channel adapters). It is built on the pi SDK
(@earendil-works/pi-agent-core + @earendil-works/pi-ai +
@earendil-works/pi-coding-agent + @earendil-works/pi-tui, pinned exact at
0.79.9) and adds product layers on top: memory, skills, sub-agents, tools, an org
hierarchy, channels, cron, an extension SDK, and a dual-mode storage layer.
- Stack: TypeScript (strict), ESM, Node ≥ 22.12, npm (never pnpm).
- Build:
tsc→dist/. No bundler. - Tests: Node's built-in
node:test, run viatsx. - State: everything under
~/.brigade/(override:BRIGADE_STATE_DIR).
- The product is Brigade. In code, comments, copy, CLI help, error messages,
env vars, daemon labels, and docs, refer to the agent SDK only as
pi/@earendil-works/pi-coding-agent. Do not name other third-party AI agent projects. - Never commit secrets, real personal data (names, phone numbers, emails), or
local absolute paths. Use placeholders (
+1 555 010 0001,/path/to/project). - The mascot is 🦁 the Pride. No other animal/symbol.
templates/workspace/holds default persona files and is off-limits: behavioural fixes go in code, never in template content.- No
Co-Authored-Bytrailers on commits.
Daemon labels (don't rename): macOS launchd com.brigade.gateway, Linux systemd
brigade-gateway, Windows Task Scheduler BrigadeGateway.
| Task | Command |
|---|---|
| Build | npm run build (tsc -p tsconfig.build.json → dist/) |
| Build (watch) | npm run build:watch |
| Typecheck | npm run typecheck (tsc -p tsconfig.json --noEmit, includes tests) |
| Test (all) | npm test (node scripts/run-tests.mjs) |
| Test (one file) | npm test -- src/agents/tools/registry.test.ts |
| Memory eval/bench | npm run bench |
| Run built binary | npm run brigade -- <args> (e.g. npm run brigade -- agent -m "hi") |
| Dev (build-then-run) | npm run dev -- <args> |
| Dev (no build, tsx) | npm run dev:tsx -- <args> |
| Dev (auto-restart) | npm run watch |
| Clean | npm run clean |
Before opening a PR, all three of these must pass: npm run typecheck,
npm test, npm run build.
Test isolation: scripts/run-tests.mjs pins BRIGADE_STATE_DIR to a fresh
tempdir so tests never touch your real ~/.brigade. There are 281 .test.ts
files under src/.
brigade/ (working tree; storage is ~/.brigade)
├── brigade.mjs # bin shim: enforces Node ≥22.12, routes to dist/entry.js
├── package.json # npm; pi 0.79.9 pinned exact
├── tsconfig.json # typecheck config (includes tests)
├── tsconfig.build.json # build config (emits dist/, excludes tests/templates)
├── scripts/ # run-brigade.mjs, run-tests.mjs, build-done.mjs, convex-*.mjs
├── skills/ # 56 bundled skill directories
├── templates/workspace/ # default persona files — OFF LIMITS for edits
├── convex/ # Convex schema + functions (optional storage backend)
└── src/
├── entry.ts # CLI entry (fast-path version/help, lazy dispatch)
├── extension-sdk.ts # public plugin SDK (defineModule + re-exports)
├── cli/ # command files + program/build-program.ts (command registry)
├── core/ # gateway server.ts, daemon/ installers, dispatch
├── agents/ # the runtime (see below) — the bulk of the code
│ ├── agent-loop.ts # per-turn loop
│ ├── session-wiring.ts # toolset assembly + before-tool-call guards
│ ├── tools/ # Brigade-native tools + registry.ts
│ ├── memory/ # facts store, decay, auto-recall, consolidate
│ ├── skills/ # 6-source discovery, eligibility, manage
│ ├── subagent-*.ts # spawn, policy, abort cascade, completion bridge
│ ├── channels/ # adapter contract + inbound-pipeline + whatsapp/
│ ├── routing/ # inbound → (agentId, sessionKey)
│ ├── org/ # org/Pride hierarchy + A2A policy
│ └── extensions/ # plugin engine + bundled modules/ (web search, etc.)
├── tideline/ # long-term memory engine (hybrid recall, link graph)
├── system-prompt/ # assembler.ts + sections (persona pin, org anchor)
├── sessions/ # session store, write-lock, transcript repair
├── storage/ # dual-mode store: local/ (filesystem) + convex/
├── config/ # brigade.json schema, io, validators
├── providers/ # model provider catalog + auth detection
├── auth/ · security/ # auth profiles, encryption (libsodium seal)
├── cron/ # scheduler + isolated-agent run executor
├── tui/ · ui/ # terminal client + brand frames
└── workspace/ # persona file loaders (bootstrap.ts)
import { makeAgentsListTool } from "./agents-list-tool.js"; // ✅
import { makeAgentsListTool } from "./agents-list-tool"; // ❌ breaks at runtimeThis applies to every relative import. The project compiles TS → ESM (NodeNext).
strict, noUncheckedIndexedAccess, and noImplicitOverride are on. Avoid any
except for unavoidable schema/plugin boundaries. Tests must pass npm run typecheck.
Privileged tools use one of two postures — prefer the first:
- Per-call gate: the tool is registered for everyone but branches on
opts.senderIsOwnerto refuse mutating actions for channel peers (e.g.cron listis allowed,cron addis not). Per-action granularity. - Blanket
ownerOnly: true: refused to non-owners at registration. Use when every action is privileged (e.g.manage_provider,composio).senderIsOwnerdefaults tofalse; only an explicit owner flow sets ittrue.
Every memory read/write threads a MemoryRecordOrigin (owner vs.
channel+conversationId+sessionKey). Owner facts are never visible to peers;
channel facts recall only on an exact origin match. Dedup is same-origin only.
Auto-recall filters by origin before injecting. Any new memory path must thread
origin or isolation breaks silently.
brigade.json writes keep 5 forensic backups (.bak + .bak.{1..4}, mode 0600).
Secrets use ${VAR_NAME} references that resolve at read and are restored (not
persisted resolved) on write. Keep brigade secrets audit clean.
Brigade does not walk project rule files. Persona comes from
~/.brigade/workspace/ only.
- Create
src/agents/tools/my-tool.tsexporting amakeMyTool()factory that returns aBrigadeTool(TypeBox params,jsonResult). - Register it in
src/agents/tools/registry.ts(createBrigadeTools). - Choose a gate posture (per-call
senderIsOwnerrecommended). - Add
src/agents/tools/my-tool.test.ts. Three enumeration tests typically need updating when a tool is added: the registry, session-wiring, and owner-only tests. (Tests that assert exact tool counts neutralizeCOMPOSIO_API_KEY— keep that pattern.)
Implement the channel adapter contract (config / gateway / outbound / security /
status / message-action / secrets), register via b.channel(...) in a module,
lazy-load heavy deps, ship reconnect-with-backoff + crypto-error narrowing + JID
canonicalization, and pass dedupe + reply-sanitizer + abort-trigger tests.
WhatsApp (src/agents/channels/whatsapp/) is the reference implementation.
Drop a directory under skills/ (bundled) or ~/.brigade/skills/ (managed, via
manage_skill). Frontmatter needs name + description (the discovery hook), and
optional eligibility metadata (requires-bins, requires-env, requires-config,
OS). Don't lift a per-cwd walker.
Create src/agents/extensions/modules/<name>.ts and register against the right
b.* slot from src/extension-sdk.ts. Web-search modules carry an
autoDetectOrder and an isConfigured(cfg, env) predicate.
Add a file under src/cli/commands/ and register it in
src/cli/program/build-program.ts. Update brigade doctor checks if relevant.
Brigade has a dual-mode storage seam (src/storage/): a BrigadeStore interface
with ~16 typed sub-stores, implemented by local/ (filesystem, the default) and
convex/ (optional). The mode is resolved at boot from a sticky sentinel and
freezes for the process. Toggle with brigade store mode set <filesystem|convex>;
copy data with brigade store migrate.
When touching storage, keep both backends in parity and never assume the path is filesystem-shaped.
npm run convex:install— downloads the standaloneconvex-local-backendRust binary + dashboard intobin/(gitignored). License FSL-1.1-Apache-2.0.npm run convex:dev— installs (no-op if cached), then starts the backend (http://127.0.0.1:3210), site proxy (:3211), and dashboard (:6791). It also writes.env.local(gitignored) with the self-hosted URL + admin key.npm run convex:codegen— regeneratesconvex/_generated/against the running local backend.
Per-machine state lives under .convex-data/ (gitignored): identity.json,
admin-key.txt, the SQLite engine file, File Storage, and logs. npx convex dev
is only safe when the self-hosted env vars from .env.local are active; without
them it would try to claim a cloud project — don't run it in that state.
- Auth files live at
<agentDir>/agent/{auth-profiles,models}.json, mode 0600. - Sessions are pi JSONL transcripts at
<agentDir>/sessions/<sessionId>.jsonl, one file per session, indexed bysessions.json. They have a write-lock and transcript-repair on crash. - Never overwrite
session.agent.streamFn—createAgentSessioninstalls an auth-aware wrapper; replacing it silently breaks all model calls. - Persona pin clobbers pi's skill injection — Brigade renders skills into the
assembled prompt itself (
applyPersonaOverrideToSessionis latched once per process). Don't revert to pi auto-injection. thinkingLevelmust be reasoning-aware — some models reject"off"; derive it frommodel.reasoning.- Heartbeat is workspace-driven via
HEARTBEAT.md; strip theHEARTBEAT_OKtoken from output before delivery. - Sub-agent depth is encoded in the session key; leaf agents (at max depth) lose the spawn tools entirely, and abort cascades to descendants.
core/server.ts/ gateway → verify thinking-persist, hot-reload, guard-sweep, model-set tests are green.agents/channels/→ the adapter contract honoured; WhatsApp is the reference; lazy-load discipline preserved.cron/→--cronpaired with an IANA--tz; isolated runs stay inside~/.brigade/cron/runs/.agents/subagent-*→ abort cascade + completion ordering + wake-on-settle green.agents/tools/registry.ts→ pi built-ins intact (read/write/edit/bash/grep/find/ls); gate posture documented in the tool's leading comment.agents/memory/→ origin threading preserved; dedup same-origin only; auto-recall filters before inject.system-prompt/→ assembler keeps the canonical sections and the persona pin.
- Use Conventional Commits.
feat/fix/perf/deps/reverttrigger a release;docs/refactor/test/ci/choredo not. - Commit messages explain why, not what. No
Co-Authored-By: Claudetrailer. - Releases are automated via release-please → npm publish (
@spinabot/brigade). See docs/RELEASING.md.