Your plan is a file β .devtree/tree.yaml β that sits next to the code. It is versioned with the
code, reviewed in the pull request, and merged line by line. From it, devtree generates a
Mermaid diagram into TREE.md or straight into your README.md, and a
drawing of its own into .svg. GitHub and GitLab render both natively. No browser extension, no
image to regenerate by hand, nothing to host.
Why keep the plan in the repository
A roadmap in a tracker drifts away from the branch it describes. A roadmap in a wiki is read once
and never again. A roadmap in the repository is a file people already have habits for.
How it works
What it looks like
The Mermaid backend, which GitHub draws right here in the page:
And the SVG backend, which devtree draws itself β further down, rendering this
repository's own plan.
Install
go install github.com/SergeyLubivui-dev/devtree@latest
Prebuilt binaries for Linux, macOS, and Windows are attached to every
release, with checksums.
Or build from source β Go 1.22 or newer, nothing else:
git clone https://github.com/SergeyLubivui-dev/devtree.git
cd devtree
go test ./...
go build -o devtree .
In a container
If you would rather not install anything at all, mount the repository and run the image:
docker run --rm -v "$PWD:/work" ghcr.io/sergeylubivui-dev/devtree render
docker run --rm -v "$PWD:/work" ghcr.io/sergeylubivui-dev/devtree board
docker run --rm -v "$PWD:/work" ghcr.io/sergeylubivui-dev/devtree add "Search filters" -p mvp
On Linux, add --user "$(id -u):$(id -g)" so the files it writes belong to you rather than to root.
Tag latest follows the last release, edge follows main; both are published for amd64 and
arm64. A shell alias makes it disappear into the background:
alias devtree='docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/work" ghcr.io/sergeylubivui-dev/devtree'
The image carries git, so devtree sync works inside it too. That is the only reason it is not built
FROM scratch: everything else devtree does needs the plan file and nothing more.
Quickstart
cd your-project
devtree init --project "Payment Gateway" --repo https://github.com/acme/pay --hook --action
devtree add "Authentication" -p mvp -b feat/auth -i 12 -o ann -s wip
devtree add "OAuth providers" -p authentication -b feat/oauth
devtree add "Password reset" -p authentication -s blocked -n "waiting on SMTP"
devtree ls # the tree, in your terminal
devtree done oauth-providers
git add . && git commit -m "feat: oauth" # the hook refreshes the diagram for you
IDs are derived from titles β "OAuth providers" becomes oauth-providers, and non-Latin titles are
transliterated so the ID stays typeable. Pass --id when you want to choose one yourself.
Everyday recipes
Short, real things people do with a plan. Copy one and change the words.
Start a feature, finish a feature. The task and the branch are named together, so anyone reading
the diagram knows where the code is:
devtree add "Search filters" -p mvp -b feat/search -i 214 -o you -s wip
git switch -c feat/search
# ...write the code...
devtree done search-filters
git commit -am "feat: search filters" # the hook refreshes the diagram
Break something big into something doable. Parents count their children automatically, so the
milestone reports progress you never have to update by hand:
Park what you cannot finish. A blocked task with no note is the one thing check complains
about, because "blocked" without a reason is a task nobody can pick up:
devtree set password-reset -s blocked -n "waiting on the SMTP contract"
devtree board -s blocked
Monday morning, in three commands:
devtree board # what is in flight, what is stuck, what is waiting
devtree ls -s blocked # only the branches that need a decision
devtree check # anything marked done that still has open work under it?
Two people, two branches, one plan. Both add tasks, both commit, and the merge is boring β the
node list is flat and .gitattributes says merge=union. If two branches happen to pick the same
ID, devtree check fails on the spot with the duplicate named.
The board
The tree says how the work is organized. The board says what state it is in this morning β same
file, different question:
devtree board
Payment Gateway
β not started Β· 1
Apple Pay Payments #51
β in progress Β· 1
OAuth providers Authentication !31
β blocked Β· 1
Password reset Authentication β waiting on SMTP
β done Β· 1
Stripe Payments !44
ββββββββββββββββββββ 1/7
Only leaves appear. A milestone is a container, not a task, and a board that lists containers next
to the work inside them stops being a board β so each card carries its milestone as a breadcrumb
instead. Empty columns are left out entirely: a board with nothing blocked should not spend a fifth
of its width saying so.
The same board renders to SVG. Name an output board.svg (or anything.board.svg) and it is drawn
as columns instead of a tree:
A plan that keeps every task ever completed stops being a plan and becomes a log: the board fills
with columns of done work and the diagram grows a tail nobody reads. Archiving keeps the record
without keeping the noise.
devtree archive # what is finished and could move
devtree archive --all # move it into .devtree/archive.yaml
devtree archive v1 # or just this branch of the plan
devtree archive --list # what has already left
devtree restore v1 # bring it back, with everything under it
Nothing moves until you say so β plain devtree archive only reports. A node qualifies only when
its whole subtree is done or dropped, so live work can never vanish along with the milestone above
it. Archived tasks keep the parent they had, which is how they remember where they came from; if
that parent is gone by the time you restore, the task comes back as a root and tells you.
The archive uses the same format as the plan, so there is no second parser to learn and nothing new
to review in a diff.
Closing what git already knows about:
devtree sync # list tasks whose branch is already merged
devtree sync --apply # mark them done
It proposes instead of acting, because git knows which branches were merged but not which of them
were merged finished β a branch merged behind a feature flag is not a task that is done. This is
the only command that runs another program; everything else works on the file alone, which is what
lets devtree run in a tarball or a container with no repository at all.
The canonical spelling is what lands in the file, whichever shorthand you type.
The file format
# Development tree. Edit by hand or through the `devtree` command.
# After editing, run `devtree render` to refresh the diagram.
version: 1
project: "Payment Gateway"
repo: "https://github.com/acme/pay"
outputs: "TREE.md"
nodes:
- id: "mvp"
title: "MVP"
status: "in_progress"
- id: "authentication"
title: "Authentication"
status: "in_progress"
parent: "mvp"
branch: "feat/auth"
issue: "12"
owner: "ann"
- id: "password-reset"
title: "Password reset"
status: "blocked"
parent: "authentication"
note: "waiting on SMTP"
The list is flat; the hierarchy comes from the parent field. That is a deliberate trade.
Nesting would mean that adding a task rewrites an existing block β exactly the shape that makes two
feature branches conflict. A flat list turns "add a task" into "append lines", so parallel branches
merge cleanly, and init writes a merge=union rule into .gitattributes to finish the job. Worst
case you end up with a duplicate ID, which devtree check catches on the spot.
Edit the file by hand whenever you like. The parser is strict and tells you the line number:
devtree: .devtree/tree.yaml: line 14: unknown node field "assignee"
Automation
Pre-commit hook β devtree install hook
Validates the plan, regenerates every output, and stages the result along with your commit. An
existing hook is preserved as pre-commit.devtree-backup. Teammates who have not installed devtree
are not blocked: the hook notices the binary is missing and steps aside.
GitHub Action β devtree install action
on pull_request: fails if the diagram is stale, so the author fixes it in their own diff;
on push to the default branch: refreshes the diagram and commits it.
Rendering into your README
devtree init --outputs "README.md"
The block is written between the <!-- devtree:begin --> and <!-- devtree:end --> markers, each on
a line of its own. If the markers are not there yet, the block is appended once and updated in place
from then on β so put the empty pair wherever you want the diagram to appear. Everything outside
them is left exactly as you wrote it, and re-rendering an unchanged plan rewrites nothing at all.
Only markers that start their own line count. Prose that mentions them mid-sentence β the paragraph
you are reading, for instance β is left alone.
Issue and pull request links are built from repo. If repo is not set, devtree falls back to
relative links (../../issues/12), which GitHub resolves correctly for files in the repository root
and which survive a fork or a move to another host.
Drawing to SVG
Mermaid has a ceiling. GitHub renders it with a strict sanitizer and a page-level CSP, so a diagram
node cannot carry an icon or a link no matter how the label is written. A file devtree draws itself
has no such ceiling β so name an output .svg and you get the native renderer instead:
What the drawing adds over the Mermaid block: status glyphs on a 24Γ24 grid, a stripe of the status
color running the full height of each card, a progress bar and a completion ratio on every parent,
branch, issue, pull request, owner and tags on a second line under each title, a status key along
the bottom, and rounded elbow connectors that stay readable when a milestone grows a dozen children.
Motion that means something
Three things move, and each one is information rather than decoration:
A dash travels down an edge
that edge leads into work that is in progress β it marks the live path through the plan
A glyph turns
that task is in flight right now
A glyph breathes
that task is blocked, the one state worth interrupting a reader for
A bar grows in, once
how much of a milestone is finished
A card fades up
it just arrived, in sequence with its neighbours β the picture assembles instead of appearing
Everything else holds still. The motion is CSS inside the file: GitHub serves repository SVGs under
default-src 'none'; style-src 'unsafe-inline'; sandbox, which permits a stylesheet and forbids
script, so declarative animation is the only kind that survives β and the only kind worth having in
a diagram.
Two rules keep it from ever costing a reader anything. Anyone whose system asks for less motion gets
a still picture, because every animated element is drawn over something that already reads correctly
without it. And printing switches the animations off outright: a renderer that froze the first frame
of a fade-in would otherwise put a blank card on the page.
The output is self-contained by design β no external fonts, no images, no script, no CSS custom
properties. GitHub serves repository SVGs under default-src 'none'; sandbox, and everything in the
file survives that. The one thing sandboxing costs is interactivity: an SVG rendered as an image
cannot carry links, so branch and issue links still live in the table under the Mermaid block.
Glyphs are vendored from Reicon (MIT) as path data in internal/icons β see
NOTICE. Nothing is fetched at render time, and the binary still has no dependencies. Every
picture in this README is drawn by the same code that draws your plan; go run ./internal/art
regenerates them, and CI fails if they drift.
How it is built
internal/tree imports nothing but the standard library. It does not know that a file format
exists, and it certainly does not know about Mermaid β which is why the rules about what a valid
plan is live in one place and stay there. Everything below the CLI is silent: no printing, no
os.Exit, errors returned as values. App takes its working directory and its two writers as
fields, so the test suite drives whole commands against a temporary directory and reads back exactly
what a user would have seen.
go test ./...
go vet ./...
The full layering, the rules that hold it, and how it is tested: docs/architecture.md.
Documentation
This page is the tour. docs/ is the detail underneath it.
The storage format is a strict subset of YAML β a flat list of scalar fields. Anchors, multi-line
block scalars, and nested mappings are not supported and will be rejected with a line number.
Nothing rendered into a README is clickable: GitHub's Mermaid ignores click directives, and an
SVG served as an image is sandboxed. Links live in the collapsed table under the Mermaid block.
An interactive HTML export is on the plan below.
Text in the SVG output is measured by estimate rather than by font metrics β shipping real metrics
would mean shipping a font. Cards are sized a few pixels generously to compensate.
Very wide trees (hundreds of nodes) render slowly in the browser. Split them across several output
files with --outputs.
devtree's own plan
Both of these come from the same .devtree/tree.yaml in this repository, regenerated on every
push. First the SVG backend β GitHub swaps the file when you switch themes:
And the same plan as a Mermaid block, injected straight into this README:
Command devtree keeps a development plan inside the repository and renders
it as a Mermaid diagram that GitHub and GitLab draw natively.
The plan lives in .devtree/tree.yaml: it is versioned with the code,
reviewed in the pull request, and merged line by line. See README.md for the
full tour, or run `devtree help`.
This file is deliberately thin. Everything it could do belongs in a package
that can be tested without a process: the tree itself in internal/tree, the
file format in internal/store, the output in internal/render, and the
command line in internal/cli.
Package render turns a plan into text: a Markdown block for the repository, a Mermaid diagram GitHub draws natively, and an ASCII tree for the terminal.
Package render turns a plan into text: a Markdown block for the repository, a Mermaid diagram GitHub draws natively, and an ASCII tree for the terminal.
Package tree is the domain layer: the nodes of a development plan, the parent/child relationships between them, and the rules that keep those relationships a tree rather than a tangle.
Package tree is the domain layer: the nodes of a development plan, the parent/child relationships between them, and the rules that keep those relationships a tree rather than a tangle.