brainfile
Markdown task boards for you and your coding agents. Every task, epic, plan and decision is a plain Markdown file in your repo; you work the board from a CLI or a TUI, and your agents work it through an MCP server.
For people, it replaces the board in a browser tab with files in the repo, worked from a CLI or a TUI. For agents, it is a board they can read and work through an MCP server, with contracts that say what done means.
$ npm install -g brainfile
$ brainfile init
Install brainfile and set up a board in this repo for me.
1. Run: npm install -g brainfile
2. Run `brainfile init` at the repo root.
3. Add the MCP server to .mcp.json:
{"mcpServers":{"brainfile":{"command":"npx","args":["brainfile","mcp"]}}}
4. Run `brainfile brief --agent <your name>` and tell me what the board says.
From here on, track work on the board: `brainfile add` before you start,
`brainfile note` as you learn things, `brainfile complete` when you are done.
Overview
A board is a .brainfile/ directory next to your code. Its config is one file,
each active document is one file, and completed work goes to a log. That is the
whole data model.
.brainfile/
├── brainfile.md # board config: columns, agent instructions, document types
├── board/ # active documents, one file each
│ ├── task-1.md
│ └── epic-1.md
└── logs/ # completion history
├── ledger.jsonl # append-only completion record
└── task-2.md # archived documents
The CLI edits those files for you, and so does the MCP server — an agent with
brainfile mcp attached can do everything the CLI does to a board: list it,
add and move tasks, leave notes, change the columns, pick up a contract and
mark it delivered, with the same rules a person gets. Because a
task is a file, git blame, pull requests and code review already work on it.
Contracts are the part built for agents: a task can carry deliverables, validation commands and constraints. An agent picks the contract up, does the work, delivers, and validation runs the commands and checks the files. Nothing is done until it passes.
Install
brainfile is an npm package; it needs Node 20 or newer. npm install -g brainfile puts the command on your PATH, and brainfile init creates the
board. The other routes are below.
npx no global install
$ npx brainfile init
$ npx brainfile list
Every command works through npx. The MCP configuration above uses it so the
server tracks whatever version npm resolves, and nothing has to be on the PATH.
Upgrading from a single brainfile.md the v1 layout
$ brainfile migrate
$ brainfile migrate --logs-to-ledger # also backfill logs/*.md into ledger.jsonl
Older boards kept every task inside one brainfile.md. migrate splits that
into .brainfile/brainfile.md plus one file per task, keeps backups, and
refuses to overwrite its own outputs unless you pass --force.
chain the rest of the toolkit
brainfile is one of the chain.sh tools. One command installs the set:
$ curl -fsSL https://chain.sh/bootstrap.sh | sh
Quickstart
The session below is real output from 0.20.0, run in an empty directory.
$ brainfile init
Brainfile initialized successfully!
Created: /tmp/demo/.brainfile/brainfile.md
Created: /tmp/demo/.brainfile/board/
Created: /tmp/demo/.brainfile/logs/
$ brainfile add -t "Swap the parser cache" -p high --tags perf --files core/parser.ts
Task added successfully!
ID: task-1
Title: Swap the parser cache
Column: To Do
Priority: high
Tags: perf
Files: 1 linked
$ brainfile add -t "Write the migration note" --assignee claude --with-contract --ready --deliverable "docs:MIGRATION.md:Notes for 0.20" --validation "npm test"
Task added successfully!
ID: task-2
Title: Write the migration note
Column: To Do
Assignee: claude
$ brainfile move -t task-1 -c in-progress
Task moved successfully!
Task: task-1 - Swap the parser cache
From: To Do
To: In Progress
$ brainfile note -t task-1 "Bounded by bytes, not entries"
Log entry added.
Task: task-1
- 2026-09-20T19:03:56.862Z: Bounded by bytes, not entries
$ brainfile complete -t task-1
Task completed!
Task: task-1 - Swap the parser cache
CompletedAt: 2026-09-20T19:03:57.055Z
Moved to: logs/task-1.md
The second task carried a contract, so an agent can claim it. pickup moves
the task to the in-progress column, marks the contract in progress and prints
the brief the agent works from:
$ brainfile contract pickup -t task-2
# Contract pickup: task-2
## Task
- **ID**: task-2
- **Title**: Write the migration note
- **Column**: in-progress
- **Contract status**: in_progress
## Deliverables
- `docs` `MIGRATION.md` — Notes for 0.20
## Constraints
- (none)
## Relevant files
- (none)
## Validation
- `npm test`
After those six commands the directory looks like this. Nothing here is generated or opaque; open any of it in an editor.
$ find .brainfile -type f | LC_ALL=C sort
.brainfile/.gitignore
.brainfile/board/task-2.md
.brainfile/brainfile.md
.brainfile/logs/ledger.jsonl
.brainfile/logs/task-1.md
The board on disk
Three kinds of file, all Markdown with YAML frontmatter, all meant to be read and edited by hand when that is quicker than a command.
brainfile.md — the config
init writes .brainfile/brainfile.md. It holds configuration only: the columns, the standing
instructions an agent should read, and any document types of your own. Tasks
never live in it, and neither do the built-in types: epic, spec, plan, task and
decision exist without being listed (see Document types).
---
schema: https://brainfile.md/v2/board.json
title: My Project
agent:
instructions:
- Task files are individual .md files in board/
- Completed tasks are in logs/
- Preserve all IDs
- Make minimal changes
columns:
- id: todo
title: To Do
- id: in-progress
title: In Progress
---
# My Project
Add your project description here.
> Note: Completing a task moves it to `logs/` via `brainfile complete`.
agent.instructions is what brainfile brief shows an agent on its first
visit, alongside the decisions that stand. Put the guidance you would
otherwise repeat in every prompt there.
brainfile columns adds, renames, reorders and removes columns. Over MCP the
columns tool does the same, and board sets the title, the instructions
and related:, so an agent never edits this file by hand.
board/ — one document per file
The file name is the ID. Everything the board knows about a document is in the
frontmatter; the body is free Markdown for the description and notes. This is
board/task-2.md, the task the quickstart handed to an agent, after pickup:
---
id: task-2
title: Write the migration note
column: in-progress
position: 1
assignee: claude
contract:
status: in_progress
deliverables:
- type: docs
path: MIGRATION.md
description: Notes for 0.20
validation:
commands:
- npm test
metrics:
readyAt: "2026-09-20T19:03:56.451Z"
pickedUpAt: "2026-09-20T19:03:57.257Z"
reworkCount: 0
createdAt: "2026-09-20T19:03:56.453Z"
updatedAt: "2026-09-20T19:03:57.257Z"
---
| Field | Meaning |
|---|---|
| id | Unique across the board. The prefix comes from the type: task-, epic-, spec-, plan-, decision-. Never regenerate one. |
| type | task (default), epic, spec, plan or decision, plus any type you define. issue and adr are accepted and written as task and decision. |
title, column, position | Where the card sits. column is a column id from the config. |
| priority | low, medium, high or critical. |
tags, assignee, dueDate | Free-form. assignee is how brief and the TUI's @name filter find an agent's work. |
| parentId | The one parent: a spec's epic, a plan's spec, a task's plan. list --tree draws it; list --parent follows it. |
| dependsOn | Tasks that come first. mobile:task-12 names a task on a related board. See Related boards. |
| decisions | The decisions this work rests on, e.g. [decision-3]. The reverse is worked out, never written on the decision. |
| relatedFiles | Paths an agent should read first. |
| subtasks | { id, title, completed } rows, managed with subtask. |
| contract | Deliverables, validation and constraints. See Contracts. |
logs/ — what got done
complete does two things: it appends one JSON line to ledger.jsonl and moves
the task file to logs/. The ledger is the record; the file is the archive.
log, search and the MCP search tool read both. After the quickstart,
logs/ledger.jsonl holds one line:
{"id":"task-1","type":"task","title":"Swap the parser cache","filesChanged":["core/parser.ts"],"createdAt":"2026-09-20T19:03:56.245Z","completedAt":"2026-09-20T19:03:57.055Z","cycleTimeHours":0,"summary":"- 2026-09-20T19:03:56.862Z: Bounded by bytes, not entries","priority":"high","tags":["perf"],"relatedFiles":["core/parser.ts"]}
Notes added with note become the summary, so the ledger reads as a
changelog written while the work happened rather than after.
A decision that is superseded, rejected or stale also moves to logs/, with
its status and links, but it writes no ledger line: it was never completed.
state/ — local to this machine
.brainfile/state/ is ignored by git and never synced. It holds each agent's
brief checkpoint (<agent>.json) and the local search index (index.db):
SQLite with full-text search over the board, the ledger, plans/ and the
project's own Markdown. Delete it at any time; it is rebuilt from the files.
Document types
Every document is a file with frontmatter, so the board is a graph and not only a list. Five types are built in, each with a place in it:
| Type | Glyph | What it is | Goes under | Holds |
|---|---|---|---|---|
| epic | ▸ | An outcome worth several pieces of work; done when everything under it is. | top level | spec, plan, task |
| spec | ◆ | What to build and why: behaviour, scope, what is not in scope. | epic | plan, task |
| plan | ⎘ | How, and in what order; done when its tasks are. | epic, spec | task |
| task | One piece of work a person or an agent can pick up. Alias issue. | epic, spec, plan, task | task | |
| decision | ● | A standing choice. Alias adr. Not work: no column, never completed; current until superseded, rejected or stale. | never in the tree | nothing |
parentId is the one parent link and the table says which parents fit. A
parent cannot complete while it has open children (complete --force
overrides). An off-pattern parent, an epic under a task or anything under a
decision, is a lint warning; with strict: true on the board, add,
patch and lint refuse it. A loop is always refused.
The other edges are few and named: dependsOn orders work; decisions: on a
piece of work names the decisions it rests on, and when one is superseded,
open work resting on it says so in show and list; supersedes and
supersededBy link decisions to each other.
issue and adr are accepted wherever a type is (add --type, list --type,
search, the MCP tools) and new documents are written as task and
decision. Existing type: adr files read as decisions and keep their IDs.
Decisions
A decision belongs to a claim family through its dotted claimKey
(dependency.http-client). A family has one current decision: decide
records a new one and supersedes the old, which gets status: superseded
and supersededBy, moves to logs/, and is pointed at by the new one's
supersedes. A current decision lives in board/; a superseded, rejected or
stale one in logs/. The body is the decision and why.
---
id: decision-3
title: Use undici for HTTP, never axios
type: decision
status: current
claimKey: dependency.http-client
kind: constraint
stance: forbids
subject: axios
holdsWhile: we target Node 20 and later
recheck: when fetch gains retries
validUntil: 2027-06-30
packageVersion: undici>=6
supersedes: decision-1
---
| Field | Meaning |
|---|---|
| claimKey | The family. Dotted segments of lowercase letters, digits, _ and -. |
| kind | How firmly it binds: decision (default), preference, constraint, rule. |
| status | current, superseded, rejected or stale. None means current. |
stance, subject | What it says about something: prefers, rejects, requires or forbids a package, a pattern, a tool. check reads them. |
holdsWhile, recheck | The condition it holds under, and when or why to look again. |
| validUntil | After this date, check says review. |
sourcePath, sourceHash | A file it rests on and its sha256; a change makes check say review. |
| packageVersion | A version it rests on, name<op>version: zod>=4.3, engines.node>=20, go>=1.22. Read from package.json, go.mod or pyproject.toml. |
supersedes, supersededBy | The links between a family's decisions. |
Name your own under types: in the config, or with brainfile types add. A
type can take a built-in's place with like:: it gets that type's parents,
children, completion rule and glyph. One with no like behaves as a task.
types:
bug: { like: task }
rfc: { like: spec, idPrefix: rfc }
An entry under a built-in's own name overrides it (plan: { completable: false } stops a plan completing when it reaches a completion column). With
strict: true, a document's type must be built in or listed.
brainfile types prints every type with its place in the graph.
The JSON Schemas the frontmatter points at are served from this domain and
never move: /v2/board.json, /v2/task.json,
/v2/contract.json, /v2/epic.json,
/v2/decision.json and /v2/adr.json, with
/v2/index.json as the directory. brainfile schema board prints the bundled copy without a network.
Choosing a command
| The question | Use |
|---|---|
| Start a board in this repo | init |
| See the board and work it by keyboard | tui |
| Create a task | add |
| Advance a task to another column | move |
| Add, rename, reorder or remove a column | columns |
| Change a task's fields | patch |
| Finish a task and record it | complete |
| Read the whole board | list |
| See epics, specs, plans and tasks as one outline | list --tree |
| Read one task in full | show |
| See what this board waits on in other repos | list --related |
| Find something, active or done, on the board or in the project's docs | search |
| Write down what you learned while working | note |
| Break a task into steps | subtask |
| Hand work to an agent, with deliverables and validation | contract |
| Orient an agent at the start of a session | brief |
| Record a decision, or replace one | decide, supersede, retire |
| Ask whether a decision still holds before acting | check |
| Let an agent work the board as tools | mcp |
| Keep an agent updating the board | hooks |
| Read what was done on a task | log |
| Record a plan | plan |
| See every type and where it fits | types |
| Export finished work to GitHub or Linear, or bring it back | archive, restore |
| Share the board with another machine or person | sync |
| See where the board lives and who has it | where |
| Upgrade a v1 board, or move a board onto its own branch | migrate |
Every command that touches a task takes -t <id>. Every command finds the
board by walking up from the current directory to the repository root,
preferring .brainfile/brainfile.md; inside a git repository it then looks
for the worktree on the board branch and checks it out if the branch exists
but has no directory yet (see Sharing a board). Legacy
names brainfile.md, .brainfile.md and .bb.md still resolve. Pass
-f <path> to point at another board, or -g for the home board in
~/.brainfile. Custom types, task templates, linting, the schemas and the
config file are in Commands.
Commands
Thirty-six commands, in four groups. -t <id> names the task and
-f <path> the board; everything else is per-command, and every flag is as
printed by brainfile <command> --help. Expand a row for its flags. Commands
that accept --json say so; list does not have one yet, so scripts and
agents should use show --json, brief --json or the MCP tools instead.
The board
init Create .brainfile/ in the current directory
$ brainfile init
$ brainfile init --force # overwrite an existing config
$ brainfile init --plain # a plain directory, even inside a git repository
$ brainfile init --here # inside a repository: this directory, not its root
$ brainfile init -g # the home board at ~/.brainfile
Writes a short brainfile.md with two columns and the default agent
instructions (no types:: the five built-in types need none), creates board/
and logs/, and adds a .gitignore that excludes state/, where per-agent
brief state lives.
Inside a git repository the board is created at the repository root as a
worktree on its own brainfile branch, hidden from the code branch through
.git/info/exclude; outside one, --tracked makes it its own repository.
Either way every change becomes a commit. --plain opts out. See
Sharing a board.
list List tasks, optionally filtered
$ brainfile list
$ brainfile list -c todo
$ brainfile list -t urgent # by tag
$ brainfile list --parent epic-1
$ brainfile list --type spec # aliases work: issue is task, adr is decision
$ brainfile list --tree # the outline: epics, specs, plans, tasks
$ brainfile list --tree --parent epic-1
$ brainfile list --type decision # decisions, current first
$ brainfile list --contract ready # ready | in_progress | delivered | done | failed
$ brainfile list --related # open tasks on related boards this board waits on
$ brainfile list --related all # every open task on every related board
Prints every column with its tasks, priorities and tags. A task waiting on a
related board says so on a Blocked by: line, and one resting on a superseded
decision says that too. Decisions have no column, so list leaves them out
and says how many; --type decision lists them with where each stands.
--tree prints open work as an outline, nested by parentId in column order:
$ brainfile list --tree
My Project — outline
▸ epic-1 Memory and the graph · In Progress
◆ spec-1 Default types · To Do
⎘ plan-1 Build order · To Do
· task-1 Registry · To Do
· task-3 Index work · To Do ⚠ rests on superseded decision-2
With a filter it shows the matches and keeps their ancestors, dimmed, for
context; --parent shows one document and everything under it.
show Full details of one task
$ brainfile show -t task-2
$ brainfile show -t task-2 --json
$ brainfile show -t mobile:task-12 # a task on a related board
--json prints the document's frontmatter as one object, timestamps included.
show names the document's type, its parent, its children (open or done),
each dependency and whether it is done, and the decisions it rests on with
where each stands; if one is superseded it says "Rests on a superseded
decision". A decision shows its status and the work resting on it. --json
adds the same as graph.
add Create a document
$ brainfile add -t "Fix login bug" -p high --tags bug,auth --files src/auth.ts
$ brainfile add -t "Auth epic" --type epic --child "OAuth flow" --child "Session hardening"
$ brainfile add -t "Token refresh" --type spec --parent epic-1
$ brainfile add -t "Use SQLite for the index" --type decision
$ brainfile add -t "Build the index" --parent spec-1 --decisions decision-1
| Flag | Meaning |
|---|---|
| -t, --title | Required. |
| -c, --column | Defaults to todo. |
| -d, --description | Written to the Markdown body. |
| -p, --priority | low, medium, high, critical. |
--tags, --subtasks, --files | Comma-separated. |
--assignee, --due-date | A name; a YYYY-MM-DD date. |
| --type | epic, spec, plan, task, decision or a type of yours; issue and adr are aliases. Sets the ID prefix. A decision gets no column. |
| --parent | Parent ID; it must fit the type (see Document types). --child <title> creates children under the new document, repeatable. |
--depends-on, --decisions | Comma-separated IDs: tasks that come first, decisions the work rests on. |
| --with-contract | Attach a draft contract. Add --ready to make it dispatchable at once. |
--deliverable, --validation, --constraint | Contract parts, each repeatable. See Contracts. |
move Change a task's column
$ brainfile move -t task-1 -c in-progress
Column names and ids both work.
patch Partial update of a task's fields
$ brainfile patch -t task-1 -p critical --assignee codex
$ brainfile patch -t task-1 --tags perf,cache # replaces the tag list
$ brainfile patch -t task-1 --clear-due-date
$ brainfile patch -t spec-3 --parent epic-2
$ brainfile patch -t task-7 --depends-on task-5,task-6 --decisions decision-2
Takes --title, -d, -p, --tags, --assignee, --due-date, --parent,
--depends-on and --decisions, plus --clear-tags, --clear-assignee,
--clear-due-date, --clear-priority, --clear-parent, --clear-depends-on
and --clear-decisions. -p none also removes the priority. A new parent
must fit the type, as for add, and neither link may loop.
complete Finish a task
$ brainfile complete -t task-1
$ brainfile complete -t epic-1 --force # even if children are still active
Appends to logs/ledger.jsonl, moves the file to logs/, and records cycle
time from createdAt. A parent with open children refuses, whatever its
type, until they are done or you pass --force; a decision is never
completed.
delete Remove a task permanently
$ brainfile delete -t task-3 --force
--force is required; there is no prompt and no undo. Prefer complete.
subtask Add, toggle, update or delete subtasks
$ brainfile subtask -t task-1 --add "Write tests"
$ brainfile subtask -t task-1 --toggle sub-1
$ brainfile subtask -t task-1 --update sub-1 --title "Write unit tests"
$ brainfile subtask -t task-1 --delete sub-1
search Search the board, its history and the project's docs
$ brainfile search parser
$ brainfile search parser -c in-progress
$ brainfile search cache --type adr # decisions, under either name
$ brainfile search "p:high #auth token" # filter tokens: p: #tag @user type: due: contract:
$ brainfile search parser --no-history --json
Current results come first: open work, current decisions, plans/ and the
project's own Markdown (README, AGENTS.md, docs/…). History comes after, in
its own lane: completed work, the ledger, and decisions that were superseded,
rejected or let go stale. Every hit has its path and line range; a board
document also has its ID, a file hit's ID is path:start-end.
On Node 22.13 and later the local index answers (.brainfile/state/index.db,
brought up to date before every search): words are matched all together
first and any of them second, ranked with titles over bodies. On older Node,
or with BRAINFILE_INDEX=off, the scan over board/ and logs/ answers in
the same shape; nothing else changes. A query that is only filter tokens
(p:high) always scans. --limit caps each lane (default 20), --json
prints { mode, query, results, count } with each hit's lane, source,
path, startLine and endLine.
note Append a timestamped line to a task's log
$ brainfile note -t task-1 "Bounded by bytes, not entries"
$ brainfile note -t task-1 --agent codex "Tests pass on the new cache"
Each note is tagged with who wrote it: --agent when given, otherwise the
agent running the command if brainfile can tell (see Who did
what), otherwise your git user.name. Notes stay with the task
and become the ledger summary when it completes.
log Read completed work
$ brainfile log --recent
$ brainfile log -t task-1
$ brainfile log -s "cache"
tui The interactive board
$ brainfile tui
Columns, decisions, detail view, filters and every write operation from the keyboard. See The TUI.
Agents
brief Decisions, your tasks, what changed, the docs that bear on them
$ brainfile brief --agent claude
$ brainfile brief --agent claude --max-chars 0 # no budget
$ brainfile brief --agent claude --peek # read without marking seen
$ brainfile brief --agent claude --json
$ brainfile brief --agent claude --offline # skip the sync a shared board does first
One block, about 1,600 characters (--max-chars), in lanes:
Brainfile brief for claude · My Project (since 3h ago)
! task-12 Build the index: rests on superseded decision-1 (now decision-3)
## Current decisions
- decision-3 dependency.http-client: Use undici for HTTP, never axios (constraint · forbids axios)
## Your tasks
- task-12 Build the index · In Progress · high
## Since your last brief
- task-12 note: [codex] wired the FTS table (2h ago)
- decision-3 decided: Use undici for HTTP, never axios (supersedes decision-1) (3h ago)
## Docs that bear on them
- docs/index.md:12-40 Search index › Design
Lines starting with ! come first: open work resting on a superseded
decision, and current decisions whose validUntil, source file or package
version no longer holds. Then the current decisions; the agent's tasks (or,
when nothing is assigned to it, the work in flight); what changed since its
last brief (notes, task changes, completions, decisions made or retired,
config), or on a first brief the latest notes and completions; and, from the
local index, the docs that share the tasks' most distinctive words. When the
budget is tight every lane still gets its first line, shortened if need be,
before any lane gets a second, and a last line says how many more there are.
The checkpoint is per agent, kept in .brainfile/state/ and ignored by git;
--peek reads without moving it. --json carries the block as text, its
lanes as sections, and the checkpoint's own lanes as before. A shared
board syncs before the brief so it reflects other machines; a sync failure is
one warning line, never an error. The SessionStart hook prints this block.
contract pickup, deliver, validate, attach, graph, activate
$ brainfile contract pickup -t task-2
$ brainfile contract deliver -t task-2
$ brainfile contract validate -t task-2
$ brainfile contract attach -t task-5 --ready --deliverable "file:src/x.ts" --validation "npm test"
$ brainfile contract activate --parent epic-1 # every draft under the epic → ready
$ brainfile contract graph --show
The whole lifecycle is in Contracts.
mcp Start the MCP server
$ brainfile mcp
Speaks MCP over stdio; your agent launches it, you never run it by hand. It
tells the agent to use its tools for every board change and never to edit
.brainfile/ by hand. Tools and setup are in For agents.
hooks Install reminder hooks into a coding agent
$ brainfile hooks install claude-code
$ brainfile hooks install cursor --scope project
$ brainfile hooks list
$ brainfile hooks uninstall cline --scope all
Supports claude-code, cursor and cline, at user (default) or project
scope. The hooks nudge the agent to update the board after it edits files, and
when a session starts they hand it its brief: for Claude Code as
hookSpecificOutput.additionalContext, for Cline as text. The session-start
hook runs from a lean entry that loads the brief and nothing else, about
150ms warm; it names the agent from BRAINFILE_AGENT, else the tool
(claude, cline), and moves that agent's brief checkpoint.
BRAINFILE_BRIEF_MAX_CHARS changes its budget.
Structure
plan First-class plan documents
$ brainfile plan add -t "Thin-frontend refactor" --status draft
$ brainfile plan list --status active
$ brainfile plan show plan-1 --json
$ brainfile plan link plan-1 -t task-42
add takes -c, --description, --tags, --parent (an epic or a spec)
and a free-form --status. link sets the task's parentId to the plan, so
list --parent plan-1 finds a plan's tasks. plan is a built-in type, so a
strict board needs no types: entry for it.
decide, supersede, retire Decisions
$ brainfile decide -k dependency.http-client "Use undici for HTTP. It ships with Node."
$ brainfile decide -k dependency.http-client --kind constraint \
--stance forbids --subject axios "Never axios: one HTTP client"
$ brainfile decide -k release.window --valid-until 2026-12-31 \
--recheck "Ask the release manager" "Release on Tuesdays"
$ brainfile decide -k storage.index --source docs/index.md "Index with SQLite FTS5"
$ brainfile decide -k runtime.node --package "engines.node>=20" --for task-12 "Support Node 20+"
$ brainfile supersede decision-3 "Use fetch: it retries now"
$ brainfile retire decision-4 --as rejected --note "Too costly to run"
decide needs -k/--claim-key and the text (- reads stdin; the first line
is the title unless --title). When the family already has a current
decision, that one is superseded and moved to logs/; when the family was
empty, it lists related families (the same parent key, or the same words) in
case the call already exists under another key. Saying again exactly what the
family's current decision says changes nothing. --no-supersede leaves both
current, and check will say review. --source records the file's sha256
relative to the project root; --package a version predicate; --for adds
the decision to the listed work's decisions:.
supersede <id> replaces one decision by hand, keeping its claim key and kind
unless you give others. retire <id> --as rejected|stale moves a decision to
logs/ with that status and an optional --note. Each command is one board
commit on a tracked board. list --type decision lists them all, current
first; show prints one with its chain.
$ brainfile add -t "Use SQLite for the index" --type decision
$ brainfile adr promote -t adr-1
ADRs are decisions: --type adr writes a decision, and an existing
type: adr file reads as one and keeps its ID. promote marks a legacy ADR
accepted and moves it to logs/. Current decisions and accepted ADRs appear
in every brief.
check Does a decision still hold?
$ brainfile check "use axios for the retry client"
$ brainfile check decision-3
$ brainfile check --claim-key dependency.http-client
$ brainfile check --claim-key-prefix dependency "add axios"
$ brainfile check "add axios" --json
check finds the claim family an action touches (a decision ID, a claim key,
or its words: the local index when there is one, a scan of the decisions when
there is not) and answers in this order:
| Verdict | When | Exit |
|---|---|---|
| review | The family has two current decisions; or the current one's validUntil passed, its source file changed or its package version no longer matches; or the action names the decision's subject next to an adopt or remove cue ("use axios", "drop axios") that goes against its stance (block_action for requires and forbids, ask_operator for prefers and rejects); or the family has no current decision. | 2 |
| use_replacement | The action matched a superseded decision; the current one is named. | 3 |
| use | A current decision stands and nothing above applies: act on it (the decision is printed; use means follow it, not "go ahead with anything"). | 0 |
| no_decision | Nothing on the board speaks to it. | 0 |
--claim-key-prefix checks every family under a prefix and gives the worst
verdict. Errors (no board, bad arguments) exit 1. --json prints the verdict,
recommendation, reason, the predicates as evaluated and the family.
columns Add, rename, reorder and remove columns
$ brainfile columns # list, with task counts
$ brainfile columns add "In Review" --after in-progress
$ brainfile columns add Shipped --completion
$ brainfile columns rename in-review Review
$ brainfile columns reorder backlog todo in-progress
$ brainfile columns remove review --move-to in-progress
Columns are named by ID or title. add makes the ID from the title
(In Review is in-review) unless --id gives one, and puts the column
last unless --after or --before says where; --completion marks a column
whose tasks count as done. rename changes the title only: the ID stays, so
the tasks in it stay. reorder puts the columns you name first, in that
order, and the rest after them. remove refuses a column that still holds
tasks unless --move-to names where they go; they move there first, without
being completed. The last column is never removed. Every command takes
--json, and each change is one board commit on a tracked board.
types Every type and its place in the graph
$ brainfile types
$ brainfile types list --json
$ brainfile types add bug --like task
$ brainfile types add rfc --like spec --id-prefix rfc --schema ./rfc.json
$ brainfile types
Strict mode: off
TYPE ID GOES UNDER HOLDS
▸ epic epic-N top level spec · plan · task
◆ spec spec-N epic plan · task
⎘ plan plan-N epic · spec task
task task-N epic · spec · plan · task task alias issue
● decision decision-N never in the tree — alias adr · no column, never completed
bug bug-N epic · spec · plan · task task like task
--like takes a built-in's place; without it a type behaves as a task. An
alias name (adr, issue) cannot be added as a type of its own.
template Create tasks from templates
$ brainfile template --list
$ brainfile template --use bug-report --title "Login times out" -c todo
Three templates ship: bug-report, feature-request and refactor, each with
a default priority, tags and a subtask checklist.
Maintenance
lint Validate the config, fix what it can
$ brainfile lint
$ brainfile lint --fix
$ brainfile lint --check # non-zero exit for CI
--fix also folds a legacy rules: block into agent.instructions. lint
also checks the graph: a parent that is missing, loops or does not fit the
type, decisions: that name nothing or something other than a decision, and
a like: that names no type. Off-pattern links are warnings, and errors on a
strict board. It also checks where the board lives: two boards that disagree
are an error; the board branch checked out away from .brainfile/, a checkout
whose folder is gone (--fix forgets it), and a code branch still tracking an
old copy are warnings.
index The local search index
$ brainfile index # bring it up to date and say what changed
$ brainfile index --rebuild # drop it and index from scratch
$ brainfile index --status # what is in it
Never required: every search, brief and check brings the index up to
date first. Unchanged files (same size and modification time) are not read; a
changed file replaces its chunks; a deleted one drops out. Inside git the
project's files come from git ls-files, so .gitignore is honoured; root
AGENTS.md and CLAUDE.md are read even when ignored. Markdown and text only,
256 KiB a file at most, never node_modules, dist and the like, never
.brainfile/ (the board is indexed on its own terms), never your home
folder. Where Node has no node:sqlite (below 22.13) it says so; search,
brief and check scan instead.
schema The bundled JSON Schemas
$ brainfile schema # list
$ brainfile schema board --json
$ brainfile schema update # check brainfile.md for a newer version
Bundled with the CLI, so validation works offline. Update checks run at most once a day and never block.
migrate Upgrade a v1 board, or change how it is stored
$ brainfile migrate --dir ./old-project
$ brainfile migrate --to-branch # move the board onto its own branch
$ brainfile migrate --to-plain # back to a plain directory
Flags: --dir, --force, --logs-to-ledger, --to-branch, --to-plain.
--to-branch keeps the board's history when it was committed on the code
branch (git subtree split), imports a gitignored one as a fresh branch, and
folds a standalone board repository into the surrounding repository. Board
changes not yet committed come along as one more commit on the branch. If any
step fails, the migration undoes itself and the repository is left as it was.
When the code branch tracked the board, one commit on it drops .brainfile/
and adds it to .gitignore (anything else you had staged stays staged); push
it like any commit. Run on a board already on its branch, it makes that commit
if the code branch still tracks an old copy. If a board already lives on the
brainfile branch, this folder and it are two boards, and --to-branch says
so and points to merge-boards. --to-plain leaves the branch intact.
where Where the board lives and who has it
$ brainfile where
$ brainfile where --fetch # ask the remote for its copy first
$ brainfile where --json
Prints the board's location, how it is stored (plain files, a branch of this
repository, or its own repository), the remote it is shared through, when it
last synced, how many changes are waiting to be sent and how many on the
remote are not here yet. It also looks for every other copy of the board in
the repository (see One board per repository) and
says when one disagrees with this one. It touches the network only with
--fetch, so it is always safe to run.
adopt, merge-boards, backups Settle two boards into one
$ brainfile adopt refs/brainfile/remotes/origin/board # take that board as it is
$ brainfile adopt .brainfile # keep this folder
$ brainfile merge-boards refs/heads/brainfile # combine them by document ID
$ brainfile backups # what was saved
$ brainfile backups restore 20261012T205217Z-folder # put one back
When a repository has two boards (see
One board per repository), these make it one.
adopt <ref|path> takes the named board as it is. merge-boards <ref|path>
combines this board with the named one by document ID, with the same rules
sync uses: frontmatter fields by updatedAt, logs unioned in time order,
completion over an edit. One ID on two different documents (created apart,
with unrelated titles) keeps the older; the newer gets the next free ID, a
log line saying so, and every reference to it on its board rewritten. When
one board already has everything the other has, merge-boards says so and
adopts it without asking.
Both end the same way: the board on the brainfile branch, checked out at
.brainfile/, descending from what the branch and the remote's copy had (so
the next sync is a plain push), with any other checkout of the branch
removed once its changes are committed. If the code branch still tracked
.brainfile/, one commit on it stops tracking the folder and adds it to
.gitignore; push that like any commit. Every board that is replaced is saved
first as refs/brainfile/backups/<UTC time>-folder or -branch; backups
lists them and backups restore <name> puts one back, backing up what it
replaces. Backups stay on this machine unless you push one by name.
sync Share a board through a git remote
$ brainfile sync # fetch, merge, push
$ brainfile sync --pull # one direction
$ brainfile sync --set-remote origin # the code remote
$ brainfile sync --set-remote git@host:me/boards.git --board-name myproject
$ brainfile sync --autosync full # off | push | full
Fetches the board, fast-forwards or merges with the board-aware merge driver,
and pushes it to refs/brainfile/<name> on the remote. Runs entirely inside the board directory; the code tree is
never touched. --set-remote in a repository that has no board yet checks the
board out from that remote. Details in Sharing a board.
archive, restore Export completed work, or bring it back
$ brainfile archive -t task-1 # same as complete
$ brainfile archive -t task-1 --to github
$ brainfile archive --all --to linear --dry-run
$ brainfile restore -t task-1 -c todo
Exports create an issue in the configured GitHub repo or Linear team; set the
destination with config set archive.github.owner, archive.github.repo or
archive.linear.teamId, and archive.default for the default target.
auth GitHub and Linear credentials
$ brainfile auth github # OAuth device flow
$ brainfile auth github --token ghp_…
$ brainfile auth linear --token lin_api_…
$ brainfile auth status
$ brainfile auth logout --all
config The user-level config file
$ brainfile config path # ~/.config/brainfile/config.json
$ brainfile config list
$ brainfile config set archive.default github
Holds archive destinations, auth and the schema update timestamp. Nothing about a specific board lives here.
Contracts
A contract is the part of a task an agent is accountable for: what files to
produce, what commands must pass, and what rules to respect. It lives under
contract: in the task's frontmatter and moves through a fixed set of states.
draft --activate--> ready --pickup--> in_progress --deliver--> delivered --validate--> done
^ |
+--------------- rework: set ready again <--- failed <---------+
| Status | Meaning |
|---|---|
| draft | Written but not dispatchable. add --with-contract starts here unless you pass --ready. |
| ready | Claimable. list --contract ready is the queue. |
| in_progress | An agent picked it up. The task moves to the in-progress column. |
| delivered | The agent says it is done. |
done / failed | The result of validate. To rework a failed one, add feedback and set status back to ready in the task file; the next pickup bumps reworkCount. |
| blocked | Waiting on something outside the board. |
Writing one
Deliverables are type:path:description, with the description optional and the
type one of file, test, docs, design or research. Validation commands
run from the repo root. Constraints are prose the agent reads at pickup.
$ brainfile add -t "Add rate limiting" --assignee codex -p high \
--with-contract --ready \
--deliverable "file:src/rateLimiter.ts:Token bucket implementation" \
--deliverable "test:src/__tests__/rateLimiter.test.ts:Unit tests" \
--validation "npm test -- rateLimiter" \
--validation "npm run build" \
--constraint "Non-blocking; no new dependencies"
contract attach adds one to a task that already exists, with the same flags.
contract graph attaches several at once with --depends-on between them, so
a research task, an implementation and its tests become a small dependency
graph; --show prints it.
Running one
The agent claims it, works from the pickup brief, and delivers. Then whoever owns the board validates: every deliverable path must exist and every validation command must exit zero.
$ brainfile contract pickup -t task-7 # → in_progress, prints the brief
$ brainfile contract deliver -t task-7 # → delivered
$ brainfile contract validate -t task-7 # → done, or failed with the reason
Each transition is recorded under contract.metrics: readyAt,
pickedUpAt, deliveredAt, duration and reworkCount sit on the task file
and go into the ledger when the task completes.
Draft contracts are how you plan a batch without dispatching it. Write them
under an epic, review the set, then contract activate --parent epic-1 turns
all of them ready in one step.
The TUI
brainfile tui opens the board in the terminal: columns across, a detail view
for the selected document, and a filter line. It edits the same files the CLI
does, so changes made elsewhere show up on r.
| Keys | Action |
|---|---|
j k ↑ ↓ | Move within a column. g and G jump to the ends, ctrl-d and ctrl-u page. |
h l ← → tab | Move between columns. |
| ↵ | Open the selected document. In the detail view, open a child, a decision the work rests on, or a decision's chain and the work resting on it. esc goes back. |
| t | Cycle the document type shown. L toggles the done view. |
| D | The decisions view, also the type cycle's decision stop. L there unfolds the history; D again goes back. |
| / | Filter. p:high, #tag or t:tag, @name, type:epic, contract:ready, due:overdue. |
a n | Add a task. N adds and opens it for editing. |
m c e | Move, complete, edit. |
| p | Set priority. In the detail view, jump to the parent. |
| space | Collapse a group. In the detail view, toggle a subtask. |
d y A | Delete, copy the ID, archive. |
r ? q | Reload, help, quit. |
Decisions have no column, so they are in none of the column lists, and a
retired one is not in the done view. The decisions view groups the standing
ones by family, the first segment of the claim key, with history (superseded,
rejected, stale) folded below. A row leads with ●, ○ once retired, and
says only what needs saying: a kind other than decision, proposed for a
legacy ADR, review once validUntil has passed, or where a retired one
went (superseded → decision-2). A decision's detail shows its claim key,
kind, stance and subject, holds while, recheck, valid until, what it
replaces or what replaced it, and the open work resting on it. Work's detail
lists the decisions it rests on under rests on, and its row says
decision changed once one of them is superseded, rejected or stale.
Decisions change through decide, supersede and retire: the TUI does
not move, complete or delete one, and e still opens its file.
Editing opens $EDITOR on the task file. Set NO_COLOR to drop colour.
For agents
The board is a directory of Markdown, so any agent that can read files can read it. Three things make it a working surface rather than a reference: the MCP server, the per-agent brief, and hooks.
The MCP server
Add this to .mcp.json at the repo root (or the equivalent for your agent) and
restart the agent. Twenty-five tools appear, one for everything the CLI does
to a board.
{
"mcpServers": {
"brainfile": {
"command": "npx",
"args": ["brainfile", "mcp"]
}
}
}
| Tool | Does |
|---|---|
| list_tasks | List board documents, filtered by column, tag, type (aliases work) or status. type: "decision" lists decisions; tree: true adds the outline; related adds related boards' open tasks. |
| get_task | One document by ID, in full, with graph: its parent, children and the decisions it rests on. Takes mobile:task-12 for a related board. |
| search | Search the board, its history and the project's docs (current first, history in its own lane, each hit with path and line range), list recent completions, or view one log entry. Takes type, limit, history. |
| brief | The agent's brief: text (budgeted by maxChars) and its sections, plus the checkpoint lanes. Takes agent=, peek. |
| check | use, review, use_replacement or no_decision for an action (query), a decision, a claimKey or a claimKeyPrefix, with exitCode as the CLI's. |
| task_add | Create a document. Takes type (aliases work), parentId, dependsOn, decisions, status, and contract parts for with_contract. |
| task_patch | Update fields on one task or many, parentId, dependsOn and decisions included; null removes a field. |
| task_move | Move one task or many to a column. |
| task_complete | Complete to the ledger, or export to GitHub or Linear. A parent with open children and a decision refuse. |
| task_delete | Remove a task. |
| subtask | action= add, toggle, delete or update, on one subtask, several, or all. |
| contract | action= attach, pickup, deliver, validate, graph or activate. |
| note | Append a line to a task's log, attributed as brainfile note does: agent, else the agent running the server, else your git user.name. |
| log | Completed work: one task's log (task), a search of it (query), or the most recent, ledger-only records included; limit. |
| decide | Record a decision in its claim family (claimKey, text, kind, stance, subject, holdsWhile, recheck, validUntil, source, package, forWork); the family's current decision is superseded. |
| supersede | Replace a decision by hand (taskId, text). |
| retire | Mark a decision rejected or stale (taskId, as, note); it moves to logs/. |
| adr_promote | Accept a legacy type: adr document and move it to logs/. |
| columns | action= list (with task counts), add (title, id, after or before, completion), rename (column, title), reorder (order) or remove (column, and moveTo when it holds tasks). |
| board | Read the title, agent instructions, related boards, columns and strict; set title, replace instructions or edit them with addInstructions and removeInstructions, and addRelated (alias, path) or removeRelated. With no changes it only reads. |
| types | Every type with its place in the graph; action=add with name, like, idPrefix, completable, schema. |
| template | action= list, or use (template, title, description, column). |
| lint | Check the board, the graph included; fix: true fixes what it can. Issues come back as data, with valid false only for errors. |
| sync | Fetch, merge and push a shared board now; pull or push for one direction. A plain or unshared board says so in skipped. |
| where | Where the board lives, how it is stored, the remote it is shared through, when it last synced and how many changes wait to be sent; every other copy of the board in the repository, and conflict when one disagrees (writes are refused until conflict.resolve runs). fetch: true asks the remote first. |
Every tool takes file to point at another board, and every one that writes
is one board commit on a tracked board. The bundled JSON Schemas are
resources: brainfile://schema/board and brainfile://schema/base, the
files brainfile schema prints.
When a client connects, the server tells it how to work: use these tools for
every board change, never create, edit or delete files under .brainfile/ by
hand, start with brief, leave a note as you go, check before acting on
something a decision covers; then a line on what each tool is for.
Some commands have no tool, on purpose: tui and mcp (they are the
interfaces), merge-driver (git runs it), hooks (events the agent host
fires, and your agent's settings), auth and config (your credentials and
user config), init and migrate (one-off, and your call), restore (the
v1 archive file; a v2 board keeps completed work in logs/) and index (a
cache that search, brief and check refresh). Choosing the remote a
board is shared through, and its autosync mode, also stays with you
(brainfile sync --set-remote, --autosync). A test holds the CLI and the
server to this: a new command fails it until it has a tool or a reason here.
Start every session with a brief
brainfile brief --agent <name> is the orientation call: the decisions that
stand, the agent's tasks, what changed since it last looked, and the docs that
bear on its work, in about 1,600 characters. With the hooks installed it
arrives on its own at session start. Have the agent use the same name it
gives as assignee, so its tasks show up under "Your tasks".
Decide, then check
When an agent settles something (a library, a pattern, a rule), it records it
with decide under a claim key, and the next session sees it in the brief.
Before an action that might cut across a settled choice, check "<action>"
names the decision that speaks to it and says use (follow it), review
(ask first), use_replacement (that one was replaced: use this) or
no_decision. Name the subject next to the verb ("use axios", "drop
axios") so a contradiction of the decision's stance is caught. Next steps are tasks and what happened is a note: a handoff
needs no other type.
Hooks that keep the board honest
$ brainfile hooks install claude-code
Installs three hooks into the agent's settings: after a file edit, a reminder
to update the task; before a prompt, a warning when files changed but the board
did not; at session start, the agent's brief. --scope project writes them
into the repo instead of the user's home.
Bootstrap files
Three files on this domain are written for agents rather than people: /llms.txt is the short index, /llms-full.txt is this manual as plain Markdown, and /llms-install.txt is the setup procedure.
Notes
One file per document, on purpose
Every task is its own file so that a task's history is its git history and a merge touches one small file at a time. The board's own branch is what lets two people, or two agents on different machines, edit at once: their commits merge field by field (see Sharing a board) instead of colliding on a feature branch. Keep IDs stable; the CLI never reuses one.
The config never holds tasks
brainfile.md is columns, instructions and types. If a legacy board still has
tasks or a rules: block in it, migrate and lint --fix move them where
they belong.
Ledger first, archive second
ledger.jsonl is the record search, log and brief read. The archived
task file in logs/ is kept for people. If they ever disagree, the ledger wins.
The index is a cache
.brainfile/state/index.db is built from the files on each machine and never
leaves it: the board branch carries the decisions and tasks, and every machine
indexes them itself. Nothing reads the index that could not be read from the
files, so deleting it costs one rebuild. It needs no native module: Node's own
node:sqlite, loaded only when present.
The library underneath
The CLI is built on @brainfile/core, which parses and writes the board format. Use it directly when a tool needs to read a board without shelling out.