Changelog
Every release of brainfile, newest first. Versions follow Semantic Versioning and match the git tags.
The canonical file is CHANGELOG.md in the repository; this page is rendered from it.
Unreleased
0.23.0 — 2026-10-16
Added
- Desktop: handing off an epic, or any task with open children, starts its
lead. The lead checks the plan first (the children, the files each touches,
whether the dependencies are right), writes it on the epic, then hands the
children off in order, woken as each ends or lands, inside rules the app
holds: dependencies done first, at most three hand-offs at once per
repository, retries to the contract's
maxRetries, and only the lead dispatches in its tree. Pause, Resume and Stop are in the epic's pane; a lead left by a restart resumes from its notes. brainfile dispatch policy [ask|bypass|clear] [--all]: the repository's permission policy indispatch.json(the data folder, shared with the window), which decides how far--until doneand an epic's lead go. Until now only the window's Settings wrote it, so from a terminal or an MCP client--until donealways stopped in Review. With no argument it prints the policy in force, where it comes from and where the file is. The person's own: refused for any agent's role, and no MCP tool.brainfile restorereopens a completed task on a v2 board: it comes back fromlogs/to a column, leaves the ledger, and its log says why (--reason). The MCP server has the same astask_restore.- A hand-off can land by pull request instead of a merge (decision-16):
brainfile pr <task>, the MCPpull_requesttool and the desktop's Open pull request push the task's branch, and only it, to origin and open a GitHub pull request into the branch it lands on, titled with the task's title. The task records it aspullRequest: { number, url, state, base, head, at }; while it is open the hand-off is out of Ready to merge (for the phone too) and Merge refuses it.brainfile pr <task> --status, and the desktop about once a minute, ask where it stands: merged, the task completes and its worktree and local branch go (a squash or rebase counts when the branch is what the pull request carried); closed without merging, it is back in Ready to merge. Core:openHandOffPullRequest,openBranchPullRequest(any head and base),followHandOffPullRequestandreleaseMergedHandOffin@brainfile/core/forge, andopenPullRequest,pullRequestStateandpullRequestForon its GitHub client, ETag and a minute's memory included. - Desktop: an open pull request shows as
#12on its row, in Review and under Open pull requests in the feed; an epic's Merge menu offers Open a pull request as its fourth choice. When the trunk has commits origin lacks (landing never pushes), Ready to merge and the feed saymain is 3 commits ahead of origin(core'sunpushed). - A pull request is refused when the branch it goes into has landings here
that origin's copy lacks (Merge lands locally and never pushes), since it
would carry them too: it names them and says to push that branch first,
or Merge instead. Once a pull request merges, the local branch it went
into follows origin's (a fast-forward, or onto a squash that has all it
had), unless it is checked out or has work origin's lacks; the task's log
says which. Core:
carriedUnpushedandfollowOrigin. - A task can land on a branch other than the trunk:
landsOn: <branch>on it, or on anything above it, is the branch its hand-off forks from and merges back into (decision-16).brainfile patch --lands-on/--clear-lands-onand the MCPtask_patchtool set and clear it, a name git would not take as a branch is refused, andbrainfile lintwarns when the branch is neither in the repository nor on origin. Core resolves it up the tree withtargetOf/landsOnOf, and@brainfile/core/repochooses the fork and the landing from the board (handOffTarget,prepareTarget), so the desktop and headless runners agree. epicBranch: trueon an epic gives it a branch of its own,dispatch/<epic-id>, made from the epic's target when its lead starts (prepareBranch). Its children fork from and land on it, the epic's target is brought into it before each hand-off (bringIn: in a checkout of its own, never the person's, by compare-and-swap), and when the last child is done the epic is one item in Ready to merge.- Desktop: Hand to… says where the hand-off lands,
Lands on main, with a picker of the trunk, branches and recent targets that writes the choice to the task. Merge on an epic with its own branch asks where it goes: its target, another branch, or kept as a branch, which completes the epic. brainfile discuss <task> [--agent --model --effort]talks a task over with an agent in the terminal, under the same rules as the desktop's Discuss (decision-19). The session is core's, opened through a runner, itsbrainfile mcpin rolediscuss, in the agent's read-only mode when it has one. Edits and commands are refused before anyone is asked; any other ask comes to the prompt (Allow once, Always, Deny). Ctrl-C ends it: the agent notes what was agreed on the task first, and a second Ctrl-C leaves at once.- Core:
discussPermissiontakes a surface. In asessionit allows git that only reads, as its rules say; the window's discussion still runs no commands. NewreadsGit(command)in@brainfile/core/dispatch. - The dispatch record on a task is core's:
dispatch:holds the request (agent,model,effort,by,at,until,role) and theclaimof the runner that started it, taken by compare-and-swap on the task file so two runners never start the same task; a claim from another machine is never taken without force.lintwarns on a malformed record. The desktop's hand-offs write it through core. @brainfile/core/repohas run and runner files under.brainfile/state/(runs/<run-id>.json,runners/<host>-<pid>.json), written atomically, with a heartbeat that says when a run is lost and pruning of ended runs after 30 minutes. Nothing writes them yet.brainfile issueslists the open issues of the GitHub repository the project's origin points at and marks each one a task on the board, open or completed, already carries;--import <n…>makes a task from each (title, body as the description, labels as tags, the issue's page assource:), in--columnor the first column that is not done, and finds one already made rather than making it twice. The MCP server has the same asissues(actionlist or import).brainfile add --source <url>and MCPtask_add'ssourcemake a task from an issue by hand, refused when a task already carries that URL.sourceis a typed task field in core (Task.source,TaskFileInput.source), withissueTask,linkedTask,labelTag,sourceKeyandsourcesOfin the main and browser entries, and forge detection and the GitHub issues client (pages, the ETag, the once-a-minute read) in the new Node-only@brainfile/core/forge, shared by the CLI and the desktop app. Both look for GitHub credentials in one order:gh auth token, thenGH_TOKENorGITHUB_TOKEN, then the saved token.brainfile fix [--dry-run] [--undo] [--expected <words>]: the desktop's Fix from the command line. It says what is wrong with where the board lives in the desktop's words, and what fix would do, then does it while that is still the plan shown: only a fix that loses nothing (untrack the code branch's old copy, adopt the copy that has everything, forget a lost checkout), with every copy backed up first.--undoputs it back within 20 seconds, whether the CLI, an agent or the desktop ran the fix: Undo now reads.brainfile/state/last-fix.json, not the desktop's memory.- MCP:
wherereportsfix(the problems and the plan) and takesaction: fix(withexpected, the plan's words) oraction: undo. - Core:
planFix,applyFix,undoFix,lastFixandboardProblemsin@brainfile/core/repo, moved from the desktop, which now calls them. brainfile mcpserves adiscussprompt (argumenttaskId) and adiscusstool that returns the same text. Either hands the agent the task as the board has it, including its branch, worktree and any agent working on it now, and the rules of a discussion: change only this task's description, subtasks and contract, with the person's yes; runcheckbefore recording a decision; note what was agreed; write no code.- Core: Discuss's context and rules moved from the desktop to
@brainfile/core/dispatch(discussContext,taskInformation,workInformation,discussRules,discussPreamble,discussPermission,chatPermission,discussMode). New:discussWork(projectId, taskId, runner)reads the review from git and the live run from the Runner.discussSession(ctx)is the text for a session outside the window.discussRulestakes a surface,window(default) orsession. brainfile dispatch <ids…|--column|--parent> [--agent --model --effort --until review|done]queues tasks for a coding agent:dispatch:on each, the agent its assignee, for a runner on this machine (brainfile run, the desktop) to start in its own worktree. It says when no runner is up;--nowruns them here in the foreground until they end.brainfile dispatch statuslists the runners, live runs, queue and claims from the board and the run files;brainfile dispatch stop <run|task>stops a run, whichever runner has it.brainfile review <task>shows a hand-off's branch, commits, files and conflicts;review --readylists what can land.brainfile land <task>checks with the contract's validation, then lands; a conflict or failed check is sent back through the queue.brainfile discard <task>throws the work away.brainfile agentslists the agents, which are installed, and their models.- The MCP server has the same as
dispatch,dispatch_status,dispatch_stop,review(show, ready, land, discard) andagents. brainfile mcpreadsBRAINFILE_AGENT_ROLE(chat, lead, worker, discuss): a worker's or a discussion's server lists no dispatch, stop, land or discard, and a lead's reaches only the tree underBRAINFILE_LEAD_SCOPE. Core's runner starts every hand-off's board server as a worker, and the desktop its chat, leads and discussions with their roles.- Core:
Runner'ssendBack: 'queue'sends a failed landing back through the queue rather than starting the agent in the calling process;projectOfBoardnames the project a board belongs to;brainfileMcpand sessions take a role. brainfile run [--once] [--max N] [--until review|done]: a foreground runner for a terminal split. It writes its runner file, works the board's queue (tasks and epics' leads, and what the leads hand off), prints one line per change (task-74 · codex · editing,task-74 in Review), asks you at its prompt one ask at a time, and on Ctrl-C stops its agents and gives their tasks back to the queue, its leads' with them.--oncereturns when nothing more can start. The desktop and anotherbrainfile runshare the queue through claims.dispatch --nownow runs through the same loop.- The TUI's rows say what an agent is doing on a task (
● codex · editing), from the run files, and change as the run does. - Core:
Runner.letGo()andsettled();RunnerOptions.untilandmaxRunning; a runner that works a queue keeps its runner file beaten while idle; a claim released with no outcome is queued again; a runner closing no longer pauses a lead whose turn it stops. - An epic's lead runs from the CLI and MCP, not only in the desktop.
brainfile dispatch <epic>queues a lead:dispatch:withrole: leadon the epic. A runner that takes up leads claims it, so one lead runs a tree across processes and machines.--nowleads it here until it ends. The lead'sbrainfile mcpis started as the lead of its epic. The runner checks the lead's rules again before it starts what the lead queues. brainfile dispatch pause|resume <epic>anddispatch stop <epic>reach the lead through its record, whichever runner leads it.dispatch statuslists the leads. The MCP server addsdispatch_lead(pause, resume), anddispatch_stoptakes an epic's id.- A child's hand-offs are counted in
trieson its record. Past its contract'smaxRetries, a child the lead asks for waits in Review. A child whose contract is still a draft waits for the person, as one the lead added does. - Core: the lead (
Leads,planReport,readTree,leadRefusal) is in@brainfile/core/dispatchbeside theRunner. The Runner gainslead,pauseLead,resumeLead,stopLead,guard,boardChanged,workLed,queuedLeads, and theleadsandonLeadoptions.readLeadRecordandchangeLeadFileare in core. The desktop's olderlead:record is read as a lead waiting for a runner, and replaced when one claims it.
Changed
- Desktop: the dock holds the board's agent sessions (its chats, discussions and hand-offs), each with its agent and where it stands (at work, Needs you, Reply waiting, how a hand-off ended), and opens one where it was left. Opening a document no longer makes a tab: the palette lists recent documents first.
- Changes are written to
changes/, one file each, and folded into CHANGELOG.md bynpm run version:setat release, so two branches never conflict in the changelog.npm run changelogshows the next release. - Core: starting and talking to ACP agents moves out of the desktop into the
node-only subpath
@brainfile/core/dispatch(decision-18), which takes@agentclientprotocol/sdkas a dependency: the agents Brainfile knows and how each starts, custom agents and the ACP registry, the dispatch settings and model cache, sessions, permissions and a hand-off's brief. It reads the same data folder as the desktop (BRAINFILE_DATA_DIR, else the platform's), so nothing moves. Internal: the desktop behaves as before, and nothing in the browser or main entry loads it. - Core: hand-off worktrees, their review, Ready to merge, the checks,
landing by compare-and-swap, discard and the send-back note move from the
desktop into
@brainfile/core/repo(prepareWorktree,reviewOf,readyToMerge,landReview,sendBackNoteand the rest), so the CLI and headless runners hand off and land the way the desktop does.prepareWorktreetakes the branch a hand-off forks from and lands on, the trunk unless given. - Landings run one at a time per repository across processes, not only
inside the desktop: a lock file in the board's
state/(.brainfile/state/landing.lock, with the pid, host and time of its holder) is shared by the CLI and the desktop. A landing that finds it held waits for it, and one whose holder has ended is taken over. - The desktop works the queue while it is open. A hand-off queued by
brainfile dispatch, an MCP client or the chat starts there, an epic's lead included, unlessbrainfile runin a terminal claims it first. - The desktop shows runs started outside it. A run that
brainfile run,dispatch --nowor an MCP client works on this machine gets the row's line and a tab in the dock, markedin a terminal; Stop reaches it, and Ready to merge is asked again when it ends. The phone hears them too. - An ask from a run in a terminal shows in the desktop's band, saying where
it waits. Allow or Deny writes
.brainfile/state/runs/<run-id>.answer.json, which that runner applies within a second, taking its own prompt down. - The desktop's chat hands work off through its
brainfile mcp(dispatch,dispatch_status,dispatch_stop,dispatch_lead). The desk, the app's own small MCP server for the chat, is gone. - Core:
Runner.answerAskanswers another runner's ask through its answer file, which the owning runner applies and then callscancelAsk.Runner.otherslists other runners' run files, ended and lost ones included. ThefromOfoption names the chat turn behind a hand-off started from the queue.writeRunAnswer,readRunAnswer,removeRunAnswerandrunAnswerPathare in@brainfile/core/repo;pruneRunFilesremoves answers whose run has ended.DESK_TOOLis gone. - A request the desktop wrote before runners claimed tasks (no claim and no
tries, by the person, the chat or a lead) on a task already in Review or done is no longer queued (answeredBefore): its hand-off has run, so a runner working the queue does not run it again.
Fixed
contract validate(CLI and MCP) run from another git worktree of the repository checks the deliverables and runs the validation commands in that worktree, not in the checkout the board was found from, so a hand-off's work no longer reads "File not found" or fails a check it passes. It marks the contract done and leaves the task open: the work is on its branch, and the task completes when that lands. The MCP result says where it checked (checkedIn).- Desktop: a task that depends on another now waits until that one's branch has landed, not just until it is complete on the board, so the work that follows is made from a trunk that has what it builds on. On a board with no Review column, a hand-off on its own branch no longer completes its task: the task is completed when its branch lands.
- Desktop: a hand-off forks from origin's trunk when the local one is only behind it (pull requests merged on the forge and never pulled), and landing brings the local trunk up to date, instead of building on and landing onto a stale copy. A local trunk with commits of its own is still the one used.
- Desktop: a hand-off's brief carries what the tasks it depends on said when they finished, including what they left unwired for it, and asks it to wire its part into theirs. An epic's lead makes sure one child puts the pieces together and tests the whole, and moves what a child left for later into the task that will do it.
- Desktop: the agents it starts (the chat, an epic's lead, a hand-off) get
the app's own
brainfile, run by the app's binary as node: their board server, and thebrainfilefirst on their shell's PATH. Before, they got whicheverbrainfilewas installed, and an older one has no dispatch tools, so the chat could not hand off and a lead stalled without a word. The packaged app ships its CLI in its Resources; when it cannot be found, the chat or the lead says so. Making a board (brainfile init) uses it too. - An agent's shell carries its role (
BRAINFILE_AGENT_ROLE), and the CLI keeps to it asbrainfile mcpdoes: a worker or a discussion cannot dispatch, stop, land, discard or open a pull request from the command line, a lead only in its own tree, and no agent runsbrainfile run,dispatch --nowordiscussinside its turn. The chat and a lead run only thebrainfilecommands that read without being refused. - The MCP
pull_requesttool keeps to the role asreviewdoes: a worker's or a discussion's server only asks where one stands. brainfile dispatch,run,discuss,review,land,discard,agents,pr,adopt,merge-boardsandbackupsbeside a folder of the same name run the command instead of opening the TUI on the folder.- The CLI reads its arguments as node's when an Electron binary runs it as node.
- Upgrading no longer re-runs old hand-offs: a
dispatch:record an earlier Brainfile wrote (no claim and notries) is answered whatever its column, and the window's oldlead:on an epic waits for you to resume it, listed paused, instead of starting. Every request written now carriestries. - A runner never starts an agent it could not claim the task for (a busy or missing task file, a board conflict in the window): the start is refused and says why, rather than running the task again and again.
dispatch stopand the window's Stop reach another runner's run throughstate/runs/<run-id>.stop.json, which that runner honours; it can no longer be lost in the runner's own writes, and no runner's process is sent SIGTERM any more.- A task being landed (its checks can take minutes) cannot be started, sent to another agent or landed twice meanwhile: "landing in progress".
- A lead taken up from the board when every child is complete but some have not landed finishes the epic (landing them first in a bypass repository) instead of being refused on every pass.
- Quitting the app stops its hand-offs and gives their claims back as stopped, so the next launch does not start them again; handing a stopped task off again, or a runner taking up one whose runner went away, carries on the same try.
- A runner that stays up tidies
state/runsevery few minutes, not only when it first looks at a board. - An epic with a branch of its own is put up to merge when its last child
lands with no lead, whatever lands it (
brainfile land, the MCP's land,brainfile run, Merge), not only through the window. - A lead hears when a child another runner ran fails or is stopped; it no longer shows as stopped for a moment as it finishes; and two passes over the queue can no longer start past the limit together.
- A contract check that runs past its time is stopped whole, the shell and
what it started, and the landing says it timed out; before, only the shell
was killed, and the landing hung on what the shell left running. A runner
going down can stop every check still running with
stopChecks(). - Landing, and bringing an epic's target into its branch, make their merge
commits past the repository's commit hooks (
--no-verify, as commit-tree already is): a commit-msg hook (husky, commitlint) refusing "Merge main into dispatch/task-N" no longer refuses every landing. - A checkout on the trunk that will not take the landing's fast-forward for a reason other than the trunk moving (a merge of the person's own left in progress) is refused once, in git's words, instead of running the checks five times and blaming a moving trunk.
- An agent that starts but refuses a session (not logged in, say) is killed and let go of; before, its process stayed behind.
- A session that writes no code can no longer rename, delete, copy, force or
re-point a branch through
git branchwith a listing flag first (git branch -v -m a b,git branch -r -d origin/x): every option must list, and a plain word is allowed only in list mode.brainfile pr --statusno longer counts as reading: it can complete the task, move its base and remove its worktree. - An agent whose
BRAINFILE_AGENT_ROLEnames no role this version knows is a worker, the least access, and stderr says so once; before it had a person's full access. - Agents no longer inherit the Claude Code session or the
npm runtheir runner was started under (CLAUDECODE,CLAUDE_CODE_*,CLAUDE_PID,npm_*,INIT_CWD,NODE_ENV), which had them run as nested. offerEpicBranchmakes the epic's worktree under the worktrees root it is given (BRAINFILE_WORKTREESby default), with the hand-offs'; core's worktree tests set that root, so they leave nothing in~/.brainfile-worktrees.@brainfile/coreandbrainfilesay they need Node 20.19 or later (the ACP SDK is ESM only), and no longer ship their compiled test fixtures.- A queued hand-off whose worktree could not be made (a
landsOnbranch that is nowhere, no trunk) no longer vanishes with its claim: its run is made first and fails saying why, so the window andbrainfile runshow it and its lead hears it, and the task's log saysNot started: …. An epic's branch under another epic's branch is made after the outer one, instead of failing because the outer did not exist yet. - A lead until review finishes its epic when its last unlanded child is
landed from outside it (
brainfile land, the MCP's land, another runner): the landing wakes a lead in its own runner, notesLanded on <branch>on the task so a lead in another runner sees a board change, and the lead's next look at the board tells a child that has landed since from one that had not. - A claim given back while another runner was writing the same task file is tried again, a few times with a wait between; one the board keeps refusing is said in the task's log, rather than left held by a live runner for its life with the task out of the queue and no word.
- Quitting the window during a landing no longer re-runs the task on the
next launch: the landing's checks are ended with their whole process
group, the hand-off's claim goes back as
reviewand the task is parked in Review for you to merge, and a lost claim whose run file says the agent had finished is not taken for a new try. - The window's board host commits what its last writes left (a hand-off's release and note as the app quits) before it exits, and the app gives it a moment to; before, they were folded into the next launch's first commit.
brainfile dispatch --nowon a task that waits on another says so once and runs it here once that has landed, while something of the run is still going; one still waiting when nothing more is going stays queued and is said so, and the command exits with status 1 when nothing asked for could run here. Dispatching a task already queued with the same request saysalready queuedand writes nothing;dispatch stop <epic>on a lead no runner had taken up says its request was taken off the board, not that it was stopped.- A worker's MCP
review action=landandpull_requestare refused with the command line's sentence (decision-15), rather than a schema error or a pull request'sopenquietly turned intostatus; and a worker is no longer sent tobrainfile pr, which it cannot run. - Run ids carry the runner's process as well as when it started, so two runners started in the same millisecond never share a run file.
- The CLI bundle is built beside
distand moved into place file by file, so abrainfile mcpstarted during a rebuild never findscli.jswithout thecli.mjsit imports; the window's agents process says when the bundle is missing, and the desktop's dev loop builds the CLI (and core, when it has no dist) once at start, so the app's own CLI is never stale. - A runner that stays up for days lets go of the runs that ended long ago as it prunes their files, and keeps the ends heard from other runners only while their files are.
- A hand-off aimed at a branch other than the trunk counts as landed only when its work is in that branch, what it tracks, or origin's copy of it, no longer when it is in origin's trunk.
- An epic handed off until done (bypass): a child whose branch conflicts with the trunk when it ends is now landed like the others, so the conflict goes back to the agent that did it, with a note saying how to settle it (decision-12). Before, the lead landed only children that merged cleanly, and one that conflicted waited in Review with nothing sent back.
- The board server handed to agents is the
brainfileon the PATH the runner reads and hands its agents. Before, it was looked up on the PATH the process was started with, which can find an olderbrainfile. - The warning for a validation command that
cds into the board's own workspace now also fires when the workspace is reached through a symlink, as macOS's temporary folders are.
0.22.1 — 2026-10-13
Fixed
- A board never commits
state/, this machine's bookkeeping (sync.json,autosync.lock,tui.json, brief checkpoints, the search index), whatever its.gitignoresays. A board that came to track it, from a commit made before its.gitignorelistedstate/, had every sync rewrite it and the next command commit it, reads included: history filled withlist,showandedit: state/...commits. The next commit or sync untracks it in a commit of its own, keeping the files on disk, and addsstate/to the board's.gitignorewhen it is missing. - A sync that brings in another machine's
state/(from an older version still committing it) no longer stops on a conflict there, and leaves this machine's copy as it was. merge-boardsandadoptno longer fail with "paths are ignored" on a board folder whose.gitignorelistsstate/.
0.22.0 — 2026-10-12
Memory moves onto the board, and the board learns its own shape. Decisions
with claim families, one search over a local index, a brief that starts
every session, and check before acting (spec-11). Every board has epics,
specs, plans, tasks and decisions, and knows how they fit together
(spec-12). A repository has one board, and Brainfile notices when it has two
(spec-15). Boards name their sisters in other repositories (spec-10).
Everything the CLI does to a board has an MCP tool (spec-13), and the TUI
catches up (task-19, task-20).
Upgrading
- A parent with open children no longer completes, whatever its type (it was
only epics);
--forcestill overrides. - Decisions are not listed in columns;
list --type decisionlists them.--type adrnow writes adecision(decision-N); existingtype: adrfiles read as decisions and keep their IDs, and a board's owntypes.adrentry no longer applies. idPrefixintypes:now names new IDs; astrictboard with notypes:map refuses types it doesn't know.- The SessionStart hook prints a real brief (about 1,600 characters) where it printed a reminder.
searchanswers from a local index on Node 22.13 and later (node:sqlite); older Node scans as before.enginesstays>=20.- While two copies of a board disagree, writes are refused until
adoptormerge-boardssettles it;migrate --to-branchalways commits the old copy's removal from the code branch. contract validateandtemplateexit 1 when they fail; scripts that ignored a failed validation will now see it.
Added
Memory and decisions
brainfile decide -k <claim.key> "text"records a decision in its claim family (--kind,--stance/--subject,--holds-while,--recheck,--valid-until,--sourcewith its sha256,--package name<op>version,--forwork that rests on it). The family's current decision is superseded: status,supersedes/supersededByboth ways, moved tologs/. An empty family lists related ones (same parent key, same words).supersede <id> "text"replaces one by hand;retire <id> --as rejected|stalelets one go. One board commit per command; no ledger lines.brainfile check "<action>"(or a decision ID,--claim-key,--claim-key-prefix) answersuse,review,use_replacementorno_decisionin recoil's order, with the stance rule and the predicatesvalidUntil, source unchanged and package version (package.json, go.mod, pyproject.toml). Exit codes 0, 2, 3, 0; errors 1.- A local search index at
.brainfile/state/index.db(gitignored, rebuildable) on Node's ownnode:sqlitewith FTS5: board and logs documents, the ledger,plans/and the project's Markdown, viagit ls-filesso.gitignoreholds. Tracked by size, mtime and sha256; changed files replace their chunks.brainfile index [--rebuild] [--status]. - MCP:
decide,supersedeandcheck(fourteen tools). - Core:
decisions(claim families, stance rule, verdicts; also in@brainfile/core/browser),decideFile,supersedeDecisionFile,retireDecisionFileand the file predicates in the main entry, and the new Node-only@brainfile/core/memory:MemoryIndex,searchMemory,composeBrief,checkDecision,checkClaimKeyPrefix. /v2/decision.jsondocumentssourcePath,sourceHashandpackageVersion.
Types and the graph
- Five built-in types that need no
types:entry:epic,spec,plan,task(aliasissue) anddecision(aliasadr).brainfile typesprints each with its place in the graph;types add <name> --like <type>takes a built-in's place (parents, children, completion rule, glyph). - Containment by
parentId: epic ▸ spec · plan · task, spec ▸ plan · task, plan ▸ task, task ▸ task.list --treeprints the outline (--parentfor a subtree);shownames the parent and children. decisions: [ids]on work: the decisions it rests on.add --decisions,patch --decisions;showlists them with where each stands and says when one is superseded, and a decision shows the work resting on it. The reverse is computed, never written.patch --parent / --clear-parentand--depends-on / --clear-depends-on;add --depends-on;list --typeandsearch --type, aliases included.- MCP:
task_addandtask_patchtakedecisions(anddependsOn);list_taskstakestree: trueand lists decisions withtype: "decision";get_taskreturnsgraph;searchtakestype. /v2/decision.json;task.jsondocumentsparentIdanddecisions;board.jsondocumentslike.- Core:
docTypes(registry,resolveDocType,parentFit) anddocGraph(buildDocGraph,docPlace,checkDocumentLinks,lintDocGraph), both in@brainfile/core/browser;readBoardGraphandfindWorkOnSupersededDecisionsin the main entry.
Board integrity
- Brainfile looks for every board a repository has: the
.brainfile/folder in the checkout (tracked by the code branch or not), thebrainfilebranch, the remote'srefs/brainfile/<name>, and checkouts of the board branch outside.brainfile/.brainfile where(and the MCPwheretool) lists them with document counts and last change, says how the two stand (one already has everything the other has, or each has changes the other lacks), and--fetch/fetch: trueasks the remote first. - While two boards disagree, every write from the CLI or the MCP tools is refused, naming the command that settles it; reads still work. A board ahead of or behind the remote is not two boards.
wheresays how many changes on the remote are not here yet, and reports a code branch that still tracks an old copy of.brainfile/.brainfile adopt <ref|path>takes one board as it is;brainfile merge-boards <ref|path>combines two by document ID on the field-level merge, renumbering the newer of two different documents that share an ID and rewriting references to it, and adopts outright when one board already has everything. Both leave one board on its branch at.brainfile/, descending from the remote's copy, with the code branch no longer tracking.brainfile/(one commit).migrate --to-branchfinishes the move: a code branch that tracked the board drops.brainfile/and ignores it in one commit, built so other staged work stays staged; on a board already on its branch it makes that commit if the code branch still tracks an old copy; and when a board already lives on the branch it says the two are two boards and points tomerge-boardsinstead of failing on the checkout.- One home for the board branch:
whereandlintreport a checkout of it outside.brainfile/; a checkout whose folder is gone is forgotten before the board is checked out (it used to block that) or bylint --fix.lintreports two boards as an error. - Backups: anything that replaces a board saves it first as
refs/brainfile/backups/<UTC time>-folder|branch.brainfile backupslists them;brainfile backups restore <name>puts one back.
Related boards
related:inbrainfile.mdnames sister boards in other repositories: a short alias for a folder relative to this repository's root, committed with the board.alias:task-id(for examplemobile:task-12) works anywhere a task ID does:dependsOn,show, and MCP tool arguments. The alias resolves to that folder's board exactly as runningbrainfilethere would, branch worktree included.- A
dependsOnon a related board blocks until that task is completed there; contract pickup uses the same rule. A folder that is not on this machine keeps the reference valid, says so inshowandlist, and counts as not done. list --relatedshows the open tasks on related boards that this board's tasks depend on,--related allevery related board's open tasks; MCPlist_taskstakes the same option.lintchecks therelated:map.
MCP and the command line
brainfile columns: list (the default, with task counts and--json),add <title>(--id,--after/--before,--completion),rename <column> <title>(the ID stays, so its tasks do),reorder <columns...>(the ones named first, the rest after) andremove <column>, which refuses a column that holds tasks unless--move-tonames where they go; they move there without being completed. One board commit per change.- MCP: eleven more tools, twenty-five in all:
note(append to a task's log, attributed likebrainfile note),log(one task's log, a search, the most recent),columns(the operations above),board(read; set the title, agent instructions andrelated:entries),types(list with the graph; add withlike,idPrefix,completable),template(list; use),lint(fix: true),sync(pull/push),where,retireandadr_promote. Every one has an output schema; every write is one board commit.list_tasksreports a document's ownstatusand filters by it. - The bundled schemas as MCP resources:
brainfile://schema/boardandbrainfile://schema/base. brainfile mcpsends server instructions: use these tools for every board change, never edit.brainfile/by hand, start withbrief, note as you go,checkbefore acting; then one line per tool.- A parity test enumerates the CLI's commands and fails unless each maps to
a tool, a resource, or an exclusion with a reason (
tui,mcp,merge-driver,hooks,auth,config,init,migrate,restore,index). - Core:
addColumn,renameColumn,reorderColumns,removeColumn,readBoardSettingsandupdateBoardSettings(pure, also in@brainfile/core/browser), and their*Fileforms withsummarizeColumns;removeColumnFilemoves a column's tasks first or refuses.
TUI
- The TUI has a decisions view, on
Dand at the type cycle'sdecisionstop: standing decisions grouped by claim-key family, history (superseded, rejected, stale) folded below and opened withL. Rows lead with●(○once retired) and say only a kind other thandecision,proposed,reviewpastvalidUntil, or where a retired one went. - A decision's detail in the TUI: claim key, kind, stance and subject, holds
while, recheck, valid until (
reviewonce past), replaces and replaced by, and the open work resting on it, each opened from the detail cursor. - Work's detail in the TUI lists the decisions it rests on, a retired one
marked
superseded → decision-2; its row saysdecision changed.
Core
@brainfile/core/repo: the board's git storage, sync, merge driver and autosync, moved from the CLI into core for other hosts to use (Node and git only; the main and browser entries stay free of git). The CLI behaves as before.
Changed
Memory and decisions
search(CLI and MCP) uses the index when Node hasnode:sqlite(22.13 and later): AND then OR, titles over bodies, current results first and history (completed work, superseded decisions) in its own lane, every hit with its path and line range, and the project's docs alongside the board. Filter tokens work as before. Elsewhere, or withBRAINFILE_INDEX=off, the scan answers in the same shape. New:search --json,--limit,--no-history.briefprints one budgeted block (about 1,600 characters,--max-chars): work resting on a superseded decision and decisions whose predicates broke, current decisions, your tasks (or the work in flight), what changed since your checkpoint, and the docs that bear on your tasks.--jsonaddssections,text,maxCharsandtruncated;lanesis unchanged.- The SessionStart hook injects that brief (Claude Code:
hookSpecificOutput.additionalContext) instead of a reminder, from a lean entry that loads neither commander, the MCP server nor the TUI (~150ms warm). log --recentand MCPsearch recentleave out decisions retired tologs/; they were never completed.
Types and the graph
- A parent with open children refuses to complete, whatever its type (it was
only epics);
--forcestill overrides. A decision is never completed. - An off-pattern parent (an epic under a task, anything under a decision) is a
lintwarning; on astrictboardadd,patchandlintrefuse it. AparentIdordependsOnloop is always refused. --type adrwritestype: decisionwith adecision-NID, no column andstatus: current;--type issuewrites a plain task. Existingtype: adrfiles read as decisions and keep their IDs.- A strict board accepts the five built-ins without listing them.
- New IDs use the type's
idPrefixfromtypes:; before, the prefix was always the type name whateveridPrefixsaid. listleaves decisions out of the columns (they have none) and says how many;list --type decisionlists them.brieforients against current decisions as well as accepted ADRs.
Board integrity
migrate --to-branchalways commits the old copy's removal from the code branch;--commitis accepted and hidden.
MCP and the command line
- The commander program is built by
createProgram()insrc/program.ts;cli.tsparses it. Nothing changes for the command line.
TUI
- The TUI no longer puts decisions in a column (with none they landed in
todo) or retired decisions in the done view. In the decisions view the board's mutating keys say how decisions change (decide,supersede,retire) instead of acting;estill edits. - The TUI's detail cursor is drawn as a bar as well as reverse video, so it
shows under
NO_COLOR; archived documents show their description. - The TUI's list keeps a child under its parent across columns: it is drawn
in the parent's tab, says its own column at the row's right edge (
review), and is no longer listed in its own tab with← epic-1. A child completed into logs/ stays under its open parent as✓ done, read-only, until the parent is done. Grandchildren nest too. A tab counts what its list holds;mstarts on the document's own column, and the cursor follows a child moved or completed under it.
Fixed
contract validateandtemplateexit non-zero when they fail; the result was dropped, so a failed contract read as passed (task-15).contract graphaccepts its own flags again.
0.21.2 — 2026-10-11
Descriptions come back whole, headings, dividers and all.
Fixed
- A task description may carry headings and dividers of its own. Reading one
stopped at its first
##heading, soshow,list --json, search and the MCP tools returned only the text above it; it now runs to the## Logor## Child Taskssection brainfile writes itself. - A
---line inside a multi-line frontmatter value (a description saved bypatch) no longer ends the frontmatter early. Only an unindented---closes it. patch --descriptionedits the description whereaddwrote it, in the body, instead of adding a second copy to the frontmatter that the body's stale one then sat beside.
0.21.1 — 2026-09-23
Shared boards stop showing up as a branch on the remote, and the board records which agent made each change without being told.
Changed
syncstores the board on the remote asrefs/brainfile/<name>instead of a branch. It is pushed, fetched and kept like any other ref, but hosts list only branches, so autosync no longer triggers GitHub's "had recent pushes / Compare & pull request" banner, and the board stays out of the branch list andgit branch -a. The last-known remote position is kept underrefs/brainfile/remotes/, wheregit fetch --prunenever touches it.--remote-branchis now--board-name(defaultboardinside a repository), stored asbrainfile.boardName. The old flag and config key still work.- A fresh clone finds the shared board with one lookup on its first
brainfilecommand, since a plain clone no longer downloads it. The lookup never prompts, gives up after a few seconds, and a miss is remembered for ten minutes. sync --jsonandwhere --jsonreportremoteRefin place ofremoteBranch;wherealso reportswebUrl.- Board commits are always authored by your git user. The acting agent is
recorded on the board instead: a
[codex]or[claude]suffix on the commit message, and the[name]on notes.
Added
- Agent detection. Without
--agentorBRAINFILE_AGENT, brainfile names the agent from the nearest agent CLI above the command (claude, codex, cursor-agent, gemini, opencode and others), falling back to the variables agents set for their commands (Codex'sCODEX_THREAD_ID, Claude Code'sCLAUDECODE) where a sandbox hides the process tree. Commands you type get no tag; your notes carry your gituser.name.BRAINFILE_DETECT_AGENT=offkeeps only explicit names. whereandsync --set-remotesay where the board is stored on the remote and, for GitHub remotes, link to its history (https://github.com/<owner>/<repo>/commits/refs/brainfile/board).
Upgrading
- Boards shared by 0.21.0 move themselves: the first
syncmerges thebrainfilebranch from the remote, publishes it underrefs/brainfile/, and deletes the branch (only if nobody pushed to it in the meantime). Upgrade every machine that shares the board; a 0.21.0 machine keeps pushing to the branch.
0.21.0 — 2026-09-22
Share a board without a service. Inside a git repository the board now lives
on its own brainfile branch: still ordinary files in .brainfile/, every
change a commit, kept out of your code and pull requests, and shared through
any git remote you choose. Nothing leaves your machine until you choose one.
Added
- Board on a branch. Inside a git repository
brainfile initstores.brainfile/as a worktree on an orphanbrainfilebranch, hidden from the code branch via.git/info/exclude. Outside one,init --trackedmakes the board its own small repository, and-gtargets a home board at~/.brainfile. Every change from the CLI, the MCP tools and the TUI is one commit authored as the acting agent; hand edits commit asedit: <files>. brainfile where: where the board lives, how it is stored, who else has it, and how many changes are waiting to be sent. Offline and read-only.brainfile sync [--pull|--push] [--set-remote <name|url>] [--remote-branch <name>] [--autosync off|push|full]: fetch, merge and push the board branch, entirely inside.brainfile/. No remote is ever assumed; a URL is registered as remoteboard, so a private board can sit next to public code.- Autosync. Once a remote is set, changes are pushed a few seconds after each
edit from a background process;
fullalso fetches before stale reads.brief(CLI and MCP) syncs first unless--offline. The TUI header showssynced 12s ago,local only, ornot synced · saved locally. - A clone gets the board on its first
brainfilecommand, says so, and sends changes back to the remote it came from. brainfile merge-driver, selected by.gitattributeson the board branch. Frontmatter merges field by field (the laterupdatedAtwins, ties go to the incoming side), log and note lists union by timestamp,ledger.jsonlmerges by line, and a task completed on one machine while edited on another stays completed with the edit appended to its archive. The driver is registered asbrainfile merge-driveronly when none is configured, so a custom one (for example an absolute path whenbrainfileis not on PATH) is kept.brainfile migrate --to-branch [--commit]moves an existing board onto the branch: history included when it was committed with the code, uncommitted edits and new files included always, every file checked by content before the old copy is removed, and a full rollback on failure.--to-plainreverses it.@brainfile/core:findBrainfileandresolveBrainfilePathacceptstopAtGitRoot;parseFrontmatterandserializeFrontmatterare exported.
Changed
brainfile initinside a git repository now creates the branch-backed board, at the repository root. Pass--plainfor a plain folder in the current directory, as before. Existing boards are untouched until you runmigrate --to-branch.- Board discovery stops at the repository root instead of walking into
parent directories, then looks for the board branch. A board kept above a
repository is no longer picked up from inside it; pass
-f <path>. - Storage messages from init, migrate, sync and brief say in plain words where the board is, who has it, and that nothing is lost when a sync fails.
0.20.0 — 2026-08-13
Added
brainfile brief --agent <name>— per-agent session orientation: full briefing on first run, delta since your last brief after (notes, moves, completions, contract changes). Also an MCP tool (count is now 11).- MCP: all tools declare
outputSchemaand returnstructuredContent(spec 2026-07-28); legacy-era clients receive identical JSON as text. - TUI:
Ltoggles the completed-history view, orthogonal to thettype cycle (they compose);$EDITORhandoff for editing documents; NO_COLOR support; launch resumes your last column and type filter;h/lcolumn keys.
Changed
- Completing a task now always writes BOTH
logs/ledger.jsonland the archived markdown, with rollback on failure;legacyModeis a no-op. - TUI panel system (1/2/3) collapsed into the single list; completed work
is the
Lview; rules panel removed with the rules system. - Releases are tag-driven: pushing
vX.Y.Zpublishes and creates the GitHub Release.
Fixed
- CLI errors print a clean message + usage instead of a stack trace.
migrate --logs-to-ledgeris a non-destructive, idempotent backfill — it previously DELETED the markdown archives it migrated.log --recentincludes ledger-only records;archive --to localcompletes the task instead of dead-ending; failed overlays no longer report documents removed; MCPcontract validatereturnsok: falseon failed checks instead of a tool error; help opened from a detail view returns to that detail.
Removed
- The rules system (adr-2): schema block, core rule operations, and the
rulescommand. Legacy boards parse fine;lint --fixfolds rules intoagent.instructions.[]column keybinds (useh/lor tab).
0.19.0 — 2026-08-12
Added
- TUI v3: complete redesign on ink 7 + react 19 via esbuild bundle — dense one-row-per-document board, type glyphs, signal-only color, split detail.
- First-class plans:
brainfile plan add/list/show/link,plan-Ndocuments. - MCP: spec 2026-07-28 (stateless era) via
@modelcontextprotocol/serverv2; one stdio server serves both protocol eras.
Changed
- Core owns all V2 board mutations; CLI, MCP, and TUI are thin frontends with parity locked by tests.
Removed
- V1 board-format support (adr-1);
brainfile migrateremains the converter.
Security
- Cleared GHSA-345p-7cg4-v4c7 et al. by leaving the pinned v1 MCP SDK.
0.18.1 — 2026-08-11
Fixed
- First monorepo release: workspaces, tokenless publishing (npm trusted publishing / OIDC), commander 14, jest 30.
0.18.0 — 2026-08-11
Changed
- Package renamed
@brainfile/cli→brainfile. Monorepo consolidation at github.com/1broseidon/brainfile.