Thanks for taking a look. ike is small and deliberately stays that way, so the most useful contributions are usually bug fixes, a sharper error message, or a test that pins something currently untested.
You need Go 1.25 or newer. Everything else is in the module.
go build ./...
go test ./...
go test -race ./... # what CI enforces
go vet ./...
gofmt -l . # must print nothingLinting matches CI:
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 run ./...Always point IKE_DATA_FILE at a scratch file when trying things out, or
you will be editing your own real matrix:
IKE_DATA_FILE=$(mktemp -d)/t.json go run . listIt must be an absolute path whose parent directory exists.
To build a binary that reports a real version rather than dev, stamp it the
way the release workflow does:
go build -ldflags "-X github.com/jonascript/ike/internal/cli.version=$(git describe --tags --always)" -o ike .The demo GIF in the README is generated from a checked-in script, so it can be regenerated when the UI changes rather than re-recorded by hand:
brew install vhs # https://github.com/charmbracelet/vhs
vhs assets/demo.tapeIt builds ike from the working tree and seeds a throwaway matrix in a fresh
mktemp -d, so recording never touches your own data file.
Dependency direction is cli → tui/mcpserver → store → task.
internal/task— the domain. Zero dependencies beyond stdlib: theTaskstruct, quadrants 1–4, andLess/SortOrder, which is the single definition of display order.internal/store— all business logic lives inops.go. Add, Complete, Restore, Move, Reorder, Rename, Delete, SetQuadrantLabel, Undo, Redo, List, ListArchive.internal/tui,internal/cli,internal/mcpserver— three frontends that must stay thin wrappers over the store.
If you find yourself putting logic in a frontend, it probably belongs in
ops.go so all three get it. CLAUDE.md documents the architecture in more
detail, including the reasoning behind the parts that look like they could be
simplified.
These are load-bearing, and each exists because the alternative broke something. Tests pin all of them, so you will find out — but it saves time to know up front.
- Every mutation goes through
Store.Mutate. It locks a sidecartasks.json.lock(locking the data file itself cannot work — the atomic rename replaces the inode), re-reads the file inside the lock, applies the change, then writes via a temp file and rename. Never write the data file any other way. - The re-read inside the lock is what prevents lost updates, and returning early on a read error is what stops a corrupt file being overwritten with stale in-memory state. Keep that ordering.
- Writes are
0600, the directory0700, the temp file comes fromos.CreateTemp(never a predictable name — a symlink at a guessable path can redirect the write), and the data is fsynced before the rename. Labels.Of(q)is the only correct way to render a quadrant name. Never printq.Label()in a frontend; that is the default name and ignores a user's rename.- Render user text through
Task.DisplayTitle()andLabels.Of(). They strip control characters. A title containing an escape sequence can otherwise repaint the terminal, soike listwould show something other than what is stored.--jsondeliberately keeps raw values, sinceencoding/jsonescapes them. pushUndois called after validation, so failed and no-op mutations do not record history.Redomust userecordUndo, notpushUndo, or it clears the stack it is walking.MCPEnabledis never inSnapshot. Undo must not be able to re-open access someone revoked.mutateFileis the only write path.Mutateis a thin wrapper that resolves the space inside it; the space operations usemutateFiledirectly. Giving either its own write path would duplicate all five properties above where no test covers them.- A space is everything a mutation can touch. Tasks, archive, labels,
NextID, and both history stacks live insideData; onlyVersionandMCPEnabledsit on the enclosingFile. Undo is therefore per space, and space lifecycle changes are deliberately not undoable. - Space resolution never creates. A name that is not in the file fails
before
fnand before any write, which is what stops a typo conjuring an empty matrix — and what keeps "no MCP tool can create a space" true. Data'sSpace,AllSpaces, andMCPAllowedare derived, never persisted. They describe the document, and exist so a frontend can render an operation's outcome without a second read.
Linux and macOS only, and Windows is not planned — see the README's platform support section. If you do want to add Windows, the path resolution and the Windows CI coverage need to arrive together, or it is just untested surface in a different place.
- Keep the change focused; one concern per PR.
- Add a test. For a bug fix, a test that fails before your change is ideal.
- Run the commands above before pushing. CI runs them on Linux and macOS.
- Explain why in the commit message, not just what. If a change looks redundant but is not, say so in a comment — several already exist for exactly that reason.
Open an issue with what you ran, what happened, and what you expected. Include
ike --version. If it involves the data file, please redact your task titles —
they are personal, and nobody needs them to fix the bug.
For anything security-related, see SECURITY.md instead.