foam 0.2.0

an issue tracker and agent memory that lives on a git ref
foam-0.2.0 is not a library.

foam

focused & organized agent memory

foam helps organize and persist the things your agent should remember

about

The handoff between coding agent sessions can be messy. When a fresh agent session is started up in an ongoing project, the context window is empty and three things need to find their way into it: rules, memories, and plans. Rules are things that a human decides about the way a project runs (even if they were proposed by an agent). Memories are things an agent has discovered about the project. Plans are descriptions and reports of work: ideas, goals, steps taken, etc.

Rules tend to be pretty well managed by your instruction files (CLAUDE.md, AGENTS.md); they naturally settle as a permanent fixture, and they often scope to an entire project. On the other hand, memories and plans are (probably) sound when they are written, but they can drift aggressively as the code and project change. Agents will place memories and plans in whatever way the harness (or your instructions) tells them to. If you're not intentional about it, it's pretty easy to end up with a CLAUDE.md that reads like a fever dream, dozens of PLAN-FOR-<X>.md files, and memories in the harness that you've never actually read.

foam gives your agent's memories and plans a structured place to live. Memories are kept in a store, and plans becomed tracked issues (like GitHub issues). Your agent learns how to use the foam CLI, and foam keeps track of everything as JSON in a git ref. This way, the memory store and issue tracking live alongside your project, but with a separate history; everything foam records is invisible to your working tree and branches, but it's still versioned and accessible.

foam was inspired by beads. The same core ideas are at play here, but foam does a lot less than beads, and it won't scale nearly as well. If you are trying to track issues by the thousand produced by swarms of agents, you probably want beads; if that sounds like overkill, you probably want foam.

quickstart

cargo install foam
cd your-repo
foam init
foam setup claude

foam init creates the ref (and sets up sync, if your repo has a remote). foam setup claude adds two hooks to .claude/settings.json: one that runs foam prime when a Claude Code session starts, and one that releases the session's claims when it ends. That's the whole integration. From there your agent figures out the rest on its own: it files issues, claims them, notes what it learns, and closes what it finishes. You can check in on it with foam board or foam list.

documentation

storage and sync

refs/foam/data is a chain of commits. Each commit's tree holds meta.json, one issues/<id>.json per issue, and one memories/<slug>.json per memory. Every foam command reads that tree with git plumbing and writes a new commit on top of it. Nothing is ever checked out, and a plain git clone won't fetch the ref. Every write also records the commit and branch you were on at the time, which is how foam knows later that a memory is fifty commits behind. If two foam commands in the same clone race, the ref's compare-and-swap catches it and the loser retries.

If your repo has a remote, foam init adds a fetch refspec so git fetch lands the remote's data on refs/foam/origin, and installs a pre-push hook that runs foam sync before every push. Any foam command merges whatever a fetch brought in before it does its own work, so a clone is never behind what it has fetched. foam sync does the fetch, merge, and push by hand, and foam sync --setup adds the refspec and hook to a clone that was made before foam init ran somewhere else.

When two clones change the same record, foam merges it field by field: a removal on either side stands, notes interleave by time, and a field both sides changed takes the newer value. Ids are random, so two clones creating issues at the same time won't collide.

On a single machine none of this applies; everything reads and writes your local .git.

how humans use foam

Mostly you just look. Your agent files and closes the issues; you read the board, open an issue now and then, and step in with foam create, foam update, or foam close when the plan needs changing.

foam board

foam board puts everything on one screen, with milestones, what's in progress, what's ready, what's blocked and by what, and the recent history.

foam  on main at 9f25882  5 open, 1 in progress, 0 deferred, 2 closed

Milestones
foam-f168e4       P1  open  milestone  2/4  v0.2.0
  └─ foam-c8576c  P1  open  milestone  2/3  Terminal output

In progress
foam-dac906  P1  in_progress  task  Wire the parser  @ann  lease 14 min left

Ready
foam-4368f8  P2  open  task  One-screen board
foam-1ad1b3  P2  open  task  Review the README

Blocked
foam-f168e4       P1  open  milestone  2/4  v0.2.0  waits on foam-c8576c foam-1ad1b3
  └─ foam-c8576c  P1  open  milestone  2/3  Terminal output  waits on foam-4368f8
foam-a5def3       P2  open  task            Test the parser  waits on foam-dac906

Recent
4b3389f  2m ago   remember parser-lib
9536e8b  5m ago   note foam-dac906
e5abdcc  20m ago  claim foam-dac906 by ann
e21cab6  1h ago   close foam-b2e188

foam list

foam list shows the open issues as a tree. Children sit under their milestones, a milestone's closed count gets its own column, and anything that's waiting on something says so.

foam-f168e4         P1  open         milestone  2/4  v0.2.0
  ├─ foam-c8576c    P1  open         milestone  2/3  Terminal output
    └─ foam-4368f8  P2  open         task            One-screen board
  └─ foam-1ad1b3    P2  open         task            Review the README
foam-dac906         P1  in_progress  task            Wire the parser  @ann
foam-a5def3         P2  open         task            Test the parser  waits on foam-dac906

--all includes closed and deferred issues, and --status, --type, --label, and --assignee narrow the list.

foam show

foam show prints one issue in full, with its fields, children, blockers, body, and notes.

foam-dac906  Wire the parser
type: task  status: in_progress  priority: P1
assignee: ann  lease 14 min left
created: 3h ago

The grammar is in docs/grammar.md. Winnow, not nom: see the parser-lib memory.

[5m ago ann] PEG was a dead end: left recursion in the grammar

foam ready and foam blocked

foam ready lists what could be worked on right now, meaning open issues with nothing blocking them and no deferral holding them. foam blocked lists everything that's held back, along with what's holding it.

foam-4368f8  P2  open  task  One-screen board
foam-1ad1b3  P2  open  task  Review the README

foam memories

foam memories lists every memory with how far HEAD has moved since it was written, and foam recall <slug> prints one.

parser-lib  winnow, not nom  (at 3d7c1a2, 1 commit ago)

short ids and the picker

Ids look like foam-c4574c, which is a pain to type. Any command that takes an id also takes a unique prefix of the hex part (foam show c45), or a word from the title when only one open issue has it (foam show parser). If you have fzf installed, foam show on its own opens a picker, and foam pick prints the chosen id so you can do foam claim $(foam pick). Put eval "$(foam setup bash)" in your .bashrc after fzf's own line and foam show **<TAB> searches the issues too.

terminal and pipe

On a terminal, output is colored, aligned, and wrapped, and times are relative ("3h ago"). Through a pipe it's plain text with full timestamps and no tree glyphs, so the first field of a line is always the id. --plain gets you that form on a terminal, NO_COLOR turns off only the color, and every command takes --json.

the records foam keeps

foam keeps two kinds of record. Issues are work with a lifecycle, and memories are facts. Everything above is a view of these two.

issues

An issue has a title, a kind, a priority from P0 to P4, a status, and a body. The kinds are task, bug, feature, chore, milestone, spike, and decision. The statuses are open, in progress, deferred, and closed. Notes get appended over time, each stamped with its author and the commit it was written at. A closed issue carries a reason, and whether the work was done or dropped.

An issue can wait on other issues, in which case it stays out of ready until they close. It can have a parent, which is how milestones are built. Claiming an issue marks it in progress under your name (or your agent's) for fifteen minutes by default. foam heartbeat extends the claim, foam unclaim gives it back, and an expired claim gets released at the next session start, so an agent that dies doesn't hold its issue forever.

foam create "Wire the parser" -p 1
foam create "Test the parser" --blocked-by <id>
foam claim <id>
foam note <id> "PEG was a dead end: left recursion in the grammar"
foam close <id> --reason "parser wired"
foam update <ids> --priority 0 --parent <milestone>
foam update <id> --defer-until 2026-10-01
foam dep add <id> <blocker>

milestones

A milestone is an issue whose children are its work. It stays out of ready while anything below it is still open, and its rollup counts the issues at the bottom of its tree, so a milestone of milestones shows how much of the actual work is done. A release is a milestone titled vX.Y.Z whose children are whatever ships in it.

foam create "v0.2.0" --type milestone -p 1
foam create "Terminal output" --type milestone --parent <release>
foam create "Color the status column" --parent <milestone>

memories

A memory is a fact the next session would otherwise have to rediscover, like which library you picked, where a fixture lives, or what a flag turned out to mean. It has a slug, up to 512 bytes of text, and the commit and branch it was written at. foam memories and foam prime say how far HEAD has moved since, or that the memory came from a branch this one never merged, and a memory that's fallen behind gets a line in prime telling the agent to confirm it or forget it.

foam remember parser-lib "winnow, not nom"
foam recall parser-lib
foam forget parser-lib

If you're not sure whether something is a memory, ask who's wrong if it turns out to be false. If a person decided it, it's a rule and belongs in CLAUDE.md. If the world moved on, it's a memory. If nothing is wrong yet but something should change, it's an issue. A memory shouldn't repeat CLAUDE.md, since both land in every session, and foam doctor will flag one that does. Facts only ever move up. An agent remembers something, you see it in prime a few sessions in a row, and you make it a rule or an issue.

how agents use foam

Your agent never sees a terminal. It reads what foam prime prints at session start, then runs the same commands you would through its shell tool. This section is what foam setup claude wires up and what the agent gets told.

The SessionStart hook runs foam prime --hook-json, which drops the text below into the session's context. The SessionEnd hook runs foam session-end, which releases any claims the session still holds. Each Claude Code session acts under its own name (your git user plus a session suffix), so two sessions of yours don't share claims; --actor or $FOAM_ACTOR overrides that.

This is what a session reads first, taken from a small repo:

# foam

foam tracks this repository's issues and memories on a git ref; nothing is checked out and nothing here is a file to edit. It is the one place work and facts go: not a TODO comment, a plan file, a handoff note, a todo list in the harness, or the harness's own memory.

Work: `foam ready`, then `foam claim <id>` before touching code. When you find work that has no issue, including anything noticed on the way, `foam create` it and keep going. `foam note <id> <text>` when you decide something or hit a dead end, so the next session does not retry it. `foam close <id> --reason <why>` once it is done, and `foam unclaim <id>` anything you stop working on.

Memories are facts about the code or the world that a next session would otherwise rediscover: `foam remember <slug> <text>`. Remember facts, never rules; a rule is something a person decided and belongs in CLAUDE.md. When a fact would serve better as a rule or an issue, say so in your reply. A memory marked old or from another branch may no longer hold: check it against the code, `foam remember` it again if it still holds, `foam forget <slug>` if not.

## Commands

foam ready [--limit N]          issues that can be worked now
foam show <id>                  one issue in full, with notes, blockers and children
foam create <title> [--type T] [-p 0-4] [--blocked-by ID] [--parent ID]
foam claim <id> | unclaim <id> | heartbeat <id>
foam note <id> <text>           append a note
foam close <id> --reason <why> [--dropped]  | foam reopen <id>
foam dep add <id> <blocker>     make <id> wait on <blocker>
foam blocked                    what is waiting, and on what
foam search <query>             issue titles, bodies, notes; memories
foam remember <slug> <text> | memories | recall <slug> | forget <slug>
foam update <id> [--title ..] [--priority N] [--defer-until DATE]
Add --json to any command for machine-readable output.
Priority: P0 blocks all other work, P1 this session, P2 soon, P3 when convenient, P4 someday.

## Status

1 open, 0 in progress, 0 deferred, 1 closed; on main at a4b8722
acting as ann

## Ready (1 total)

s-c1f981  P2  task  Test the parser

## Memories

parser-lib: winnow, not nom

foam config sets the two numbers every clone shares. lease-minutes is how long a claim holds, and stale-after is how many commits behind HEAD a memory can be before prime marks it old.

json

--json output is for scripts and agents, and its shape is kept stable on purpose. Within a minor release, keys only get added, never removed or renamed, and a value never changes type. Removing or renaming a key bumps the minor while the crate is below 1.0 (and the major after), and the changelog names the command and the key. There's no version key in the output; foam --version tells you which shape you have.

building from source

You need git 2.38 or newer and a Rust toolchain.

git clone https://github.com/jackroddy/foam
cd foam
cargo install --path .        # installs foam into ~/.cargo/bin
cargo build --release         # or just build it: target/release/foam
cargo test                    # unit tests and the integration tests

license

MIT or Apache-2.0, at your option.