Git-native tickets, notes, and code review. One binary, zero dependencies beyond git. Storage lives in refs/notes/maitake β invisible to your working tree, pushed only where you choose.
go install github.com/cygnusfear/maitake/cmd/mai@latestmai init --remote origin --block github.com # set up hooks + sync config
mai ticket "Fix auth race" -p 1 -l auth --target src/auth.ts
mai start mt-5c4a
mai add-note mt-5c4a --file src/auth.ts --line 42 "Race condition here"
mai context src/auth.ts # see everything about a file
mai close mt-5c4a -m "Fixed with mutex"Every write auto-pushes to the configured remote. Every note records the current git branch. Everything is JSON, append-only, and mergeable.
Each ticket, warning, review finding, or comment is one JSON line in a git note. Nothing is mutated β state is computed by folding events:
{"id":"mt-5c4a","kind":"ticket","title":"Fix auth race","branch":"main","timestamp":"..."}
{"kind":"event","field":"status","value":"in_progress","branch":"feature/auth","timestamp":"..."}
{"kind":"comment","body":"Found root cause","branch":"feature/auth","timestamp":"..."}
{"kind":"event","field":"status","value":"closed","branch":"main","timestamp":"..."}
Closed from main β the branch was merged. The event stream tells the story.
| Command | What |
|---|---|
mai ticket [title] [opts] |
Ticket (task by default) |
mai warn <path> [message] |
Warning on a file |
mai review [title] [opts] |
Code review (open, needs response) |
mai artifact [title] [opts] |
Record/output (born closed β ADRs, research, mid-mortems) |
mai create [title] [opts] |
Any kind β use -k |
Options: -k kind, -t title, --type type, -p priority, -a assignee, -l a,b (tags), --target path, -d description
Git-native PRs β no GitHub, no Forgejo, no platform lock-in. Stored as kind: pr notes.
# Create (from a feature branch)
mai pr "Add auth middleware" --into main # β mai-5c4a feature/auth β main
mai pr # list open PRs (auto-closes merged ones)
# Inspect
mai pr show <id> # details + diff summary + review verdict
mai pr show <id> --diff # include full inline diff
mai pr diff <id> # full diff between source and target
mai pr diff <id> --stat # summary only
# Review
mai pr accept <id> [-m message] # LGTM (resolved comment)
mai pr reject <id> -m 'reason' # request changes (unresolved comment)
mai pr comment <id> -m 'msg' # general comment
mai pr comment <id> -m 'msg' --file <path> --line N # inline comment
# Merge
mai pr submit <id> # merge source β target, close PR
mai pr submit <id> --force # skip unresolved comment checkPRs that are merged outside mai (via git merge, GitHub, etc.) auto-close when listed.
mai start <id> # β in_progress
mai close <id> [-m message] # β closed
mai reopen <id> # β open
mai add-note <id> [text] # comment
mai add-note <id> --file <path> [text] # file-level comment
mai add-note <id> --file <path> --line N [text] # line-level comment
mai tag <id> +tag / -tag # add/remove tag
mai assign <id> <name> # set assignee
mai dep <id> <dep-id> # add dependency
mai undep <id> <dep-id> # remove dependency
mai link <id> <id> # symmetric link
mai unlink <id> <id> # remove linkmai show <id> # full state with comments
mai ls # open + in_progress (work queue)
mai ls --status=all # everything
mai ls -k warning # filter by kind
mai closed # recently closed
mai context <path> # everything targeting a file
mai ready # unblocked work
mai blocked # stuck on deps
mai dep tree <id> # dependency graph
mai kinds # all kinds in use
mai doctor # graph healthmai --json ls # JSON array of summaries
mai --json show <id> # JSON state with events + comments
mai --json context <path> # JSON array of states
mai -C /path/to/repo --json ls # query a different repomai init [--remote R] [--block H] # hooks + config + .gitignore
mai sync # manual fetch + merge + push
mai migrate [--dir .tickets/] [--dry-run] # import tk ticketsmai init --remote forgejo --block github.comThis creates three things:
.maitake/hooks/pre-writeβ scans notes for secrets before every write (gitleaks with regex fallback).maitake/configβ sync remote + blocked hosts.gitignoreentry β keeps.maitake/out of the repo
remote forgejo
blocked-host github.com
blocked-host gitlab.com
Every write auto-pushes refs/notes/maitake to the configured remote. On conflict: fetch + set-union merge + retry. Push failures warn but never block.
Manual sync pulls remote changes:
mai sync # fetch + merge + pushHooks live in .maitake/hooks/ (per-repo) or ~/.maitake/hooks/ (global fallback). Per-repo wins when both exist.
| Hook | When | Receives |
|---|---|---|
pre-write |
Before every note write | JSON note on stdin |
post-push |
After every successful auto-push | MAI_REMOTE, MAI_REF, MAI_REPO_PATH env vars |
Exit non-zero from pre-write to reject the write. post-push failures warn but don't block.
# Secret scanning (installed by default with mai init)
cp examples/hooks/pre-write-gitleaks .maitake/hooks/pre-write
# Sync to GitHub Issues (requires gh CLI)
cp examples/hooks/post-push-github .maitake/hooks/post-push
# Sync to Forgejo Issues (requires curl + jq)
cp examples/hooks/post-push-forgejo .maitake/hooks/post-pushSet up once for all repos:
mkdir -p ~/.maitake/hooks
cp examples/hooks/pre-write-gitleaks ~/.maitake/hooks/pre-write
cp examples/hooks/post-push-github ~/.maitake/hooks/post-push
chmod +x ~/.maitake/hooks/*Every repo gets these unless it provides its own.
Comments can target specific files (and lines) within a ticket:
mai ticket "Auth hardening" --target src/auth.ts --target src/http.ts
mai add-note mt-5c4a --file src/auth.ts "Add mutex around token refresh"
mai add-note mt-5c4a --file src/http.ts --line 15 "Missing backoff"mai context src/auth.ts shows the ticket and only auth.ts comments β not http.ts comments. Review agents leave findings on files, fix agents see exactly what to address.
Every JSON event records the git branch at write time. No flags needed β it's automatic.
{"kind":"ticket","title":"Fix auth","branch":"feature/auth","timestamp":"..."}
{"kind":"event","field":"status","value":"closed","branch":"main","timestamp":"..."}Closed from main tells you the feature branch was merged.
The index caches in ~/.maitake/cache/, keyed by the notes ref tip SHA. Cache invalidates automatically on every write. Cold start reads from git; warm start skips all git round-trips.
mai artifact creates notes with type: artifact β born closed. They don't appear in mai ls or mai context unless you query with --status=all. Use for ADRs, research results, oracle findings, mid-mortems, and other records that aren't active work.
Reviews (mai review) are open by default β they need a response.
mai migrate --dir .tickets/ # import all tickets
mai migrate --dir .tickets/ --dry-run # preview without writingPreserves original IDs, timestamps, deps, links, parent refs, Forgejo issue numbers, and comments. Old-format files without YAML frontmatter are skipped.
Notes refs don't push by default β git ignores them. Only the remote configured in .maitake/config receives notes. Blocked hosts are checked before every push.
- Event-sourced β immutable JSON lines, state computed by folding
- Append-only β changes via events, never mutation
- Set-union merge β
cat | sort | uniqresolves conflicts (inherited from git-appraise) - Kind-agnostic β tickets, warnings, constraints, decisions, reviews are all notes with different
kindfields - Performance β 10,000 notes: index build <20ms, query <1ms. Cache eliminates git reads on warm start.
- openprose/mycelium β git notes substrate
- google/git-appraise β code review on git notes (Apache 2.0, repository package adapted)
