Status: pre-alpha. The core tracker works — tasks, epics, dependencies, an append-only event stream, typed notes, cross-worktree claims and presence, full-text search with structured filters, activity and staleness reads, external refs that link tasks to GitHub PRs and Linear issues with live status through
refreshand policy-driven advancement throughreconcile,snapshot/restoreso the store survives a fresh clone, and twenty-seven commands over them, includingbriefandboardfor restoring context at the start of a session, andmigrate beadsfor importing an existing beads backlog. Seedocs/katra-spec.mdfor the full design.
katra is a local, git-native, agent-first project manager and coordination layer for AI coding sessions working in a single repo across multiple git worktrees.
Named for the Vulcan katra — stored consciousness that can be carried and later restored. That is the whole idea: context that survives the end of a session.
An AI coding session starts cold every time. It doesn't know what the last session did, what was decided and why, or what another session in a sibling worktree is touching right now. Existing trackers are built for humans reading a web UI, and the ones that aren't tend to commit their database into the repo, where it turns into merge conflicts.
- Agent-first. In the ideal case a human never touches katra directly — they get information through the agent. Every read has a
--jsonmode, andkatra brief <task>assembles a whole working context in one call. - Git-native, zero-ceremony. One SQLite database under your repo's shared git dir. Every worktree resolves to the same store. It is gitignored by construction, so there is no binary merge conflict to have.
- Daemon-free. No server, no port, no background process. It's a file and a CLI.
- External refs are augmentation, never a requirement. katra links to GitHub issues/PRs (Linear and Jira later) through pluggable providers, and it only ever reads them. With no provider installed and no network, every core feature still works.
$ katra init
Created katra store at /your/repo/.git/katra/katra.db
$ epic=$(katra add "core tracker foundation" --level epic)
$ a=$(katra add "storage layer" --lane Planned --priority 0 --parent $epic)
$ b=$(katra add "task CRUD" --lane Planned --priority 1 --parent $epic)
$ katra dep $b --blocked-by $a
$ katra next
kt-x93qjo P0 storage layer
lane Planned
blockers none
epic kt-34vt8g core tracker foundation
$ katra close $a --reason "shipped"
kt-x93qjo is now Done
reason shipped
unblocked 1:
kt-s3l2m4 task CRUDEvery write is recorded, so the next session can read what the last one did:
$ katra update $b --lane "In Progress"
$ katra note add $b --kind handoff --body-file - <<'EOF'
Storage layer is done. CRUD is scaffolded but the update path
still needs the reparenting case.
EOF
$ katra log $b
2026-08-05 17:31 note-added kt-s3l2m4 task CRUD nt-rxqzhj
2026-08-05 17:31 status-changed kt-s3l2m4 task CRUD Planned -> In Progress
2026-08-05 17:31 created kt-s3l2m4 task CRUDkatra show puts the same thing beside the task, both sections capped so a
summary stays a summary:
$ katra show $b
kt-s3l2m4 task CRUD
lane In Progress
epic kt-34vt8g core tracker foundation
blockers none
notes (1, newest first — `katra note list` for bodies)
nt-rxqzhj handoff 2026-08-05 17:31 Storage layer is done. CRUD is scaffolded but the updat…
activity (newest first — `katra log` for the rest)
2026-08-05 17:31 note-added nt-rxqzhj
2026-08-05 17:31 status-changed Planned -> In Progress
2026-08-05 17:31 createdA session that comes back cold reads one command instead of three. katra brief
carries the handoff in full — that is the difference from show, which
prints previews:
$ katra brief $b
kt-s3l2m4 task CRUD
level task
lane In Progress
priority P1
epic kt-34vt8g core tracker foundation
blockers none
handoff — last touch main @ /your/repo, 2026-08-05 17:31
Storage layer is done. CRUD is scaffolded but the update path
still needs the reparenting case.
activity (newest first — `katra log` for the rest)
2026-08-05 17:31 note-added nt-rxqzhj
2026-08-05 17:31 status-changed Planned -> In Progress
2026-08-05 17:31 createdAnd katra board answers the other question — where does the whole repository
stand? Actionable first, activity last, and the counts are totals even when a
section is capped:
$ katra board
1 open · 1 in flight · 0 ready · 0 blocked · 0 untriaged
in flight
kt-s3l2m4 P1 In Progress task CRUD
recent (newest first — `katra log` for the rest)
2026-08-05 17:31 note-added kt-s3l2m4 task CRUD nt-rxqzhj
2026-08-05 17:31 status-changed kt-s3l2m4 task CRUD Planned -> In Progress
2026-08-05 17:31 closed kt-x93qjo storage layer Planned -> Done shipped
2026-08-05 17:31 created kt-s3l2m4 task CRUD
2026-08-05 17:31 created kt-x93qjo storage layer
2026-08-05 17:31 created kt-34vt8g core tracker foundationkatra board --digest puts the newest handoff in the store above all of that,
which is what a session opening in a fresh worktree wants to read first.
| Command | What it does |
|---|---|
init |
Create the store for this repository |
migrate beads |
Import an existing beads backlog — preview by default, --apply to write |
add · show · list |
Create and read tasks; list filters by lane, kind, level, epic, tag, assignee, priority, and ready/blocked |
update |
Change any mutable field, including reparenting |
close · cancel · reopen |
Finish, abandon, or revive — and report what each released |
delete |
Remove a task that should never have existed |
dep · link |
Blocking dependencies, and associations that don't block |
next |
The one task that can be started right now |
claim · release |
Hold a task for this worktree while you work it, and hand it back when you're done |
log |
What has happened — to one task, to an epic and its children, or across the store |
note add · note list |
Typed prose on a task: general, handoff, decision, acceptance |
brief |
Everything needed to resume one task or epic, in one call — handoff body included |
board |
Where the repository stands: in flight, ready, blocked, and what just moved |
search |
Full-text over titles, descriptions and notes, plus structured filters, with or without query text |
recent |
Activity-sorted — what has been touched, newest first |
stale |
Open items with no recent activity — --older-than defaults to two weeks |
Every read takes --json. Every refusal names what would unblock it — an ambiguous id lists the candidates, a rejected dependency prints the cycle path, and next with nothing ready tells you whether the work is blocked, untriaged, or simply finished.
Every write records an event in the same transaction as the change itself, so history can never describe something that did not happen. Events are never deleted: katra delete appends a final deleted event carrying the task's title, and katra log <id> still answers for a task that no longer exists. Notes are the opposite case and are removed with their task — history survives, content does not (ADR-008).
Each event and note records who wrote it as <branch> @ <worktree path>, so two agents in two worktrees are always distinguishable in the record (ADR-007).
katra claim <id> records that this worktree is working a task. A second worktree attempting the same claim is refused and told who holds it and how recently they were seen:
$ katra claim kt-s3l2m4
kt-s3l2m4 claimed by main @ /path/to/repo
$ katra claim kt-s3l2m4 # from a second worktree of the same repo
katra: kt-s3l2m4 is held by main @ /path/to/repo, last seen just now — release --force to take it overnext and board steer around a claim without ever moving it between the board's counts (ADR-012): next never offers a task another worktree holds, and hands your own still-Planned claim back first if you have one; the board marks another worktree's claimed rows and lists them last. katra release <id> gives a claim back, and close/cancel release it for you automatically. A claim left behind by a session that will not return is taken over with katra release <id> --force, informed by exactly the holder and liveness a refusal already showed. Presence — the "last seen" behind that liveness — is a heartbeat only: every command bumps it for its own worktree, no hooks required (ADR-011), and claims are scoped to a worktree rather than a session, so two agent sessions sharing one worktree share one claim too.
katra search <query> is full-text over titles, descriptions and note bodies, built on SQLite's own FTS5 — no extra dependency, and the index stays current automatically (ADR-013). A note match rolls up to the task it belongs to and says so:
$ katra search oauth
kt-owvndz P2 Defined feat oauth migration for the billing service
[oauth] migration for the billing service
kt-cr8lrz P2 Defined feat rotate the staging credentials
note match — still need to sort out the [oauth] callback…The same command takes structured filters — --lane, --kind, --level, --epic, --tag, --updated-before/--updated-after — with or without query text, so "everything tagged urgent" is as valid a search as a keyword:
$ katra search --tag urgent
kt-ryc943 P2 Defined feat tag demo taskkatra recent reads your own event history back to you, newest first — the direct answer to "what was I working on":
$ katra recent
kt-cr8lrz P2 Defined rotate the staging credentials just now
kt-owvndz P2 Defined oauth migration for the billing service just now
kt-8ind1q P2 Defined core tracker foundation just nowkatra stale is the inverse — open items nothing has touched in a while, oldest first, default window two weeks:
$ katra stale
stale — untouched since before 2026-07-31T02:43:24.049Z
kt-dlcpbk P2 Defined old audit follow-up nobody touched 15d agokatra ref add attaches a task to the thing that tracks or ships it elsewhere — paste a GitHub PR/issue URL or a Linear id and katra derives the provider and a canonical qualified id (ADR-014); re-adding is a safe no-op that says so:
$ katra ref add kt-28fs2e https://github.com/acme/billing/pull/128
kt-28fs2e linked github: acme/billing#128 https://github.com/acme/billing/pull/128
$ katra ref add kt-28fs2e ENG-451
kt-28fs2e linked linear: ENG-451
$ katra ref add kt-28fs2e https://github.com/acme/billing/pull/128
kt-28fs2e already linked github: acme/billing#128 https://github.com/acme/billing/pull/128show and brief carry a task's refs, so the next session finds the review context without git archaeology:
$ katra show kt-28fs2e
kt-28fs2e oauth migration for the billing service
level task
kind feat
lane Defined
priority P2
blockers none
refs github: acme/billing#128 https://github.com/acme/billing/pull/128
linear: ENG-451Any other tracker stores through the explicit form — core is provider-agnostic in what it keeps, opinionated only in what it parses:
$ katra ref add kt-28fs2e https://gitlab.com/acme/tool/-/merge_requests/9
katra: not a recognized github.com or linear.app reference URL — store it explicitly with --provider <name> --id <id> [--url <url>]
$ katra ref add kt-28fs2e --provider gitlab --id "acme/tool!9" --url https://gitlab.com/acme/tool/-/merge_requests/9ref add itself never touches the network. ref remove takes the url, the qualified id, or provider:id when two refs collide, and linking/unlinking is recorded in the task's history like every other write.
katra refresh asks each ref's tracker what actually happened — GitHub through your already-authenticated gh, Linear through its API with LINEAR_API_KEY in the environment — and fills the caches show and brief render:
$ katra refresh
2 ref(s) checked — 1 updated, 1 unchanged, 0 unresolved
updated (1)
linear: GRI-4 none -> unstarted
unchanged (1)
github: itsacoyote/katra#13
$ katra show kt-qyeewf
refs linear: GRI-4 unstarted Set up your teams · synced just nowA real change lands in the task's history as a ref-status-changed event; an unchanged ref just bumps its sync time. Offline, unauthenticated, or an unknown provider is a state, not a failure — every ref reports its reason and refresh exits 0:
$ katra refresh
2 ref(s) checked — 0 updated, 1 unchanged, 1 unresolved
unresolved (1)
linear: GRI-4 LINEAR_API_KEY not setrefresh is pure read on the external side and never moves a task — acting on what it learned is reconcile's job, deliberately a separate, explicit command (ADR-015 covers why providers are built-in rather than discovered plugins).
katra reconcile is the one path by which external state can move a task, and it never runs implicitly. It reads only what refresh cached — no network — applies a fixed policy map (GitHub merged → Done, Linear completed → Done, Linear canceled → Cancelled; everything else no move), and previews by default:
$ katra reconcile
2 task(s) checked — 1 would advance, 1 blocked, 0 conflicting, 0 skip-claimed, 0 no-op
advance (1)
kt-qyeewf wire the auth flow -> Done
reason: merged — github:itsacoyote/katra#13
github: itsacoyote/katra#13 merged · synced just now
blocked (1)
kt-ab12cd split the parser
github: itsacoyote/katra#14 open · synced just now
$ katra reconcile --apply
2 task(s) checked — 1 advanced, 1 blocked, 0 conflicting, 0 skip-claimed, 0 no-opA task with several refs advances only when all of them agree (one merged PR out of three is not done); refs that disagree — one says Done, another Cancelled — are a flagged conflict and never auto-apply. A task claimed by another worktree is skipped and reported, even under --apply, and a never-refreshed ref holds its task back ("couldn't read it" never means "it's gone"). Every applied move lands in the task's history as a closed/cancelled event stamped actor = reconcile with the triggering ref in the reason — reconcile-derived changes are always distinguishable from an agent's own judgment. The policy is data inside the engine, not branches; a user-facing way to swap it is deliberately deferred (ADR-016).
The store lives inside .git/, so it is invisible to git by construction — which is right for cross-worktree coordination but means a fresh clone starts empty. katra snapshot writes the whole store to one deterministic, git-diffable JSONL file you commit; katra restore rebuilds a store from one:
$ katra snapshot
wrote 954 row(s) across 9 table(s) to /repo/.katra/snapshot.jsonl (schema v6)
$ git add .katra/snapshot.jsonl && git commit -m "snapshot the backlog"
# on a fresh clone, or to undo a bad write:
$ katra init && katra restore .katra/snapshot.jsonl --apply
applied .katra/snapshot.jsonl: loaded 954 row(s) across 9 table(s) (schema v6 -> v6)An unchanged store snapshots to a byte-identical file, so a no-op session commits a clean diff. restore previews by default — --apply executes, and --force is additionally required over a non-empty store, since restore replaces everything. It rebuilds a fresh database at the snapshot's own recorded schema version and migrates it forward, so a snapshot dug out of git history stays restorable after upgrades; the previous store is kept alongside as katra.db.bak. Snapshots carry every source-of-truth table (claims included; presence, machine-local telemetry, is not), round-trip stored bytes exactly (a backup never sanitizes), and are how a backlog is shared, survives a fresh clone, or a bad write is undone (ADR-017, ADR-018).
Everything above is pull: an agent (or its human) has to remember to check the board, claim before working, and release after. The specific failure that bites is a silent one — worktree A claims a task and edits it, worktree B runs release --force to take it over, and A never notices and keeps editing. Two live worktrees now own the same work, and nothing catches it at the moment of the edit.
katra install-hooks <agent> wires katra into an agent's own native hooks so the coordination happens on its own, no convention required. It merges three touchpoints into the agent's settings — for Claude Code and Codex today:
$ katra install-hooks claude
installed claude hooks into .claude/settings.json — review and commit it; shared with your team- session start injects
katra board --digest, so the agent orients on the current board without being told to. - before edit runs
katra guard, which denies the edit — exit 2, with the reason fed back to the agent — when the caller worktree's in-progress task has been force-taken by a different, live worktree, and allows it otherwise (it still holds the task, it re-coordinated onto other work since, or the rival went stale). Enforcement is task-level: katra's claims are task↔worktree with no file scope, so guard catches the takeover, not which file you touch (ADR-019). Any infrastructure problem — no store, a locked database — fails open: a hook that can't read the store must never block every edit in the session. - session end runs
katra release --mine, releasing every claim this worktree holds so the next session sees them free. It fires only on a real exit — a/clearor a resume keeps your claims, so the session picks its work back up.
The merge is idempotent and reversible: re-running makes no further change, --print shows the exact block without writing, --remove strips only katra's entries and leaves the rest of your settings untouched, and --local targets .claude/settings.local.json for a personal trial before you commit. The adapter contract is one thin per-agent mapping over shared touchpoints, so adding an agent needs no core change (ADR-020). Claude Code is the proven path; the Codex adapter is best-effort against an evolving hooks surface. See AGENTS.md for the full setup, the trust step each agent requires, and the caveats.
External provider discovery. The spec describes it.
Published as @itsacoyote/katra:
npm install -g @itsacoyote/katra
# or run without installing
npx @itsacoyote/katra --helpThe unscoped
katraname on npm belongs to an unrelated, abandoned package, so katra publishes under the@itsacoyotescope. The installed command is stillkatra.
Then, per repository: katra init creates the store, and docs/agents-snippet.md is the block to paste into that repository's AGENTS.md so agent sessions actually use it.
Requires Node ≥ 22.12 and pnpm.
pnpm install
pnpm build # bundle with tsup
pnpm test # vitest
pnpm check # lint + typecheck + test — what CI runsThe suite runs against real SQLite in throwaway git repositories, and spawns real OS processes where multi-process contention is the thing under test. The traceability docs map every acceptance criterion to the test that covers it, and record where coverage is genuinely limited rather than claiming a tick: f1, f2, f3, f4, f5, f6.
Already tracking work in beads? katra migrate beads converts your export in one shot — preview by default, --apply to write, and a report naming everything mapped, dropped, or degraded, nothing silent. See docs/migrating-from-beads.md for the field mapping and what does and doesn't carry over.
Contributions are welcome. Start with CONTRIBUTING.md, and please read the spec first — it records which ideas were considered and declined, so you can tell a gap from a deliberate omission.
katra owes ideas to beads, tk, beans, and aweb. The spec's §14 explains what each got right and where katra diverges.
MIT © itsacoyote