An issue tracker for coding agents that works across git worktrees, clones and machines, and keeps its state off your code branches.
Several agents (and people) work on one repository at once, each in its own
worktree, often on more than one machine. They need one tracker: the same
issues everywhere, a claim that means mine and nobody else's the moment it is
accepted, and nothing to merge. beadroll gives each machine an append-only
journal of events, replays all journals into the current state, and moves them
through one dedicated git ref (refs/beads/journal) on the repository's own
remote. Ownership is settled online by a conditional update of that ref. No
server, no daemon, no database, no commits on your branches.
It reads and writes Beads JSONL
(.beads/issues.jsonl), so a tracker kept with br or bd imports in one
command and exports back for bv or any Beads reader. The command is bead;
its verbs mirror the br verbs agents actually use.
One binary, bead, built from a Cargo workspace (crates/: the pure core,
git, the store, the sync protocol and the command line), for macOS and Linux
with git 2.40+. Up to 0.1.0 it was a Python package (the tag python-0.1.0);
0.2.0 is the same tool in Rust, reading and writing the same stores and
published trees, with the same commands, output and error codes.
Alpha, in daily use by its author: this repository tracks its own work with
it, real Beads trackers (the largest 227 issues, 1.9 MB of JSONL) have been
imported and published through their repositories, and the cross-machine
protocol is exercised by a scripted drive over four machines against GitHub.
Import, export, ready and blocked have been checked against br 0.5.7 on
48 real trackers and bead's export is read by bv; upstream bd has not been
tested. The on-disk and
published format is frozen at version 1 (changes are additive; see Format
and upgrades). Seven independent reviews have been run against the code;
every finding and its resolution is in docs/FINDINGS.md.
bead is built from source; there is no package to download. It needs
rustup, which fetches the pinned toolchain (Rust 1.99.0,
rust-toolchain.toml) on first use. From a checkout's root:
cargo install --locked --force --path crates/beadroll --root ~/.local
bead --versionRun it from the root: rustup reads rust-toolchain.toml from the current
directory, not from --path, and elsewhere it builds with the default
toolchain, which fails rust-version. Without a checkout, given read access to
the repository:
cargo +1.99.0 install --locked --force --config net.git-fetch-with-cli=true \
--git ssh://git@github.com/bpcakes/beadroll.git beadroll --root ~/.localcargo refuses the scp-like git@github.com:… form; a --git install never
reads the repository's toolchain file, hence +1.99.0; fetching through git
uses the machine's own SSH setup. Either command puts bead in
~/.local/bin, and run again it upgrades it. In a checkout, cargo build --package beadroll builds the working copy as target/debug/bead. Start-up
is about 2 ms, and a read (show, list, ready) about 15 ms.
cd some-repo # any worktree or clone
bead init --import .beads/issues.jsonl # keeps ids, comments, dependencies; publishes at once
bead # the one-screen overview; bead help COMMAND for one command
bead prime # the workflow an agent loads at the start of a session
bead onboard # the instruction block for a project's AGENTS.md / CLAUDE.md
bead ready --type task --json
bead ready <epic> --json # only its subtree: its children, theirs, and so on
bead claim <id> # or: bead update <id> --status in_progress
bead comments add <id> "started"
bead close <id> --reason "done"
bead claims # who holds what, on which branch, and when each was last active
bead log <id> # who did what: machine, agent, thread, branch
bead export -o /tmp/x/.beads/issues.jsonl # for bv or any Beads reader
bead info # where the store is, who you are, what the remote still lacksSet a repository up once, from any checkout: bead init, with --import when
it comes from Beads. Nothing is committed: the tracker is found from the origin
URL, so every worktree, clone and machine of the repository shares it. On
another machine or in another clone, install bead and run bead sync there;
an init there joins the tracker the remote already publishes and never starts
a second one. Every command that writes publishes before it returns
(--offline keeps a write local); sync is the catch-up after --offline or a
remote that did not answer, and ready, show, list and the other reads
never touch the network.
Commands: create, show, update, list, ready, blocked, close,
reopen, delete, dep add|remove|list|tree|cycles, comments [add],
search, lint, plus init, import, export, sync, claim, release,
takeover, claims, log, info, machines, and for agents help, prime,
onboard. --json works anywhere on the command line; bead alone prints the
overview.
bead ready is the frontier: open issues that nothing blocks and nobody
holds, by br's rule. bead ready ID, or --epic ID as in br, keeps to ID's
subtree: its children, theirs, and so on, through every parent link and
whatever the issues in between are (closed, deferred, claimed, an epic still
waiting). ID itself is not part of it, nor is an issue whose key only looks
like a child's. Readiness is still judged on the whole tracker, so a blocker
outside the subtree (of the root, say, or of a second parent) still counts.
--parent ID keeps to ID's direct children, here and in list. A scope that
names no issue is ISSUE_NOT_FOUND, never an empty answer, and only one scope
is taken at a time.
Everything bead publishes goes to refs/beads/journal on the coordination
remote (origin by default), and only there: never to a branch, never into a
commit of yours. Anyone who can read that repository can read the tracker. A
journal holds, for every event: the issue data you wrote (titles, descriptions,
comments), who wrote it — an opaque machine id, the agent type and its session
or thread id (claude:<session>, codex:<thread>), or the --actor name you
chose — the branch and commit the command ran on, and a timestamp. It never
holds hostnames, user names, paths, or the password of a remote URL (those are
dropped before anything is stored or shown; git's credential helpers are the
place for secrets). "where": false in .beadroll.json stops events from
recording the branch and commit; bead machines --name keeps hostnames as
local aliases only. On a public repository, the ref is public: treat issue text
as you would a public issue tracker.
- One journal per machine, in append-only segment files. A machine only appends to its own journal; a full segment is never rewritten.
- Every event names what its writer had seen (UUIDv7 ids, explicit causal dependencies). Current state is the replay of all journals in causal order.
- One store per repository per machine, outside every working tree
(
~/.local/state/beadroll/<workspace>/, or$BEAD_HOME). Every worktree and clone resolves to it; nothing is written in a checkout. - Sync through one dedicated git ref (
refs/beads/journal) on one coordination remote. The pushed history holds a manifest and the journals, and no code. - Claims are persistent and settled online. A claim is a conditional update of the coordination ref; it lasts until release, close or takeover. Ownership events live beside the journal as one immutable file each and count only once the remote holds them.
- Every write is published at once. A create, edit, comment, dependency,
import or init reaches the remote before the command returns (about 1.7 s
against GitHub, like a claim);
--offlinekeeps it local. A remote that does not answer never fails a write: it is recorded, and every command says what the remote still lacks (unpublished) until a sync or an online command carries it.bead syncis the catch-up, not the only way out. - Identity on every event: machine, agent type, thread id.
- Place on every event: the branch and commit the command ran on, so a claim says where the work is happening and a close whether it is merged.
- Beads JSONL is an import and export format, not the store.
The journal holds sequential events; ownership is kept apart from it. An
attempt (a claim, release or takeover, plus any edits made in the same
command) is one small file under events/<machine>/claims/, written whole, and
such a file exists only once the remote has accepted the push that carried it.
A reader therefore sees all of an attempt or none of it.
| Action | What happens |
|---|---|
bead claim X |
Under the store lock: fetches the ref, checks nobody holds X, writes the claim as a pending attempt, and pushes it as a fast-forward of the head just fetched. Accepted: the file is moved into place and the claim is confirmed. Refused (another machine moved the ref first): the attempt is dropped and the decision is made again against the new head. No answer: the attempt stays pending, counting for nothing, until the next contact with the remote finds it there (confirmed) or pushes a fence commit on the same head, after which the lost push can no longer land, and drops it. Nothing in the journal is ever written or taken back for a claim. |
bead update X --claim --title ... |
The claim and the edits made with it form one attempt: all of it lands with the push, or none of it. The edits live in the attempt file, not in the journal. |
| the push lands but the answer is lost | The command keeps trying; when the next fetch finds its attempt on the remote, it reports the result of that attempt instead of doing the work again. Every pending attempt also carries a receipt of the command that made it, so after a crash a retry of that same command by the same actor finds its attempt accepted and reports done, rather than rebuilding the command against the state it already changed. A receipt lives until a later command of that actor goes through (the remote accepts it, or a local write is appended); a failed command in between leaves it alone. A recognised retry still answers for the claim as it is now (ALREADY_CLAIMED if someone took it over since). |
| the remote has lost objects | A ref on the remote that names objects it no longer has is REMOTE_DAMAGED, naming the ref and the repair (delete the ref there, then bead sync everywhere: every machine still has its own journal). |
| the remote refuses the push | A refusal by the remote itself (a declining hook, a read-only key, a policy) is REMOTE_REFUSED at once, quoting git: nothing was lost and retrying will not help. A remote that could not take the pack (maintenance running there, our own sync repository pruned under us) is retried after a fresh fetch, like a race. The outcome of a push is read from git's own per-ref status (--porcelain), never from text a server hook prints. A push that got no answer is REMOTE_UNREACHABLE with git's words, and the attempt stays pending. |
| an attempt file differs from the accepted copy | Attempt files are immutable: on sync the remote copy wins, the local one is kept beside it as *.fork-* and reported. A pending attempt that differs from the remote file of the same id is ATTEMPT_FORK (the machine identity is in use elsewhere). A file with an unreadable line is ignored whole. |
bead claim X --offline |
Records a provisional claim. It is listed, never exclusive, and never displaces a confirmed owner. |
bead release X |
Ends your claim; the issue is open again. |
bead takeover X --reason "..." |
Replaces the current owner and starts a new generation. Online only. |
bead close X / bead delete X (as owner) |
Closes (or tombstones) and ends exactly your claim, then publishes at once. show tells whether the closing commit is merged into the default branch. |
| old owner acts after a takeover | Its close or release names a claim that is no longer current, so every machine ignores it and reports it under stale. |
in_progress is reached only through a claim, so update --status in_progress
claims. Any other status change on an issue someone else holds is refused
(--force overrides), --status closed is the same transition as bead close,
--status tombstone is refused in favour of bead delete (which ends the claim),
and ready never offers an issue that has a confirmed owner. The owner is the
full agent thread id. Ownership never expires. bead claims shows each owner's
branch, last journal activity, and any of our own attempts still awaiting an
answer; there is no heartbeat. While an attempt of ours is unanswered, further
ownership commands refuse (UNRESOLVED) until a sync can settle it; plain
edits are never held up.
| Two machines, before syncing, both… | Result on every machine |
|---|---|
| edit different fields of one issue | both edits kept |
| edit the same field | both values kept and listed under conflicts; the newer one is shown; any later write that has seen both settles it |
| set different statuses | as above, and until settled the issue is not ready, cannot be claimed, and does not unblock others |
| add comments | all kept |
| add and remove a label or dependency | a removal removes only the additions its writer had seen |
| create a child of the same parent | the later one is renumbered (x.1 → x.2) and sync reports it |
| add dependencies that form a loop | kept, reported by dep cycles, never ready |
An edit made after seeing another is never a conflict, whatever the clocks say.
- An interrupted append (crash mid-write) is repaired before the next write or
publish: the fragment is kept beside the segment as
*.torn-*. - A malformed event, an event of a newer format, an unknown operation, or a
field of the wrong type is reported by
bead infounderintegrityand applied by nobody; everything unrelated keeps working. One event id with two different payloads is set aside the same way, and nothing waits on it. A record without a usable id is reported wherever it was found. - An event that does not build on its predecessor in its own machine's journal
is reported and ignored, and its position is never used to infer what it saw.
On sync, that condition in our own journal is
JOURNAL_FORK: the machine identity is in use somewhere else. A copy of an event found in another machine's journal never changes where the event sits in its own. - A takeover that names a claim its writer had not seen, or that is not a claim on the same issue, applies nothing and is reported.
- A ref whose manifest is not this version's format is refused untouched
(
FORMAT_UNSUPPORTED): an older client never rewrites a newer tracker. - A damaged private sync repository (
sync.gitwith empty or corrupt objects, which git reports as a local read error) is set aside assync.git.damaged-<time>and made again from the remote andevents/, once per command;infolists the copies kept, which hold nothing that is not also elsewhere. Damage again after that isSYNC_REPO_DAMAGED. - git's own maintenance is off in
sync.git(no detachedgcracing the next command) and what a fetch brings stays in the pack it arrived in. bead repacks the repository itself, under the store lock, once loose objects or packs pile up (BEAD_REPACK_LOOSE,BEAD_REPACK_PACKS);sync --jsonthen saysrepacked. title,priority,issue_typeandstatuscannot be cleared; asetthat tries is reported and applies nothing.
Events carry "v": 1 and the published manifest "format": 1. Format 1 is
frozen; changes are additive. A newer bead may add an operation or a field,
still as format 1; an older bead sets aside the events it cannot apply
(reported under integrity, applied by nobody), carries them through sync
untouched, and applies them once upgraded. Machines can therefore be upgraded
in any order; a mixed fleet is safe, and shows integrity notes until it is
uniform. The format number changes only for a change that would make an old
reader misread events; none is planned. Should one ever be needed it follows
writers lag readers: the new bead reads both formats but writes the old one
until every machine on the tracker has announced, in the manifest it publishes,
that it reads the new one; then it flips. Nothing ever rewrites existing events.
- An imported dependency on an issue that is not in the tracker yet is kept by key and becomes a real edge on every machine the moment that key exists, so importing in several batches gives the same result as one. An import is checked whole before anything is written: one bad record fails it.
- Several bead processes on one machine (agents in different worktrees) never
collide inside the private
sync.git: each fetch lands in a ref of its own. - When the publish of an owner's close could not go out, the
--jsonanswer says so ("unpublished": "recorded locally, not yet published (…)") and a retry of the close publishes as the first run would have. - Human output never hands the terminal a control character: titles, comments
and names from journals or imports are shown with
\xNNescapes. JSON output and stored data are untouched. Output is UTF-8 whatever the locale. - A password in a remote URL is dropped (with a note) and never stored, shown
or used, whatever its shape (
https://TOKEN@host,https://TOKEN:x@host,transport::https://…included), and only user-info in the authority counts: an@in a path or query is left alone; git's credential helpers are the place for secrets. The store is private by its own modes whatever your umask (0700 directories, 0600 files,sync.gitcreated--shared=0600); files written into a checkout (.beadroll.json,export -o) follow your umask like any other file there.BEAD_HOMEmust be absolute (~is expanded): the store never lives in a checkout. - An event nests at most 100 levels deep, so every supported Python can read
what any machine wrote; an import deeper than that fails whole, as do a
repeated id, a self-dependency, malformed comments, a record whose
created_atis null and text holding a lone surrogate. Records end at\nonly, so bead's own export (with U+2028 and friends in text) imports again. - Every bead reads a journal line the same way, whichever Python (or binary) it
runs on: as UTF-8, never in an encoding guessed from its first bytes; at most
512 levels deep; with NaN, Infinity and numbers beyond a float refused. A line
that fails is reported under
integrityand skipped, never guessed at, and so is an event that would have made replay or a query fail. - A status bead does not set itself (
pinned,hooked, a custom one; Beads tools keep these as they are) is imported and exported verbatim; such an issue is neither ready nor done.bead update --statustakes only bead's own vocabulary. An imported record withoutcreated_bystays unattributed; the importer is not its author. - An imported dependency on a key that is not an issue yet is listed by
showanddep listasunresolved_dependencies("not an issue yet"), exported under the record's own id, and stamped like any other dependency when it binds. A new or renamed issue never takes a key such a dependency is waiting for. - A closed stdout or stderr, a terminal that hung up (EIO) or a descriptor closed at exec never produce a traceback: a committed command exits 0, a failed one keeps its status. Help is plain text, never coloured.
- An owner's close that a takeover superseded says so:
"superseded"on the record and a!line, exit 0 (the decision indocs/REVIEWING.md§2 stands). - Text comes from files and stdin as UTF-8 whatever the locale; anything else
is
VALIDATION. A read-only store, a full volume or a missing file is anIOerror, never a traceback, and never a half-written journal. A fetch that gives no answer is killed with the helpers it started; a push that gives no answer is abandoned on our side and left to land or fail on the remote's, which is exactly what the pending-attempt protocol settles next. - An imported dependency on a key that is not an issue yet can be taken back
(
dep remove A KEY, or re-importing the record without it) and stays taken back when the key arrives. - A foreign journal file that differs from the published copy is replaced by
the published copy, ours is kept beside it as
*.fork-*, andsyncreports it. Our own file in that state isJOURNAL_FORK.
The golden path needs none. The workspace is derived from the origin URL (where
git push origin goes: the first pushurl, else the first url, read as git
reads it, so host:path, ssh aliases and gh: shortcuts are remotes, not
paths; git@host:org/repo and https://host/org/repo are one workspace) and
the coordination remote is origin, looked up each time, so nothing is
committed and every checkout finds the same tracker. Before bead init starts
a tracker it asks the remote: one already published there is joined instead,
and adopted when it was published under another id (below).
A tracked .beadroll.json is the exception: for a coordination remote other
than origin, or for "where": false. bead init --tracked [--remote NAME_OR_URL] writes one with an explicit workspace id (a tracked id the remote
already publishes is kept; a URL-derived tracker stays URL-derived, since its id
would stop matching when the URL changed) and optionally a different
coordination remote. Origin's own URL is stored as origin, so the file follows
origin when it moves; a remote given by name, here or in --remote and
$BEAD_REMOTE, means where git push NAME goes, its push URL, as origin itself
does. Commit the file on the default branch at once: a checkout whose tree
predates it reads the committed copy (HEAD, then origin/HEAD), but a file
that is not committed, or not on the default branch, reaches no other checkout,
and bead info and bead prime warn about it until it does. Running it again
keeps the keys it does not manage. --remote and $BEAD_REMOTE override it per
call; a tracked remote that differs from the one a store already uses is
refused until named deliberately, and a tracked id that is another repository's
derived id is refused outright: a cloned repository cannot attach you to someone
else's store.
bead's own git runs in a private bare repository with the checkout's transport
settings carried over (http.*.extraheader, credential helpers,
core.sshCommand, includeIf includes), the project's object format, no
client hooks, and the remote string always passed after --.
"where": false in .beadroll.json stops events from recording the branch and
commit; outside a checkout nothing is recorded anyway.
Agent type and thread come from the environment: CLAUDE_CODE_SESSION_ID
for Claude Code and CODEX_THREAD_ID for Codex (both confirmed to reach shell
commands). When one agent is started from inside the other's session both are
present, and bead picks the nearest agent in the process tree. BEAD_AGENT,
BEAD_THREAD and --actor NAME override them. The machine is an opaque id; its
hostname is a local alias (bead machines --name ID=ALIAS), never in events.
A new origin URL (a rename, another owner, another host) gives the URL-derived
workspace a new id. Each machine, once its checkout's origin is the new URL,
finds no tracker until its first bead sync (or bead init), which adopts the
tracker published under the old id and remembers the mapping (moved.json in
BEAD_HOME). That needs the move to carry the tracker's ref,
refs/beads/journal: a rename or a transfer on a forge keeps every ref, and
git push --mirror copies them. An import that copies only branches and tags
leaves it behind: bead sync then says none is published and names this
machine's tracker of the same name if it has one; BEAD_WORKSPACE=ID bead sync
on a machine that has it publishes it at the new URL, and every other machine's
next sync adopts it. Until a machine's origin moves it keeps using the old
URL, which a forge's rename redirects.
A tracked .beadroll.json keeps its id through any move. With its remote left
to origin, or named origin, the first sync after the move publishes the
tracker at the new URL if it is not there yet. A tracked remote stored as a URL
keeps pointing at the old place: bead init --tracked --remote origin rewrites
it, and the file is committed again.
./check.sh # every test, the doctests and the infra/ drivers' tests (about 4 minutes)
./check.sh --stress # also the randomised multi-machine models
./check.sh --real # also import/export of this machine's real tracker exports, with br and bv./check.sh runs cargo xtask check with the options given: one line per step, a scratch
BEAD_HOME for every step, and a non-zero exit when any step fails, the later steps still
running. It needs rustup and cargo-nextest. Every selected check must pass. --real exits
non-zero if a command fails or exported records differ, and keeps the failing round-trip report
instead of only its last line. Its imports run offline in the scratch store.
The tests are the Cargo workspace's, one integration-test binary per crate, organised by subject
as the Python suite they were ported from was, test by test: replay (causality, registers,
quarantine, validation, cycles; in beadroll-core, in-process and fast), protocol (claims,
attempts, fences, lost acknowledgements, sync races), storage (segments, torn tails, identity,
readers against writers), commands (every verb end to end, identity detection, branch
tracking), compat (Beads import/export, real exports, br/bv), hardening (the base under
the protocol: git environment and remotes, URLs and credentials, store files and modes, output
streams, imports), stress (randomised models, run by --stress), and sweep (the ownership
protocol enumerated: three machines, nine operations at seven fault boundaries, every ordered
pair of follow-ups; a twelve-schedule slice by default, including retries of interrupted owner
closes and deletes; BEAD_SWEEP=full for all 660, with no failure exemptions). golden compares
every help page and the reading commands' output with Python's, captured before the cutover, and
parity holds what the mixed Python and Rust fleets checked, among it receipts byte for byte as
Python wrote them. crates/beadroll-test-support holds the sandbox: every test builds its own
repository, bare remotes and machine homes in a temporary directory, each machine with a scratch
BEAD_HOME, so the default store is never touched.
Every test that runs bead runs the one Cargo builds, unless BEAD_UNDER_TEST names another by its
absolute path. Until 0.2.0 bead was a Python package, whose suite the Rust tests port; the
port's tests passed against both, and the last Python is tagged python-0.1.0
(docs/RUST-PORT.md is the plan the port followed).
Seven independent reviews have been run against the code. Their reports are kept
verbatim under docs/reviews/; docs/FINDINGS.md is the ledger of every finding, its status,
how it was resolved and which tests cover it. docs/REVIEWING.md is the brief for the
next one: the map of the design, the decisions that are not findings, the rules,
the checklist and the deliverables. infra/ holds the maintainer's harness against
real machines and a throwaway GitHub repository, stdlib Python that drives any bead:
drive.py (cross-machine scenarios over ssh), soak.py (random agent-like traffic),
roundtrip.py (import/export over real Beads exports), selected.py (which bead each runs, and
the record of it before any test data is written), their tests beside them, and the synthetic
seed.jsonl.
| First design | Now | Why |
|---|---|---|
| Concurrent values need an explicit resolution operation | Flagged with every alternative; any later write that saw them settles it | Same information, no extra protocol for agents to learn |
| Fallback display is the smallest event id | The newest event id | Matches what people expect to see |
Conditional push with --force-with-lease |
Plain fast-forward push | Already conditional, and cannot truncate history |
| A corrupt duplicate holds back its dependents | Only the event itself is set aside | With a project-wide frontier, everything later depends on it |
| Heartbeat events | Last journal activity per owner | No extra traffic or permanent noise |
| Structured acceptance items | One text field | Round-trips through Beads JSONL |
| Checkpoints and pruning | Segment rotation only | The rest is not needed at this size |
| Owner's close is a conditional transition | Local write, published at once | The stale-claim rule makes a late one harmless |
| Ownership in the journal | Ownership files beside the journal, pending until the remote holds them | A refused or unanswered claim then needs no compensating record, and the journal stays append-only |
- The owner of a claim is the agent thread. A later session is a different
owner and has to
takeover(or use a stable--actor) to continue the work. - The store lock is held while a
claimfetches and pushes, and while asyncpushes, so other local writes wait one or two network round trips (a second or two against GitHub). A routinesyncfetches before taking the lock. - A
--statuschange made withupdatefollows the same ownership rules and publishes an owner's close immediately, likebead close;--parentrefuses a loop, likedep add. - Exported Beads comment ids: an imported comment keeps its id; a comment made
here exports an integer derived from its event id (48 bits, from 2^47), so
every machine and every later export agrees. Two such comments sharing a
number is a one-in-10^14 event; if it happens, the tie is broken by the ids
alone and one of them moves. Beads readers accept these ids;
brwould not have minted them. dep cycleslists every elementary cycle, up to 200.- What each event had seen is kept as one clock per event over chains: every
machine's journal is a chain, and the claims, releases and takeovers of
attempt files join chains of their own, about one for each machine that
writes them. A clock holds the furthest position seen on each chain, so
whether one event saw another is one comparison, and memory grows with events
× chains rather than with events × ownership events. Replaying 200,000
events, about 55% of them in attempt files, takes 0.33 s and peaks at 958 MB,
747 MB of it the loaded journal (
docs/RUST-PORT.md, Appendix A; Python's bitsets took 11.8 s and 1.7 GB). - The branch recorded on an event is whatever
HEADwas in the directory the command ran in. It is a trace, not a lock: a claim does not pin a branch. - With no persisted projection, every command replays the journal: 8 ms and a
52 MB peak for 10k events, 53 ms / 236 MB at 50k and 0.26 s / 921 MB at
200k (
docs/RUST-PORT.md, Appendix A; most of the memory is the loaded journal, roll-dih8.11), far beyond the trackers measured so far (the largest real one is 280 issues). A persisted projection is the obvious next step if that ever matters. A fetch or push that gets no answer fails afterBEAD_NET_TIMEOUTseconds (default 45) instead of hanging. - Publishing is serialised on one ref. Under heavy contention (four machines syncing continuously) claims took 9 s median and 27 s at p90, and 3 of 841 commands gave up after eight attempts.
- A renumbered child key changes under the machine that created it. Events refer to issues by an immutable id, so nothing is lost, but a key written into a commit message before the sync now names the other issue.
- A repository with neither a remote nor a
.beadroll.jsongets a machine-local tracker that cannot sync. - Not implemented:
stats,count,epic status,graph, saved queries, templates, external-project dependencies,bv-style ranking.
beadroll is not a fork of Beads and does not use its database. It shares the
JSONL record format (import, export), the issue model (ids, parent-child
keys, dependencies, comments, labels, statuses) and the verbs agents use, so a
project can move from br/bd to bead and back with its history intact
(bead export writes what br sync --import-only and bv read);
MIGRATING.md is the procedure, both ways. What it
adds is the multi-machine, multi-worktree model: journals instead of a database,
one ref instead of commits on branches, claims settled online. What it leaves
out is listed under Known limits. A leftover .beads/ directory is history:
nothing in bead reads or writes it after the import.
See CONTRIBUTING.md: the setup, the one rule about stores,
the layout and how a change is made and tested. AGENTS.md is the same for an
agent working in this repository; docs/REVIEWING.md is the brief for an
independent review. Unless you say otherwise, a contribution is licensed as the
project is.
Licensed under either of the Apache License, Version 2.0 or the MIT license, at your option.