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 draws a
Mermaid diagram into TREE.md or straight into your README.md, and a
picture of its own into .svg β as a tree, or as a board. 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 devtree's own drawing β further down, rendering this repository's plan.
Install
brew install SergeyLubivui-dev/tap/devtree # macOS and Linux
go install github.com/SergeyLubivui-dev/devtree@latest
Prebuilt binaries for Linux, macOS, and Windows are attached to every
release, with checksums. Or run it
without installing anything:
docker run --rm -v "$PWD:/work" ghcr.io/sergeylubivui-dev/devtree render
Building from source needs Go 1.22 and nothing else. Details, and a shell alias that makes the
container disappear into the background: docs/container.md.
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.
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 card β and each task carries its milestone as
a breadcrumb. Name an output board.svg and the same thing is drawn as columns:
A plan that keeps every task ever completed stops being a plan and becomes a log. Two commands
handle that from opposite ends:
devtree archive # what is finished and could move
devtree archive --all # move it into .devtree/archive.yaml
devtree restore v1 # bring a branch of it back
devtree sync # tasks whose branch git has already merged
devtree sync --apply # mark them done
Nothing moves until you say so, and a node qualifies only when its whole subtree is done or dropped,
so live work can never leave with the milestone above it. sync proposes rather than acts, because
git knows which branches were merged but not which of them were merged finished.
ls and board take the same four filters, and they combine:
devtree ls -s blocked # one status
devtree board -o ann # one person's work
devtree ls --tag billing,payments # any of these tags
devtree board --root mvp # one branch of the plan
render takes --root too, so a milestone can have a picture of its own:
devtree render --root mvp --file docs/mvp.svg
And when you want the thing itself rather than the plan:
devtree open authentication # its pull request, issue, or branch
devtree open authentication --print # print the URL instead of opening it
Statuses
The canonical spelling is what lands in the file, whichever shorthand you type.
Where the diagram goes
The plan lists its own output files, and the name decides the drawing:
File name
What gets written
TREE.md, README.md
the Mermaid block, between <!-- devtree:begin --> and <!-- devtree:end --> markers
Everything outside the markers is left exactly as you wrote it, and re-rendering an unchanged plan
rewrites nothing at all. Point a <picture> at a light and dark pair and GitHub switches it with the
reader's theme.
Three things in the drawing move, and each one is information: a dash travels down the edges leading
into work in progress, a glyph turns while a task is in flight and breathes while it is blocked, and
progress bars grow in once. Readers who ask their system for less motion β and anyone printing β
get a still picture.
File naming rules, palettes, the animation vocabulary, and the content security policy all of it has
to survive: docs/svg-output.md.
The one thing a diagram cannot do
Neither backend can carry a link: GitHub's Mermaid ignores click, and an SVG served as an image is
sandboxed. So there is a third one. Name an output .html and devtree writes a page where every
task that points somewhere is a link to its pull request, issue, or branch β with a tree view, a
board view, filters by status, owner and tag, and notes as tooltips.
It is one self-contained file with no script, no fonts and no images: the whole interface is CSS
built on :has(). Open it from a checkout, drop it on a shared drive, or point GitHub Pages at it.
This repository's own is docs/plan.html β see
docs/html-export.md.
Automation
devtree install hook # validate and re-render before every commit
devtree install action # fail a pull request whose diagram is out of date
The hook keeps your own commits honest; the Action keeps everyone else's honest without requiring
them to install anything. A teammate without devtree is never blocked β the hook notices the binary
is missing and steps aside. On GitLab, devtree install gitlab writes a job that runs the published
container, so the pipeline needs no toolchain at all. Line by line:
docs/automation.md.
Where you have been
The plan has been in the repository all along, so its own past is already recorded. Nothing had to be
tracked to read it back:
devtree history
Payment Gateway β 3 revision(s) of the plan
2026-08-14 ββββββββββββββββββββ 0/2
2026-08-15 ββββββββββββββββββββ 1/3 +1 done +1 planned
2026-08-16 ββββββββββββββββββββ 2/5 +1 done +2 planned
2 of 5 done, up 2 since 2026-08-14
β 2 not started β 1 in progress β 2 done
Documentation
This page is the tour. docs/ is the detail underneath it.
The layering, the rules that hold it, and how it is tested
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 rejected, with the 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.
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:
flowchart TD
classDef todo fill:#f6f8fa,stroke:#8c959f,color:#1f2328
classDef in_progress fill:#fff8c5,stroke:#bf8700,color:#1f2328
classDef blocked fill:#ffebe9,stroke:#cf222e,color:#1f2328
classDef done fill:#dafbe1,stroke:#1a7f37,color:#1f2328
classDef dropped fill:#eaeef2,stroke:#8c959f,color:#6e7781,stroke-dasharray:4 3
n_v0_1["β v0.1 - first release<br/><i>6/6</i>"]:::done
n_v0_1 --> n_domain
n_domain["β Domain model and validation"]:::done
n_v0_1 --> n_format
n_format["β Strict YAML subset parser"]:::done
n_v0_1 --> n_diagram
n_diagram["β Mermaid renderer"]:::done
n_v0_1 --> n_cli
n_cli["β Command line"]:::done
n_v0_1 --> n_automation
n_automation["β Pre-commit hook and GitHub Action"]:::done
n_v0_1 --> n_tests
n_tests["β Test suite"]:::done
n_v0_2["β v0.2 - sharper day-to-day use<br/><i>11/11</i>"]:::in_progress
n_v0_2 --> n_filters
n_filters["β Filter ls by owner and tag"]:::done
n_v0_2 --> n_focus
n_focus["β Render one subtree with --root"]:::done
n_v0_2 --> n_open
n_open["β devtree open ID - jump to the issue or PR"]:::done
n_v0_2 --> n_history
n_history["β Progress history from git log"]:::done
n_v0_2 --> n_svg_output
n_svg_output["β Native SVG output with embedded icons"]:::done
n_v0_2 --> n_html_export
n_html_export["β Interactive HTML export"]:::done
n_v0_2 --> n_animation
n_animation["β Animation in the SVG output"]:::done
n_v0_2 --> n_board
n_board["β Board layout, in the terminal and in SVG"]:::done
n_v0_2 --> n_archive
n_archive["β Archive and restore finished work"]:::done
n_v0_2 --> n_sync
n_sync["β Close tasks from merged branches"]:::done
n_v0_2 --> n_docs_layout
n_docs_layout["β Documentation folder and a tidy root"]:::done
n_distribution["β Distribution<br/><i>4/4</i>"]:::in_progress
n_distribution --> n_binaries
n_binaries["β Prebuilt binaries on every tag"]:::done
n_distribution --> n_homebrew
n_homebrew["β Homebrew tap"]:::done
n_distribution --> n_gitlab
n_gitlab["β GitLab CI template"]:::done
n_distribution --> n_container
n_container["β Container image on GHCR"]:::done
β not started Β· β in progress Β· β blocked Β· β done Β· β dropped
Contributing
Issues and pull requests are welcome. CONTRIBUTING.md covers the checks CI runs,
which package new code belongs in, and the two rules the layering depends on.
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.