Brigade v1 — what ships in 5.5 hours.
A concrete build spec. Pi SDK as the engine, OpenClaw-grade internals, NanoClaw-style smallness, Boop-pattern memory + sub-agents. Every code snippet on this page is verified against live source in the workspace on May 1, 2026.
What Brigade IS
A personal headless agent service with a single core process, multiple streaming channel clients (TUI → Web → Mobile), persistent file-based memory, file-based skills, sub-agent support, and a deliberately small surface — built on Pi SDK so the loop is rock-solid from day one.
Identity
OpenClaw's brain, NanoClaw's smallness, in a fresh codebase you fully understand and control.
Foundation
Pi SDK (@mariozechner/pi-coding-agent@0.70.6) — the same stack OpenClaw runs on.
Wire format
WebSocket on ws://localhost:7777 — TUI, web, mobile all speak one protocol.
Single headless core. Many channel clients. One wire format. Every box maps to a real file in §4.
v1 · Pi-TUI"] WEB["🌐 Web
v1.5 · Vite+React"] MOB["📱 Mobile
v2 · Expo / RN"] end subgraph core["BRIGADE CORE · single Node process"] direction TB WS["WebSocket Gateway
ws://localhost:7777"]
BA["BrigadeAgent wrapperPi event → WS broadcast"] PI["Pi Agent loop
@mariozechner/pi-agent-core"] SP["Layered system prompt
cache-stable · 8 .md files"] SM["Pi SessionManager
JSONL tree"] MEM["Memory Store
JSONL per segment"] SK["Skills loader
Pi DefaultResourceLoader"] TOOLS["Tools · 8 v1
read · bash · edit · write · grep
+ write_memory · recall_memory · spawn_agent"] SUB["spawn_agent → fresh Pi Agent
scoped tools · own context"] end subgraph providers["LLM PROVIDERS · pluggable via Pi-AI"] direction LR AN["Anthropic
claude-opus-4-7"] OAI["OpenAI"] GR["Groq · Ollama · OpenRouter"] end TUI --> WS WEB --> WS MOB --> WS WS --> BA BA --> PI PI --> SP PI --> SM PI --> TOOLS PI --> SK TOOLS --> MEM TOOLS --> SUB PI --> AN PI --> OAI PI --> GR classDef channel fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a,stroke-width:2px classDef coreBox fill:#fde7c4,stroke:#d97706,color:#92400e,stroke-width:2px classDef provider fill:#d1fae5,stroke:#047857,color:#064e3b,stroke-width:2px class TUI,WEB,MOB channel class WS,BA,PI,SP,SM,MEM,SK,TOOLS,SUB coreBox class AN,OAI,GR provider
End-to-end — keystroke to streamed reply
Every box in the architecture diagram is a step in this sequence. Trace your finger from keystroke to render — that's the whole life of one turn.
A typical turn that calls one tool. ~14 hops, ~2-4 seconds wall clock for a short reply.
(Pi-TUI Editor) participant WSC as ws-client.ts
(in TUI) participant WSS as ws-server.ts
(in Core) participant BA as BrigadeAgent
wrapper participant PI as Pi Agent loop
(pi-agent-core) participant AI as Anthropic API
(via pi-ai streamFn) participant T as write_memory
tool participant FS as 💾 .brigade/memory/
identity.jsonl U->>TUI: types "remember I prefer Python"
+ hits Enter TUI->>WSC: editor.onSubmit(text) WSC->>WSS: { type:"prompt", text } via WebSocket WSS->>BA: brigadeAgent.prompt(text) BA->>PI: agent.prompt(text) + emits "agent_start" WSS-->>TUI: broadcast "agent_start" → spinner shows PI->>PI: assembleSystemPrompt() reads .brigade/prompts/*.md PI->>PI: auto-recall hook injects memory citations PI->>AI: streamFn(model, context, tools)
HTTP POST + cache headers AI-->>PI: stream: text_delta + tool_use(write_memory,{...}) PI-->>BA: emits "message_delta" per chunk BA-->>WSS: broadcast each delta WSS-->>TUI: deltas → Markdown component updates PI->>PI: AJV validates tool args (TypeBox schema) PI->>T: execute(toolCallId, params, signal, onUpdate) T->>FS: fs.appendFile("identity.jsonl", JSON.stringify(entry)) FS-->>T: ok T-->>PI: { content:[{text:"Stored mem_..."}], details:{id} } PI->>AI: continue streamFn with toolResult appended AI-->>PI: stream: final text "Got it — I'll remember that." PI-->>BA: emits "message_end" + "agent_end" BA-->>WSS: broadcast "agent_end" WSS-->>TUI: agent_end → spinner clears TUI->>U: rendered reply visible
Editor submit fires; TUI's WS client serializes a single JSON message and sends it. Core's WS server parses and dispatches to brigadeAgent.prompt().
BrigadeAgent wraps Pi. assembleSystemPrompt() reads layered .md files and inserts the cache boundary. Auto-recall hook keyword-greps memory and prepends top matches.
pi-ai issues HTTP to Anthropic with prompt-cache headers. Streaming chunks fan out to every WS client as message_delta. When a tool_use block arrives, AJV validates args against the TypeBox schema before execute() ever runs.
Tool returns {content, details}. Pi appends a toolResult message and re-enters the loop. Anthropic returns the final text. Pi emits agent_end. WS broadcasts. TUI clears the spinner and finalizes the markdown.
Where the wall-clock seconds go. Anthropic streaming dominates; everything else is sub-100ms.
What "OpenClaw quality" actually means
These six primitives ARE the agent. Get every one to grade-A in v1 and Brigade is peer-grade with OpenClaw / Boop / Hermes at the runtime layer. Skip even one and Brigade is a toy.
| Primitive | Toy version | Brigade v1 bar (grade-A) |
|---|---|---|
| Agent Loop | Calls model in a while-loop | Pi Agent with abort, steer, followUp, hooks, transformContext, AbortSignal threaded everywhere |
| System Prompt | One big string concatenated each turn | Layered .md files (soul / identity / instructions / tools), explicit cache boundary, dynamic memory section last → 10× cost win from turn 2 |
| Tools | Map of name → function | TypeBox schemas + AJV validation + onUpdate streaming + {content, details} split + before/after hooks |
| Memory | Array in RAM, lost on restart | File-based JSONL with frontmatter + auto-recall extension hook + post-turn extraction (Boop pattern) |
| Skills | All loaded at boot, full bodies in prompt | Pi auto-discovery + eligibility filtering + lazy body load. Drop-a-folder-it-works. |
| Sub-agents | Recursive function call | Boop dispatcher/executor pattern — isolated Pi session, scoped tools, abort propagation, result-as-tool-result |
Each axis = one primitive scored 0–10. Brigade v1 targets grade-A (8/10) on all six, matching OpenClaw's runtime quality at a fraction of OpenClaw's surface area.
Pi as engine, lifts as accelerators
Every layer named, every choice justified, every source linked.
| Layer | Decision | Source / lift |
|---|---|---|
| Loop engine | Pi Agent | @mariozechner/pi-agent-core |
| Sessions | Pi JSONL tree (id/parentId) | pi-coding-agent SessionManager |
| Built-in tools | read · bash · edit · write · grep | Pi createCodingTools() |
| WS gateway | 17-line broadcast pattern | Lift boop/server/broadcast.ts |
| Sub-agent | spawn_agent tool → fresh Pi Agent | Boop execution-agent.ts:92-248 |
| Memory tools | JSONL file per segment | Boop memory/tools.ts de-Convex'd |
| System prompt | Layered .md + cache boundary | OpenClaw system-prompt.ts pattern |
| Skills loader | Pi DefaultResourceLoader | Pi coding-agent built-in |
| Auto-recall | Pi context extension hook | Pi extension API |
| TUI | Pi-TUI components (no flicker) | @mariozechner/pi-tui |
| Wire format | WebSocket JSON, ~15 message types | Brigade-original (Pi events 1:1) |
| Default model | claude-opus-4-7 | Anthropic via Pi-AI |
| Code style | TS strict + Biome | Same as pi-mono |
Every dependency, every version, every role
Brigade's full stack — eight layers, ~14 direct dependencies, zero magic. Versions pinned to what the local pi-mono repo and boop-agent use today.
Layer 1 · Loop engine
Pi SDKThe agent loop itself — model call → tool dispatch → result back → loop. Multi-provider streaming, abort, hooks.
"@mariozechner/pi-agent-core": "0.70.6" "@mariozechner/pi-ai": "0.70.6"
Layer 2 · Sessions + tools + extensions
Pi SDKJSONL session tree, ResourceLoader (auto-discovers skills/extensions), AuthStorage, ModelRegistry, built-in coding tools.
"@mariozechner/pi-coding-agent": "0.70.6"
Layer 3 · Terminal UI
Pi SDKDifferential rendering (no flicker), Markdown component, Editor with autocomplete, Loader spinner.
"@mariozechner/pi-tui": "0.70.6" "chalk": "^5"
Layer 4 · LLM provider
ExternalDefault Anthropic. Pi-AI swaps providers with one line. 8+ supported including OpenAI, Google, Groq, Ollama, OpenRouter.
"@anthropic-ai/sdk": "^0.81" // model: "claude-opus-4-7"
Layer 5 · WebSocket gateway
Lift Boop17-LOC broadcaster lifted from boop-agent. One core, many channel clients.
"ws": "^8" "@types/ws": "^8"
Layer 6 · Schema + validation
Inherited via PiTypeBox describes tool params; AJV validates at runtime before execute(). Single source of TS types + JSON schemas.
"@sinclair/typebox": "^0.34" // ajv inherited via pi-agent-core
Layer 7 · Build & dev tooling
Standardpnpm workspace, TypeScript 5 strict, Biome (lint + format), tsx for dev. Same setup as pi-mono.
"typescript": "^5" "tsx": "^4" "@biomejs/biome": "^1.9" # pnpm 8+ as package mgr
Layer 8 · Runtime
StandardNode.js 20+ for the core. Bun optional for the TUI (faster startup). Zero browser dependencies in v1.
# engines.node >= 20.0.0 # platform: macOS / Linux / Windows
Same WebSocket protocol — different renderers. Each new channel is just a UI on top of an unchanged core.
Aligned to D:\nodebase exactly so Brigade can mount inside nodebase as a route or sub-app later with zero stack rewiring.
"next": "^16.1.0" // Turbopack "react": "19.1.0" "react-dom": "19.1.0" "tailwindcss": "^4" "ai": "^5.0.60" // Vercel AI SDK 5 "@ai-sdk/anthropic": "^2.0.23" "@radix-ui/react-*": "latest" // shadcn primitives "lucide-react": "latest" "convex": "^1.18" // optional sync // shadcn style: "new-york" / base: "neutral" // also lifts Boop's debug/ panels for memory + agent timeline
Same component vocabulary as v1.5 (Radix → React Native via NativeWind + RN-Reusables), so design tokens carry across web/mobile.
"expo": "~51" "expo-router": "~3.5" "react-native": "0.74" "react": "19.1.0" "nativewind": "^4" // Tailwind for RN "react-native-reusables": "latest" // shadcn-port "expo-notifications": "~0.28" "ai": "^5.0.60" // reuse v1.5 WS client logic + types verbatim
Every package.json Brigade v1 ships with
Five package.json files total. Workspace root + four leaf packages. Copy-paste these to bootstrap.
Root — workspace manifest
Just a workspace shell. No runtime deps at the root; only dev tooling shared across all apps and packages.
{
"name": "brigade",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.12.0",
"engines": { "node": ">=20.0.0" },
"scripts": {
"dev:core": "pnpm --filter @brigade/core dev",
"dev:tui": "pnpm --filter @brigade/tui dev",
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r build",
"check": "biome check .",
"format": "biome format --write ."
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.7.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
}
}
Core — the headless agent service
Wraps Pi's Agent, runs WS server on port 7777, persists sessions and memory. Three Pi packages + WebSocket + Anthropic SDK = the whole engine.
{
"name": "@brigade/core",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.81.0",
"@brigade/protocol": "workspace:*",
"@mariozechner/pi-agent-core": "0.70.6",
"@mariozechner/pi-ai": "0.70.6",
"@mariozechner/pi-coding-agent": "0.70.6",
"@sinclair/typebox": "^0.34.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/ws": "^8.5.12"
}
}
TUI — the v1 channel client
Pi-TUI for the renderer (no flicker, markdown, editor with autocomplete). ws for the connection. Tiny.
{
"name": "@brigade/tui",
"version": "1.0.0",
"private": true,
"type": "module",
"bin": { "brigade": "./dist/index.js" },
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@brigade/protocol": "workspace:*",
"@mariozechner/pi-tui": "0.70.6",
"chalk": "^5.3.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/ws": "^8.5.12"
}
}
Protocol — WebSocket message types
Pure TS types. Zero runtime dependencies. Imported by core and every channel client so message shapes stay in sync.
{
"name": "@brigade/protocol",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": "./src/index.ts" },
"scripts": {
"build": "tsc -p tsconfig.json"
}
}
Workspace — the package graph
Two glob patterns describe the whole repo. apps/* and packages/* — that's it.
packages: - "apps/*" - "packages/*"
Pinned to what the local reference repos actually use today.
| Package | Version | Why pinned here |
|---|---|---|
@mariozechner/pi-* | 0.70.6 | Latest stable in pi-mono/packages/*/package.json on May 1, 2026. OpenClaw is pinned to the older 0.66.1; Brigade ships fresh and tracks current. |
@anthropic-ai/sdk | ^0.81.0 | Same major as OpenClaw's pinned override; Pi-AI tested against this line |
@sinclair/typebox | ^0.34.0 | Pi-AI re-exports Type from this; explicit dep avoids hoisting surprises |
ws | ^8.18.0 | Same as Boop's broadcast.ts target; native Node WS, no transitive bloat |
typescript | ^5.6.0 | Matches pi-mono's target (verified in pi-mono/package.json) |
tsx | ^4.19.0 | Fastest TS dev runner; supports ESM + watch mode out of the box |
@biomejs/biome | ^1.9.4 | Same as pi-mono. One binary for lint + format, zero config beyond biome.json |
pnpm | 9.12.0 | Best workspace ergonomics; deterministic installs; same as pi-mono |
Where every line lives
~1,200 LOC of trunk code, organized as a pnpm workspace. Apps consume packages. Everything new lives in brigade/.
brigade/ ├── apps/ │ ├── core/ # headless service (v1) │ │ └── src/ │ │ ├── index.ts [NEW] boot: load tools, start WS │ │ ├── ws-server.ts [lift Boop] WebSocket broadcast (~50 LOC) │ │ ├── brigade-agent.ts [NEW] wraps Pi Agent + WS broadcast │ │ ├── prompt-assembler.ts [NEW] layered .md + cache boundary │ │ ├── session-manager.ts [Pi] thin wrapper over Pi SessionManager │ │ ├── memory/ │ │ │ ├── store.ts [NEW] file-based JSONL (~80 LOC) │ │ │ └── inject.ts [NEW] auto-recall context hook │ │ └── tools/ │ │ ├── memory.ts [adapt Boop] write_memory, recall_memory │ │ └── spawn.ts [adapt Boop] spawn_agent (sub-agent) │ ├── tui/ # v1 channel │ │ └── src/ │ │ ├── index.ts [NEW] entry, ws://localhost:7777 │ │ ├── ws-client.ts [NEW] receive + dispatch │ │ ├── render.ts [Pi-TUI] Markdown + Loader components │ │ └── input.ts [Pi-TUI] Editor + autocomplete │ ├── web/ # v1.5 placeholder (lift Boop debug/) │ └── mobile/ # v2 placeholder (Expo) ├── packages/ │ └── protocol/ │ └── src/index.ts [NEW] WS message types (TS union) ├── .brigade/ │ ├── memory/ # auto-created, .jsonl per segment │ ├── skills/ # user drops SKILL.md here, Pi auto-discovers │ ├── prompts/ # soul.md, identity.md, instructions.md, tools.md │ └── sessions/ # Pi JSONL transcripts ├── BRIGADE.md [NEW] Brigade's own AGENTS.md ├── package.json ├── pnpm-workspace.yaml ├── tsconfig.base.json └── biome.json
Legend: [NEW] Brigade-original · [lift / adapt] taken from another repo · [Pi / Pi-TUI] consumed from Pi SDK as-is
Most of Brigade's code is the TUI client and the core orchestration. Memory + tools + protocol total under 350 LOC. Pi handles everything else.
Every snippet, every source — verified against live repos
A subagent re-read the source on May 1, 2026. Every signature, every import, every line below traces to a file in this workspace.
pi-mono/packages/agent/src/agent.ts:190
Pi Agent — the loop, free
Brigade doesn't write the loop. It instantiates Pi's Agent and adds a thin wrapper that broadcasts every event over WebSocket. Multi-provider streaming, abort, steer, followUp, hooks — all inherited.
import { Agent } from "@mariozechner/pi-agent-core"; import { streamSimple, getModel } from "@mariozechner/pi-ai"; const agent = new Agent({ initialState: { systemPrompt: await assembleSystemPrompt("./.brigade"), model: getModel("anthropic", "claude-opus-4-7"), tools: brigadeTools, thinkingLevel: "off", }, streamFn: streamSimple, }); agent.subscribe(broadcastToWS); // every Pi event → all WS clients await agent.prompt(userMessage); // runs to completion, streams over WS
AgentTool from pi-mono/packages/agent/src/types.ts:308
The write_memory tool — TypeBox schema, dual content/details
Every Brigade tool follows this exact shape: TypeBox schema → typed params, returns content (goes to LLM) + details (goes to UI). AJV validates before execute() ever runs.
import { Type } from "@mariozechner/pi-ai"; import type { AgentTool } from "@mariozechner/pi-agent-core"; const writeMemoryParams = Type.Object({ content: Type.String({ description: "Fact to remember" }), segment: Type.Union([Type.Literal("identity"), Type.Literal("preference"), Type.Literal("project"), Type.Literal("knowledge")]), importance: Type.Number({ minimum: 0, maximum: 1 }), }); export const writeMemoryTool: AgentTool<typeof writeMemoryParams> = { name: "write_memory", label: "Write Memory", description: "Persist a durable fact for future turns", parameters: writeMemoryParams, async execute(_id, params) { const id = await memoryStore.write(params); return { content: [{ type: "text", text: `Stored ${id}` }], // → LLM sees this details: { id, ...params }, // → UI only }; }, };
boop/server/memory/tools.ts:18 (Convex stripped)
File-based memory — JSONL per segment, no DB needed
Boop's memory model is gold; its Convex backend is overkill for v1. Brigade keeps the segment / importance / lifecycle vocabulary, swaps storage to JSONL files. Trivial to upgrade to SQLite-FTS5 later if keyword search struggles.
import * as fs from "node:fs/promises"; import * as path from "node:path"; const MEMORY_DIR = "./.brigade/memory"; const SEGMENTS = ["identity", "preference", "project", "knowledge"] as const; export async function write(record: { content: string; segment: string; importance: number }) { await fs.mkdir(MEMORY_DIR, { recursive: true }); const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const entry = { id, ...record, created: Date.now(), accessed: Date.now() }; await fs.appendFile(path.join(MEMORY_DIR, `${record.segment}.jsonl`), JSON.stringify(entry) + "\n"); return id; } export async function recall(query: string, limit = 10) { const hits: any[] = []; for (const seg of SEGMENTS) { try { const lines = (await fs.readFile(path.join(MEMORY_DIR, `${seg}.jsonl`), "utf8")) .split("\n").filter(Boolean); for (const line of lines) { const rec = JSON.parse(line); if (rec.content.toLowerCase().includes(query.toLowerCase())) hits.push(rec); } } catch {} } return hits.slice(0, limit); }
boop/server/execution-agent.ts:92
Dispatcher → Executor — Boop's pattern, Pi-flavored
The most powerful agent pattern in production. Cheap dispatcher answers chit-chat directly; only spawns an expensive executor when real work is needed. Each spawn = isolated Pi session, scoped toolset, separate context window. Result returns as a tool result.
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent"; import type { AgentTool } from "@mariozechner/pi-agent-core"; const spawnParams = Type.Object({ task: Type.String({ description: "What the sub-agent should do" }), tools: Type.Optional(Type.Array(Type.String())), }); export const spawnAgentTool: AgentTool<typeof spawnParams> = { name: "spawn_agent", label: "Spawn Agent", description: "Delegate a focused task to a sub-agent with its own context window", parameters: spawnParams, async execute(toolCallId, { task, tools }, signal) { const { session } = await createAgentSession({ model: getModel("anthropic", "claude-opus-4-7"), sessionManager: SessionManager.inMemory(), customTools: filterTools(tools ?? defaultExecutorTools), }); session.agent.setSystemPrompt(`You are an executor. Your single task: ${task}`); await session.agent.prompt(task); const finalText = extractFinalText(session.messages); return { content: [{ type: "text", text: finalText }], details: { sessionId: session.id, toolsUsed: session.toolUsageStats() }, }; }, };
spawn_agentParent agent stays cheap. Executor gets its own context window and tool subset. Result returns as a normal tool result.
(via TUI) participant P as Parent Brigade Agent
(dispatcher) participant T as spawn_agent tool participant E as Executor (fresh Pi Agent) participant LLM as Anthropic API U->>P: "Refactor the auth module" P->>LLM: prompt + tools (incl. spawn_agent) LLM-->>P: tool_use spawn_agent({ task, tools:["read","edit","grep"] }) P->>T: execute(toolCallId, params, signal) T->>E: createAgentSession({ scoped tools }) E->>LLM: own loop, own context window LLM-->>E: streaming response + tool calls E-->>E: read/edit/grep iteratively E-->>T: final assistant text T-->>P: { content:[{text}], details:{sessionId, toolsUsed} } P->>LLM: continue with tool result LLM-->>P: final reply to user P-->>U: streamed via WebSocket
openclaw/src/agents/system-prompt.ts:39
Cache-stable prompt assembly — 10× cost win from turn 2
OpenClaw orders its 8 prompt files by priority (10–70) and explicitly marks where the Anthropic prompt cache should boundary. Stable identity above; volatile time/memory below. Cache hit rate determines whether Brigade costs $0.10 or $1.00 per turn.
import * as fs from "node:fs/promises"; const CACHE_BOUNDARY = "<!-- BRIGADE_CACHE_BOUNDARY -->"; const ORDER = [ // stable first, volatile last { file: "soul.md", pri: 20 }, // personality / tone { file: "identity.md", pri: 30 }, // "You are Brigade…" { file: "instructions.md", pri: 40 }, // hard rules / prohibitions { file: "tools.md", pri: 50 }, // how to use tools ]; export async function assembleSystemPrompt(brigadeDir: string): Promise<string> { const stable: string[] = []; for (const { file } of ORDER) { try { stable.push(await fs.readFile(`${brigadeDir}/prompts/${file}`, "utf8")); } catch {} } const volatile = [ `Current time: ${new Date().toISOString()}`, await renderRecentMemoryCitations(), // changes every turn ].join("\n\n"); return stable.join("\n\n---\n\n") + "\n\n" + CACHE_BOUNDARY + "\n\n" + volatile; }
boop/server/broadcast.ts — 17 LOC, MIT
The smallest WS gateway that works
No room to mess this up. Boop's broadcast is 17 lines and has been running in production for months. Brigade copies it verbatim with attribution.
// adapted from boop-agent (MIT) — server/broadcast.ts import type { WebSocket } from "ws"; const clients = new Set<WebSocket>(); export function addClient(ws: WebSocket): void { clients.add(ws); ws.on("close", () => clients.delete(ws)); } export function broadcast(event: string, data: unknown): void { const payload = JSON.stringify({ event, data, at: Date.now() }); for (const ws of clients) { if (ws.readyState === 1) ws.send(payload); } }
pi-mono/packages/tui + the Pi gist
Pi-TUI markdown + editor — flicker-free, autocompleted
The same TUI components OpenClaw renders. Differential rendering means no flicker. Autocomplete provider for slash commands. WebSocket events drive the renderer.
import { TUI, ProcessTerminal, Editor, Markdown } from "@mariozechner/pi-tui"; import { connect } from "./ws-client.js"; const tui = new TUI(new ProcessTerminal()); const editor = new Editor(tui, theme); tui.addChild(editor); tui.setFocus(editor); const ws = connect("ws://localhost:7777"); let streamingMd: Markdown | null = null; let buffer = ""; ws.on("message_delta", ({ delta }) => { buffer += delta; if (!streamingMd) { streamingMd = new Markdown(buffer); tui.insertBeforeFocus(streamingMd); } else streamingMd.setText(buffer); tui.requestRender(); }); ws.on("agent_end", () => { streamingMd = null; buffer = ""; }); editor.onSubmit = (text) => ws.send({ type: "prompt", text }); tui.start();
packages/protocol/src/index.ts
The Brigade wire — discriminated union, 1:1 with Pi events
Channels (TUI, web, mobile) speak this. ~15 message types. Server messages mirror Pi event shapes so the client renderer is dead simple.
export type ClientMessage = | { type: "prompt"; sessionId: string; text: string; images?: ImageContent[] } | { type: "abort"; sessionId: string } | { type: "steer"; sessionId: string; text: string } | { type: "follow_up"; sessionId: string; text: string } | { type: "set_model"; sessionId: string; provider: string; modelId: string } | { type: "list_sessions" } | { type: "open_session"; sessionId: string } | { type: "new_session" }; export type ServerMessage = | { type: "agent_start"; sessionId: string } | { type: "turn_start"; sessionId: string } | { type: "message_start"; sessionId: string; role: string } | { type: "message_delta"; sessionId: string; delta: string } | { type: "message_end"; sessionId: string; message: AgentMessage } | { type: "tool_execution_start"; sessionId: string; toolCallId: string; toolName: string; args: any } | { type: "tool_execution_update"; sessionId: string; toolCallId: string; partial: any } | { type: "tool_execution_end"; sessionId: string; toolCallId: string; result: any; isError: boolean } | { type: "turn_end"; sessionId: string; usage: TokenUsage } | { type: "agent_end"; sessionId: string } | { type: "session_list"; sessions: SessionMeta[] } | { type: "error"; message: string };
5.5 hours, end-to-end
Pair-programmed with Claude Opus 4.7. Each step is sequential — finish before the next. Times are realistic, not optimistic.
Cumulative ~5.5 hours. The TUI client is the longest single step (1 hr). Lifts from Boop / OpenClaw shrink the rest.
Scaffold pnpm workspace
Create brigade/, root package.json + pnpm-workspace.yaml, base tsconfig.json, biome.json, .gitignore, empty .brigade/ dirs.
Protocol package
Write packages/protocol/src/index.ts — the discriminated-union message types. ~50 LOC. Shared by core and every channel.
WS gateway (lift Boop verbatim)
Copy broadcast.ts from boop-agent. Add ws-server.ts that accepts connections on port 7777 and parses incoming JSON.
Brigade Agent wrapper
Write brigade-agent.ts — wraps Pi Agent, subscribes to all events, broadcasts each over WS. ~80 LOC.
Layered system prompt assembler
Port OpenClaw's pattern. Reads .brigade/prompts/*.md in priority order, inserts cache boundary, appends volatile section. ~40 LOC.
Memory store + tools
File-based JSONL implementation + write_memory + recall_memory tools with TypeBox schemas. ~120 LOC total.
spawn_agent tool
Adapt Boop's executor pattern. Tool that creates a fresh Pi session with scoped tools, runs to completion, returns final text. ~60 LOC.
20 minAuto-recall extension
Pi context hook that runs recall_memory on the last user message and injects top-N matches into the system prompt. ~40 LOC.
Core entry point
apps/core/src/index.ts — wires Pi createAgentSession() with all custom tools, starts WS server, handles client messages. ~100 LOC.
TUI channel client
apps/tui/src/* — Pi-TUI components, WS client, event-driven render, editor for input with slash-command autocomplete. ~250 LOC.
Smoke test
Boot core, boot TUI, run a real conversation: file ops + memory write/recall + one sub-agent spawn. Fix what breaks.
30 minWhat we deliberately don't build
Each of these is a real feature in OpenClaw or Hermes. Each is also a multi-week side quest. Defer them all to v1.5+.
Every version from v1 → v5+ in one place
Permanent reference. Each grade is a named bundle of capability with a fixed time estimate, exact tech additions, and a "definition of done" so you know when to ship.
A single-glance index. Find your current grade, look right to see what's already in, look down to see what's coming.
| Grade | Theme | Time | Status |
|---|---|---|---|
| v1 | The 6 primitives at grade-A · TUI · sub-agents | ~5.5 h | 🎯 next build |
| v1.5 | Web dashboard (Next.js 16 · drop-in to nodebase) | ~1 weekend | queued |
| v2 | Mobile (Expo + NativeWind) | ~1 weekend | queued |
| v2.5 | Memory upgrade — SQLite-FTS5 + decay + embeddings | ~3-5 days | queued |
| v3 | Production hardening — approvals · budgets · audit · evals | ~2-3 weeks | queued |
| v3.5 | Sandbox via Docker (per-session container) | ~1 week | queued |
| v4 | Multi-channel — Telegram · Discord · Slack · scheduler | ~2-4 weeks | queued |
| v5 | ACP server — Brigade in Zed · Cursor · JetBrains | ~1 week | queued |
| v5+ | Self-improving learning loop (Hermes-inspired) | research | exploratory |
Brigade Core — the engine
Everything in this brief from §1 through §6. Ships a real working terminal agent.
Adds- Pi Agent loop + 6 primitives at grade-A
- 8 tools (read/bash/edit/write/grep + memory ×2 + spawn_agent)
- WS gateway on
:7777 - Pi-TUI channel with markdown + editor
- JSONL sessions + JSONL memory
- Layered system prompt with cache boundary
- TUI starts, connects, streams a turn end-to-end
- spawn_agent runs an executor and returns text
- write_memory + recall_memory persist across restarts
Web dashboard — drop-in to nodebase
Next.js 16 web app aligned to D:\nodebase's exact stack so it can mount inside nodebase as a route or sub-app later.
- Next.js 16 + React 19 + Tailwind 4 + Turbopack
- shadcn/ui (new-york style, neutral base)
- Vercel AI SDK 5 for any direct LLM calls in the UI
- Sessions browser · agents timeline · memory graph (lift Boop debug)
- WS client mirrors Pi event stream → live updates
- Optional Convex sync (
@convex-dev/agent) for shared sessions across devices
- Web client renders the same conversation as TUI in real time
- Brigade dashboard mounts at
/brigadeinside nodebase without conflict - Auth: piggyback nodebase's Better Auth + Polar when integrated
Mobile — Expo + NativeWind
Same WS protocol. NativeWind so design tokens and shadcn primitives carry from web to phone.
Adds- Expo SDK 51 + expo-router
- NativeWind 4 (Tailwind for RN) + react-native-reusables (shadcn-port)
- expo-notifications for push on
agent_end - Background mode keeps WS alive for long-running spawns
- Reuses v1.5 WS client + protocol types verbatim
- iOS + Android dev builds chat with the same Brigade core
- Push notification fires when an agent finishes while app is backgrounded
Memory upgrade — SQLite-FTS5 + decay + embeddings
When v1 keyword grep starts to feel coarse, upgrade the memory backend. Hermes / Boop hybrid.
Adds- SQLite (better-sqlite3 or libsql) backend with FTS5 lexical search (Hermes pattern)
- Optional embeddings via
@ai-sdk/openaior Voyage - Adaptive exponential decay (Boop's
memory/clean.tspattern) - Post-turn extraction → auto-write durable facts
supersedespointers for memory corrections
- Recall returns top-N by hybrid score (FTS5 + embedding cosine)
- Decay job runs every 6h, archives stale facts, prunes pointless ones
Production hardening — approvals · budgets · audit · evals
The "trust this with real work" milestone. Multi-user-ready.
Adds- Approval flow for dangerous tools (write/bash/spawn_agent) — UI prompt + auto-approve allowlist
- Per-session token + dollar budgets with hard stop
- Audit log of every tool call and decision (Paperclip pattern)
- Eval harness — regression suite that runs on every commit
- Better Auth (matching nodebase) for multi-user; Polar for billing if external
- Structured logs via pino, OTEL traces optional
- You can hand a stranger a Brigade login without losing sleep
- Eval suite has 20+ scenarios, all passing
Sandbox — per-session Docker container
Bash and write tools route through Docker via Pi's operations hook. OpenClaw's pattern, lighter weight.
createBashTool({ operations: { exec: dockerExec } })— Pi gives us this hook- Per-session container; workspace mounted RO + outbox RW
- Credential proxy (NanoClaw-style) — agent never sees raw API keys
- Timeout + memory caps per container
- Brigade can run an untrusted prompt without risk to host
- Container boot < 2s; tool call latency < 100ms over IPC
Multi-channel + scheduler
Brigade leaves your terminal. NanoClaw-style adapters + Paperclip-style routines.
Adds- Channel adapter interface + barrel-import registry (NanoClaw pattern)
- Telegram (telegraf), Discord (discord.js), Slack (@slack/web-api)
- Two-DB-per-session SQLite (NanoClaw inbound/outbound) for true isolation
- Cron / heartbeat scheduler (croner) — recurring jobs that wake the agent
- Slash commands per channel ("/scan inbox", "/plan tomorrow")
- "DM me the sales summary every weekday at 9am" works end-to-end on Slack/Telegram
- Adding a 4th channel takes < 4 hours of work
ACP server — IDE integration
Brigade speaks Agent Client Protocol → shows up in any ACP-compatible IDE. Same wire OpenClaw and Hermes use.
Adds@agentclientprotocol/sdkwrapper- Translator: Pi events ↔ ACP events (lift OpenClaw
src/acp/translator.ts) - ToolKind mapping: read · edit · execute · search · fetch · think · other
- Stdio agent server — IDE spawns Brigade as a subprocess
- Registry entry for Zed / JetBrains / Cursor / Cline
- Brigade appears in Zed's agent picker and answers prompts
- Tool calls render as ACP tool tiles in the IDE
Self-improving learning loop (Hermes-inspired)
Long-game endgame. The agent that grows with you. Speculative until v3 evals exist.
Adds- Skill self-authoring — agent proposes new SKILL.md candidates from successful turns
- Honcho-style dialectic user model (stores beliefs about you, evolves)
- Optional trajectory export for RL fine-tuning (Atropos-flavored)
- "Refine" pass on existing skills based on usage outcomes
- Personal model fine-tune cadence (monthly?)
- Brigade after 3 months feels measurably more "yours" than Brigade on day 1
- Eval scores trend upward without manual prompt tuning
Each grade builds on the last. Skipping ahead breaks compounding.
Approvals on a flaky core = double-broken. Fix v1 bugs before adding v3 features.
A multi-channel agent without approvals is one prompt-injection away from disaster.
The learning loop only makes sense once v3 evals exist to measure improvement. Don't let it gate practical milestones.
Forecast from current builds. Shows how each grade compounds. By v5 you've built ~10× v1's surface — but in 9 months of ~weekly increments, not a death march.
What each repo taught us
| Repo | Single best lesson | What Brigade takes |
|---|---|---|
| Pi-mono | The agent loop is ~80 lines if you let an SDK handle providers. | Engine itself |
| OpenClaw | Layered prompt files with explicit cache boundary = 10× cost win. | Prompt assembly + skill eligibility |
| Boop | Dispatcher / executor split is the cleanest sub-agent pattern in production. | Sub-agent + memory shape + WS broadcast |
| NanoClaw | Two-DB-per-session + barrel imports = 14k LOC for a multi-channel platform. | Channel adapter pattern (deferred two-DB to v1.5) |
| Hermes | Self-improving learning loop is the long-game endgame. | Memory tool design leaves room for future skill self-edit |
| Paperclip | Heartbeat-driven scheduling > push-driven for autonomous work. | (Deferred — v1 is interactive, no autonomy) |
The feasibility audit — receipts for every claim
"5.5 hours · ~1,200 LOC · OpenClaw-grade engine" sounds like a pitch deck. It isn't. Each claim below has direct evidence in the local source.
Pi SDK does the hard 80%.
The agent loop is ~80 LOC inside pi-mono/packages/agent/src/agent-loop.ts at runLoop(). We don't write the loop, the abort plumbing, the multi-provider streaming, or the JSONL session format. Brigade is a thin bezel around an SDK that's already in production behind OpenClaw.
Every pattern is lifted from production code.
Boop's WS broadcast = 17 LOC running for months (boop-agent/server/broadcast.ts). OpenClaw's prompt-cache boundary = battle-tested at scale. Boop's dispatcher/executor pattern = the recommended Anthropic agent topology. We're not inventing protocols.
Each primitive is < 250 LOC.
Memory store: ~80. WS gateway: ~50. spawn_agent: ~60. Auto-recall hook: ~40. Prompt assembler: ~80. The TUI is the largest single piece (~250) and most of that is event-handler glue around Pi-TUI components. Nothing is "big" by any normal definition.
Tool surface fits in your head.
8 tools. All identical shape: TypeBox schema → typed params → returns {content, details}. AJV validates before execute() runs. You can describe the entire tool model on a napkin and write a new tool in 15 minutes.
Zero infrastructure to set up.
No database. No cloud. No auth provider. No Docker. pnpm install && pnpm dev and you have a server. Sessions are JSONL on disk. Memory is JSONL on disk. Skills are markdown files. The whole thing runs from your home directory.
All references are in the same workspace.
Pi-mono, OpenClaw, Boop, NanoClaw, Hermes — every reference repo is on disk. Copy + adapt is 3-5× faster than write from scratch. When stuck, grep the neighbours. There's no Stack Overflow tax on this build.
Pair-programming with Claude collapses the time.
Verified empirically across this whole conversation: research that took hours solo took minutes with subagents. Boilerplate (TS types, package.json, glue code) compresses 5-10×. ACP translator equivalents take a day instead of a week.
The failure mode is small.
Worst case: a v1 feature is buggy or the TUI feels off. v0 (just the loop + Anthropic) gives you a working agent in 25 minutes. Even if v1's auto-recall is broken, you've already shipped a usable terminal agent. The downside has a floor.
Forecasting failure honestly. None of these block v1. Each has a mitigation.
If TUI disconnects mid-stream, what happens to in-flight tool calls? Mitigation: v1 ignores reconnects (single-client assumption). Add reconnect in v1.5 when it matters.
Keyword recall might inject too many false positives, polluting context. Mitigation: Cap to top-3 hits with min-score gate; add a /forget slash command to debug.
Naive spawn_agent could chain spawns into infinite cost. Mitigation: Hard depth-cap of 1 in v1 (sub-agents can't spawn). Lift to 3 with budget tracking in v1.5.
Verdict — go.
Brigade v1 is a 5.5-hour pair-programming sprint that produces a real working agent. The risks are bounded, the references are on disk, and the engine you'd otherwise have to build is already in node_modules/@mariozechner/. The only legitimate reason not to start today is "I want to keep researching" — but past a certain point, more research is procrastination wearing a lab coat.
Ready to build
Path is locked. Spec is verified. Every snippet on this page is traceable to a live file in the workspace.
Brigade v1 — what to do next
Open Claude Code in this workspace. Say "go" — the scaffold begins in the next message. Estimated time to working v1: ~5.5 hours of pair programming.
$ cd c:\Users\SmartSystems\Downloads\
$ mkdir brigade && cd brigade
$ "go" # in the Claude Code conversation