Skip to content

Repository files navigation

TodoMD logo

TodoMD

A Kanban TUI and agent-friendly CLI over a plain markdown TODO.md.

demo

The markdown file is the single source of truth — readable and diffable for humans, renderable on GitHub, and deterministic for the tool. Humans use the TUI; AI agents drive the CLI (--json everywhere, stable task IDs, meaningful exit codes).

📦 Install

macOS or Linux — installs (and updates) the latest release to ~/.local/bin, verifying the checksum:

curl -fsSL https://raw.githubusercontent.com/walm/todomd/main/install.sh | sh

Or grab a binary from the releases page, or build from source:

go install github.com/walm/todomd@latest   # or: go build -o todomd .

todomd --version prints the version.

Upgrading

todomd upgrade          # download the latest release and replace this binary
todomd upgrade --check  # just report what's available

It verifies the release's published sha256 before swapping the binary, and leaves the old one in place if anything fails. If todomd lives somewhere you can't write, it says so rather than half-upgrading. (Re-running the install script above works too, and is the right move for a source build.)

todomd mentions a newer release only where a human is looking: in the TUI footer and at the end of todomd --help (on stderr, so --help output itself is unchanged). No other command ever prints it, so scripts and agents are unaffected. The check runs at most once a day, in the background, from a cached answer — and TODOMD_NO_UPDATE_CHECK=1 turns it off entirely.

🚀 Quick start

todomd init                 # creates TODO.md with Backlog / In Progress / Done
todomd                      # opens the Kanban TUI

🤖 CLI (for agents and scripts)

todomd add "Fix the parser" --tag parser --priority high --due 2026-08-01 --json
todomd list --priority high --json    # what to work on first
todomd list --json                    # everything, grouped by board
todomd show 3f2a --json               # full task detail
todomd update 3f2a --title "New" --priority low --tag a --clear-due
todomd move 3f2a --to "In Progress" --pos 1
todomd done 3f2a
todomd comment 3f2a --author ai "Tried X, going with Y."
todomd delete 3f2a --yes
todomd boards --json
todomd boards delete Review           # --force if it still holds tasks
todomd archive --dry-run              # what clearing Done would remove
todomd changes --as claude --ignore-author claude --json

🔄 Change tracking for agents

todomd changes answers "what happened since I last looked" without the agent having to read or diff the whole file. Each consumer names a cursor with --as; reading advances it (--peek doesn't). The first call just initializes the cursor. Because it diffs snapshots of the file (stored under $XDG_STATE_HOME/todomd, default ~/.local/state/todomd, keyed by the file's path), it catches every source of change: CLI, TUI, $EDITOR, hand edits, formatters, git pulls.

Events: task_added (includes the full task as detail), task_deleted, task_moved (from/to), task_updated (fields with old/new per changed field — renames stay the same task, identity is the ID), comment_added (the comment). Reorders within a board are not reported.

A consumer is never notified about its own writes. Pass the same --as name when you write (or set TODOMD_CURSOR once) and that write is folded into your own cursor, so it isn't reported back to you — while everyone else's cursor still sees it. Only what you changed is skipped: if a human comments on the very task you just moved, you still get that comment.

The typical agent loop:

export TODOMD_CURSOR=claude          # or pass --as claude to each call
todomd move 3f2a --to "In Progress"  # mine: won't come back as an event
todomd comment 3f2a --author claude "done, please review"
# …later…
todomd changes --json                # only what others did
  • File resolution: --file flag > TODOMD_FILE env > TODO.md searched from the cwd upward (stopping at the repo root).
  • IDs are stable 4-char base36 (they survive renames and moves) and may be abbreviated to any unique prefix. Deleted IDs may be reused later — don't hold IDs across deletions.
  • Priority is high, normal (the default) or low, on add/update and as a list --priority filter. It's advisory: the suggested convention is to work High first, then Normal, then Low — todomd records it and leaves the ordering to you. "priority" is always present in JSON, so an agent never has to infer the default.
  • Exit codes: 0 ok · 1 general error · 2 task not found · 3 ambiguous ID prefix.
  • Boards are matched case-insensitively and created on demand (new boards land before Done). boards delete <name> removes one; an empty board goes straight away, one holding tasks needs --force because its tasks go with it.
  • Concurrent invocations are safe: every write takes an advisory lock (kept in ~/.local/state/todomd, never next to your file) and replaces the file atomically.
  • Any text is safe to pass — titles reject newlines, and description/comment lines that would read as file structure are escaped on write and unescaped on parse. The one restriction: multi-line text must not contain an unclosed
    
    

⌨️ TUI

Keys Action
h/l j/k Navigate columns / cards (g/G first/last)
H/L Move task to previous / next board
J/K Reorder task within its board
Enter Open task detail (modal over the board; q/esc back)
a / e Add / edit task (tab next field, ctrl+s save, esc cancel)
E Edit the task as markdown in $VISUAL/$EDITOR (title, tags, due, description, comments)
p Cycle priority (normal → high → low)
(in the add/edit form, priority is a / select)
c Comment on task
d / D Delete task (confirm) / move to Done
X Delete the current board (asks first if it holds tasks)
A Mark every card as read (clear all badges)
/ Search: filters as you type (enter keeps it, esc cancels)
u Show only changed/unread cards
esc Clear the active filter
r Reload from disk
? / q Toggle help / quit

Inside the open task: j/k scroll, ctrl+d/ctrl+u half a page, g/G jump to the top and bottom, and the title, id, board, tags, priority and due date stay pinned above the scrolling body. e, E, c and p work there too and return you to the task afterwards.

🖱️ Mouse works alongside the keys: click a card to select it, click it again to open; inside the open task the footer hints (e edit · E editor · c comment) are clickable and tapping outside the card closes it. Forms use the same style (ctrl+s save · esc cancel), and on the board footer a add, ? help, and q quit are clickable too — every actionable label underlines on hover. Column headers select their column, and the wheel scrolls (cards on the board, text in the open task). Terminals need shift-click to select text for copying while mouse mode is on.

🔔 Unread badges

The TUI tracks what changed since you last looked (its own tui change cursor): cards added by someone else show with a green border, cards updated/moved/commented show with a yellow border, and the status line counts them on startup. Opening a card marks it read, A marks the whole board read at once; your own actions never badge; unread state persists across sessions.

While you idle on the board, the TUI auto-reloads: it stats the file every 2s and refreshes (badging changed cards, keeping your selection) whenever the file actually changed — so agent activity appears live. Auto-reload pauses while a task, form, or confirm prompt is open; r still forces a reload any time.

The TUI auto-detects light/dark background at startup. Set GLAMOUR_STYLE (dark, light, notty, …) to pin the theme and skip the terminal query — useful for terminals that don't answer OSC color queries.

🌐 Web UI

Prefer a browser? todomd-web is a separate application serving the same board over HTTP — one Go binary, no database, the same TODO.md as the source of truth.

📝 The file format

# TODO

Free-form preamble — never touched by the tool.

## Backlog

### Implement markdown parser
<!-- id:3f2a -->
`#parser` `#core` **priority:** high **due:** 2026-08-01

Description: any markdown, multiple paragraphs, code fences, lists.

#### Comments

- **ai** (2026-07-18): Considered goldmark; hand-rolled round-trips better.
- **user** (2026-07-18): Agreed.

## In Progress

## Done

Rules, briefly:

  • ## headings are boards (column order = file order); ### headings are tasks (card order = file order). Empty boards persist.
  • The <!-- id:… --> comment is the task's stable ID (invisible when rendered). Hand-added tasks without one get an ID on the next write.
  • The metadata line (tags, priority, due) is the first non-blank line below the ID comment (formatter-inserted blank lines are fine). **priority:** is high or low; the default normal is left out, so ordinary tasks stay clean.
  • Everything up to #### Comments / the next heading is the description, preserved verbatim.
  • Comments are one list item each: - **author** (YYYY-MM-DD): text, with continuation lines indented two spaces.
  • Hand-editing is fine; the tool re-canonicalizes spacing on its next write and reports malformed content with line numbers.

⚠️ A board, not a database

todomd is built for the active work of a project — a few boards, dozens of tasks. It is not meant to hold hundreds of tasks: a board you can't read in one glance has stopped being a board.

  • Delete Done tasks once they've been reviewedtodomd delete <id> --yes (or d in the TUI). Agents should clear confirmed Done items as part of their loop instead of letting the board grow.
  • todomd archive clears the whole Done board in one go. Because that is bulk and destructive it confirms first, and it refuses unless the tasks would survive somewhere: either the file is committed to git (so git log -p TODO.md still has them) or you pass --to FILE to move them into another markdown board. --dry-run previews, --force overrides the git check, --yes skips the prompt (required when not on a terminal).
  • Archive and history are git's job. Keep TODO.md in version control and nothing is ever lost: every task, move, and comment lives in the file's history (git log -p TODO.md), which beats any archive board.

🏷️ Releasing

todomd follows semver (0.x: minor bumps may break — called out in CHANGELOG.md). Versions come from git tags; goreleaser stamps the binary. To cut a release:

  1. Add a ## vX.Y.Z section to CHANGELOG.md and commit.
  2. git tag vX.Y.Z && git push origin main --tags

The release workflow runs the tests, cross-builds darwin/linux (amd64 + arm64) archives, and publishes a GitHub release.

📄 License

MIT — free to use however you like; please keep the notice, which points back to this repo as the source.

About

Kanban TUI and agent-friendly CLI over a plain markdown TODO.md

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages