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 .
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:
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 bar grows in, once
how much of a milestone is finished
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. Readers who ask their system for less motion get a still picture, because every animated
element is drawn over something that already reads correctly without it.
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 ./...
Limitations
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.
Click to show internal directories.
Click to hide internal directories.
go.dev uses cookies from Google to deliver and enhance the quality of its services and to
analyze traffic. Learn more.