ait is a small, local-first issue tracker built primarily for coding agents. Heavily inspired by Steve Yegge's beads project. Just pruned down to the essentials I need.
It is intended to help an agent turn a plan into structured work, track dependencies, preserve notes between sessions, and quickly answer a practical question: what should I do next?
Repository: https://github.com/ohnotnow/agent-issue-tracker
Sibling projects:
- web-ait — web UI for monitoring agent progress
- ant — agent-first note/ADR/"why" tracker to compliment ait's "what"
This project is working - but use at your own risk. It's been used on real projects, but no guarantees are made about API stability or compatibility. However, of note - it's effectively feature complete. The project has a fairly tight scope - and all expected features are implemented. If you need something 'fancier' - then look for another tool.
Schema changes are managed through a forward-only migration system, so existing databases are upgraded automatically on startup.
Prebuilt binaries for Linux, macOS and Windows (amd64 and arm64) are attached to every release, so you don't need Go installed. The releases/latest URL always points at the newest release, so you can fetch it without knowing the version number:
curl -fsSLO https://github.com/ohnotnow/agent-issue-tracker/releases/latest/download/ait-linux-amd64
curl -fsSLO https://github.com/ohnotnow/agent-issue-tracker/releases/latest/download/SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
chmod +x ait-linux-amd64
sudo mv ait-linux-amd64 /usr/local/bin/aitThe files are named ait-<os>-<arch>: swap in linux, darwin or windows, and amd64 or arm64 (Windows files end in .exe). On macOS use shasum -a 256 -c --ignore-missing SHA256SUMS for the check.
Once installed, ait self-update keeps it current (see Self-update).
If you do have Go, go install github.com/ohnotnow/agent-issue-tracker/cmd/ait@latest also works.
ait comes with a set of skills and sub-agents. You can install them (by default into ~/.claude/) by running ait install-skills. See Skills and agents for more details.
The tool is optimized for agent workflow first:
- create initiatives, epics, and tasks
- model dependencies
- store progress notes
- claim issues to coordinate between multiple agents
- resume work after session loss or conversation compaction
- surface unblocked work via
ready, ordered by priority - allow delegation of work to sub-agents
Human-friendly output is intentionally secondary for now. JSON is the default interface.
There are three issue types, forming a natural hierarchy:
initiative— the strategic "why". Captures the vision, goals, and key decisions behind a group of related epics. Useful as a reference point when an agent needs to make a judgement call during implementation. Initiatives are always top-level (no parent).epic— a container for related tasks. Can be top-level or a child of an initiative.task(default) — a unit of work. Always a child of an epic (or another task for subtasks).
ait create --title "Auth overhaul" --type initiative --priority P0
ait create --title "OAuth Epic" --type epic --parent <initiative-id>
ait create --title "Login page" --parent <epic-id>
ait create --title "Login page" --description @spec.md # description from fileThe hierarchy is reflected in the ID structure: proj-abc (initiative) -> proj-abc.1 (epic) -> proj-abc.1.1 (task).
The tool is not a replacement for a real issue tracker. The workflow is envisioned as 'developer has a plan/issues/feature - gets the coding agent to plan them out (or does it themselves), then the actual coding agent manages the sub-epics/issues for that work alone.
It's not designed to handle cross-team shared issues, work, projects. The internal database lives in .ait/, which is created by an explicit ait init — every other command refuses with an uninitialised error (exit code 1) until then, so a stray ait list in the wrong directory never leaves a database behind. In a git repository, init also adds .ait/ to your .gitignore; outside one, its JSON output carries a note saying the .gitignore step was skipped.
initconfigcreateshowlist(--type,--status,--priority,--parent,--all,--long,--human,--tree)statussearchupdate(--title,--description,--status,--priority,--parent)close(--cascade,--note,--reason— alias for--note)reopencanceldelete(--force,--cascade)claimunclaimready(--type,--long)flush(--dry-run,--summary)log(--last,--since,--search,--long)log purge(--keep,--before,--full)dep adddep removedep listdep treenote addnote listversionself-update(--check,--yes)install-skills(--dry-run,--force,--dir)export(--output)completion(bash,zsh)
Every subcommand supports --help (or -h) for command-specific usage, flags, and examples:
ait list --help
ait dep add --help
ait create -hTab completion is available for bash and zsh. It completes subcommands, flags, flag values (status, type, priority), and issue IDs.
Bash — add to ~/.bashrc or ~/.bash_profile:
eval "$(ait completion bash)"Zsh — add to ~/.zshrc:
eval "$(ait completion zsh)"Restart your shell or source the file, then use Tab to complete:
ait <Tab> # subcommands
ait list --<Tab> # flags
ait show <Tab> # issue IDs
ait list --status <Tab> # status valuesThe search command matches against issue titles and descriptions, case-insensitively:
ait search "auth" # matches "Auth Flow", "AUTH_TOKEN", etc.By default, queries (list, ready) and mutations (create, update, close, cancel, reopen, claim, unclaim) return a slim view with only the fields an agent typically needs: id, title, status, type, and priority. This keeps token usage low when chaining commands or running them in batches.
Pass --long to get the full issue record including description, parent_id, claimed_by, timestamps, and closed_at.
The relation mutations dep add, dep remove, and note add return a slim ack ({ok: true, ...ids}) by default. Pass --long to get the full blocker list (dep add/dep remove) or the full Note record (note add).
list also includes a hidden_count field in its JSON response when the default filter is active, telling you how many closed/cancelled issues are being filtered out — handy when an empty-looking response would otherwise be confusing. The field is omitted when --all or an explicit --status is passed.
For human-friendly output, two display modes are available:
--human— compact tabular view with initiatives, epics, and children grouped, child IDs shown as short suffixes (.1,.2)--tree— parent-child hierarchy using tree connectors (├──,└──), full IDs on every line
These are mutually exclusive and can be combined with the usual filters (--type, --status, --priority).
ait list # slim JSON (5 fields per issue) + hidden_count
ait list --long # full JSON record
ait list --all # include closed/cancelled, no hidden_count
ait list --human # compact tabular view
ait list --tree # tree hierarchy view
ait list --human --priority P1 # filtered tabular view
ait ready --type task # slim, tasks only (excludes epics)
ait ready --long # full record, all types
ait create --title X # slim ref back
ait create --title X --long # full Issue backWhen multiple agents share one tracker, claiming prevents duplicate work:
ait claim <id> <agent-name> # mark an issue as being worked on
ait unclaim <id> # release the claimIf an issue is already claimed by another agent, claim returns a conflict error with the current holder's name. Claims are visible in show output via claimed_by and claimed_at fields.
The --description flag on create and update supports reading from a file using the @file convention (familiar from tools like curl). This avoids shell escaping problems with long descriptions containing quotes, backticks, or newlines:
ait create --title "Auth overhaul" --description @design-doc.md
ait update <id> --description @updated-spec.txtIf the value starts with @, the remainder is treated as a file path and the contents are used as the description. A bare - reads the description from stdin, so a heredoc works (matching ant --body -):
ait update <id> --description - <<'EOF'
Rewritten body, apostrophes and `backticks` fine.
EOFWithout either, the value is used as a literal string as before.
Both guards exist because an agent once wiped an issue body by piping an empty heredoc into an update, and another sent a diff where the whole text was expected.
update --description ""(or@fileon an empty file) is refused rather than blanking the body.- A new description less than half the length of the existing one is refused with a
shrinkerror, unless you pass--force. Growing a description is never questioned. Bodies under 200 characters are exempt. - When
CLAUDE_CODE_SESSION_IDis set (Claude Code sets it for every command an agent runs),showrecords the session and time against the issue.update --title/--description/--humanandclose/cancel --notethen refuse with anunreaderror unless that session ranshowon the issue within the last hour. The error says which it was: never shown, shown by another session, or shown too long ago. A plain terminal has no session id, so humans are never checked. The override is deliberately long:
ait update <id> --description @rewrite.md --dangerously-skip-read-checkThe hour is a constant (ShownWindow in internal/ait/guards.go), chosen so that a session whose context has been compacted since it last looked cannot keep editing on a stale memory of the issue.
The close command accepts an optional --note flag that attaches a closing note to the issue before closing it, leaving a breadcrumb for why something was closed:
ait close <id> --note "Superseded by new approach"This is equivalent to running ait note add <id> "Closed: ..." followed by ait close <id>, but in a single command. It can be combined with --cascade.
--reason is accepted as an alias for --note for backwards compatibility.
By default, close only affects the specified issue. To close an initiative or epic and all of its descendants in one operation:
ait close <id> --cascadeThis recursively closes all open or in-progress children and grandchildren. Issues that are already closed or cancelled are skipped. The command returns the list of newly closed issues.
The flush command permanently deletes all closed and cancelled issues to keep the database lean. Since ait tracks ephemeral work, there is no need to keep completed issues around indefinitely.
ait flush # delete all terminal issues
ait flush --dry-run # preview what would be deletedFlush only removes root-level issues whose entire descendant tree is also closed or cancelled. If a closed initiative or epic still has open or in-progress children, it is skipped and reported in the skipped list. Notes and dependencies belonging to flushed issues are removed automatically via cascade delete.
The --summary flag lets you attach an editorial note to the flush, giving future sessions a quick description of what was accomplished:
ait flush --summary "Fixed pg case-sensitivity in searches, added API docs"Before deleting, flush records every flushed issue into a history log stored in the same SQLite database. This gives agents a way to look back at what was done in previous sessions without keeping the issues themselves around.
ait log # summary view: date, summary, root items, item count
ait log --long # full detail: all items with parent IDs and close reasons
ait log --last 5 # most recent 5 flush events
ait log --since 2026-04-01 # flushes since a date
ait log --search "migration" # find items by title or close reason
ait log --search "auth" --long # search with full detailBy default, log returns a slim view: each flush entry shows its date, summary, total item count, and only root-level items (initiatives, standalone epics and tasks). This follows the same slim/long pattern as list and ready. Use --long to see all items including children and their close reasons.
The --search flag filters items by title or close reason (case-insensitive). Flush entries with no matching items are excluded. This is useful when a user mentions past work and you need to find the relevant history.
Over time the history log can grow large. The log purge subcommand compacts it:
ait log purge --keep 20 # compact all but the last 20 entries
ait log purge --before 2026-01-01 # compact entries older than a date
ait log purge --keep 10 --full # fully delete old entriesBy default, purge compacts — it removes the per-issue item records but keeps the summary rows (date, summary text). This preserves the timeline of what happened while reclaiming storage. Use --full to delete entries entirely.
The export command produces a self-contained Markdown briefing for an issue and all its descendants. This supports a lightweight delegation workflow for handing work to sub-agents that don't have access to the ait binary, or don't need to know about it at all.
ait export <id> # print Markdown to stdout
ait export <id> --output briefing.md # write to fileFor an epic or initiative, the output includes the title, ID, priority, description, a checklist of children ordered by priority, dependencies, notes, and a summary with counts. The resulting file is also useful as a human-readable report of current state.
The delegation workflow is straightforward:
- Export an epic as a Markdown briefing
- Delegate the file to a sub-agent — in a worktree, background process, or remote context
- The sub-agent works through the checklist — no tracker needed
- The supervisor agent reconciles the results back into the tracker by closing completed tasks
This keeps the contract in plain Markdown, so it works across context boundaries, and doesn't couple the receiving agent to any tooling. For the full workflow with worked examples, see claude/skills/ait/DELEGATION.md.
The ready command surfaces unblocked issues ordered by priority (P0 first, then P1, P2, etc.), with creation order as a tiebreaker within the same priority level. This means the most urgent actionable work appears first.
Use config to check the current project settings without inspecting the database directly:
ait configReturns the current prefix and schema_version as JSON.
When adding a dependency with dep add, the tool performs a transitive reachability check. If the new dependency would create a cycle (e.g. A depends on B, B depends on C, and you try to make C depend on A), the command is rejected with a validation error.
Run ait init once per project to create the database — until then, every other command returns an uninitialised error pointing you at it. Use --prefix <value> to set the project prefix used for public issue IDs (it defaults to the project directory name).
Errors are emitted as a JSON {"error": {"code", "message"}} envelope on stderr with a non-zero exit code — stdout only ever carries data. This matches ant, so scripts and agents can treat the two tools identically.
Examples:
ait init --prefix aitait init --prefix deliveries
If no prefix has been set yet, the tool will infer one automatically the first time you use it by normalizing the current project directory basename.
The prefix is stored in local project configuration inside the SQLite database. Running init --prefix ... later will update the stored prefix and re-key existing public issue IDs to match.
Public issue IDs are hierarchical:
- root issue:
<prefix>-<sqid> - first child:
<prefix>-<sqid>.1 - first grandchild:
<prefix>-<sqid>.1.1
This makes parent-child structure visible directly in the identifier while keeping the root segment compact and readable.
A child keeps its number for life: deleting a sibling leaves a gap rather than renumbering the others. Moving an issue with ait update <id> --parent <new-parent> (or --parent "" to make it a root) gives it and its children new IDs that match their new place. The old IDs are kept as aliases, so references in notes and commit messages still work: ait show <old-id> returns the issue with a moved_from field set to the ID you asked for. A freed number is never handed to a new issue.
By default, the database is stored at .ait/ait.db in the current git repository root. You can override this with the --db flag:
ait --db /path/to/other.db list
ait --db /path/to/other.db create --title "Task in another DB"This is useful for git worktrees (pointing back to the main repo's database), keeping separate databases for different subsystems, or using :memory: for testing.
The database schema is managed through a forward-only migration system. Each migration is numbered and runs in its own transaction, recording its version as it commits, so an interrupted upgrade carries on from the last step that finished. On startup, the tool checks the current schema version and applies any pending migrations automatically.
This means you can update the ait binary and your existing database will be upgraded transparently — no manual steps required. The current schema version is visible via ait config.
The tool uses SQLite and creates a local database at .ait/ait.db in the current git repository root (or the current directory if no git root is found). Each time you run ait init inside a git repository, it makes sure .ait/ is in the project's .gitignore so the database isn't committed, adding the entry back if it has gone missing. It only does this for the default location, and leaves an existing entry (.ait, .ait/, /.ait or /.ait/) as you wrote it.
That database stores:
- issues, dependencies, and notes
- project-level configuration such as the current public ID prefix
This keeps issue state close to the codebase it belongs to and makes it easy to inspect or back up.
ait carries the Claude Code skills, agents and command that go with it, built into the binary, so a binary and its skills always come from the same release:
skills/ait: core command reference, workflow patterns, and how to turn a plan into issues (SKILL.md), plus a guide to delegating work to sub-agents via Markdown export (DELEGATION.md)skills/ait-recap: a friendly recap of recent ait activity, for one project or a folder of themskills/ait-expand-issue: turns a terse "note to self" issue into a plan document ready forplan-to-ait, listing the assumptions it madeskills/ait-feature-workflow: a session workflow for feature work: orient withait ready, plan in plan mode, create issues withplan-to-ait, then implement against each issue's acceptance criteriaagents/plan-to-ait: converts plan-mode plans into structured ait initiatives, epics and issuesagents/ait-audit: reports open issues that look finished but were never closedagents/ait-amnesia-check: reads new issues with no conversation context and shows what it would build from each, so you can see what a spec failed to saycommands/hello-ait: example session-start command that reads the README and checks for outstanding work. It's a starting point; edit it to suit your own style.
The sources live in this repo's claude/ directory. To install or update them:
ait install-skills # copy them into ~/.claude
ait install-skills --dry-run # only say what would change
ait install-skills --force # replace changed files without asking
ait install-skills --dir ~/work/.claudeA missing file is added and an identical one left alone. If you've edited your installed copy, install-skills asks before replacing it ([y/N/d], where d shows the diff). Run without a terminal, as an agent would, it never asks: changed files are left as they are and listed.
To keep them current: ait version says whether the installed copies match this binary's, and after an update ait self-update reminds you to run ait install-skills --dry-run to see whether the skills changed too.
install-skills never deletes anything. A skill that ait no longer ships stays installed until you remove it yourself; for example, ait-crafting was retired in favour of the ait skill's "Turning a Plan into Issues" section plus the general spec-crafting skill, so delete ~/.claude/skills/ait-crafting/ if you have it. Likewise expand-issue and feature-workflow, which used to come from the agentic-stuff repo, are now ait-expand-issue and ait-feature-workflow here; remove the old copies from ~/.claude/skills/.
plan-to-ait works best alongside spec-crafting, which ait doesn't ship. That skill, and other related skills and agents that don't need ait, are in the agentic-stuff repo.
The skills use the shared SKILL.md format, so they work in other agents too. --dir is the root the whole claude/ tree is copied under (skills/, agents/, commands/), so point it at the other tool's config folder.
OpenCode already reads skills from ~/.claude/skills, but looks for agents in its own folder:
ait install-skills --dir ~/.config/opencodeThat puts the agents in ~/.config/opencode/agents/. It also puts a second copy of the skills in ~/.config/opencode/skills/, which OpenCode handles fine. The agents' frontmatter is written for Claude Code (model: opus, tools: Bash, Read, ..., skills: [ait]), so you may want to adjust it to OpenCode's own conventions.
Codex reads skills from ~/.codex/skills:
ait install-skills --dir ~/.codexThe skills work as they are. Codex agents are TOML files (~/.codex/agents/<name>.toml), not Markdown, so the agents land in ~/.codex/agents/ as .md files that Codex won't pick up until you convert them by hand:
nameanddescriptioncome straight from the frontmatter- the Markdown body (everything after the closing
---) becomesdeveloper_instructions, in a"""string - drop
model,toolsandskills; Codex has no way to preload a skill, so add a line at the top of the instructions telling the agent to use theaitskill plan-to-aithas a section about Claude Code's permission prompts ("one Bash call per response"), which you can delete
For example, ait-audit becomes:
name = "ait-audit"
description = "Audits open ait issues against the codebase. Reports which issues appear complete but aren't closed. Does NOT close issues - reports findings for human review."
developer_instructions = """
Use the ait skill for the ait command reference.
# Ait Audit Agent
...
"""Two things to know with either tool. ait version only checks ~/.claude, so it will report the skills as not installed if you only installed them somewhere else. And commands/hello-ait.md is a Claude Code slash command; it gets copied, but the other tools may not do anything with it.
You might also want to allow the agent to run ait and use the ait skill without needing permission. For claude code for example, in ~/.claude/settings.json :
{
"permissions": {
"allow": [
"Bash(ait *)",
"Skill(ait)"
]
}
}Released binaries can update themselves in place:
ait self-update # interactive: shows release notes, prompts y/N
ait self-update --yes # skip the prompt
ait self-update --check # report whether an update is available; exit onlyExit codes for --check mirror the composer outdated style — 0 if you are
already on the latest, 1 if a newer release is available, 2 if the lookup
failed (e.g. offline). The command verifies the downloaded binary against the
release's SHA256SUMS before swapping it into place atomically. It then
checks whether the new release's skills differ from your installed ones (see
Claude Code Skills and Agents).
If ait was installed via Homebrew or go install, self-update declines and
points you at the right tool. Dev builds (Version == "dev") are also a no-op
— rebuild from source instead.
To run the test suite:
GOCACHE=$(pwd)/.gocache go test ./...To build the binary:
GOCACHE=$(pwd)/.gocache go build -o ait ./cmd/aitRelease builds inject the version and repository URL at compile time via ldflags. For development builds, the version defaults to dev and the update check is skipped.
If you fork this repository and want the update check to point at your own releases, either update the RepoURL default in internal/ait/version.go or pass it via ldflags:
go build -ldflags "-X github.com/ohnotnow/agent-issue-tracker/internal/ait.Version=v0.1.0 -X github.com/ohnotnow/agent-issue-tracker/internal/ait.RepoURL=https://github.com/youruser/yourfork" -o ait ./cmd/aitIf you'd like a web UI to monitor the progress of your agent - there is a sibling project at ohnotnow/web-ait
There is a technical overview document that explains the internals of the tool.