@punchsoftware/groom
v0.9.0
Published
A markdown-native project roadmap that lives in your repo
Maintainers
Readme
groom
A project roadmap that lives in your repo as markdown files.
Roadmaps rot. They live in a doc nobody opens, drift from what's actually being built, and get rewritten from scratch every quarter. groom puts the roadmap next to the code, computes progress instead of asking you to type it, and tells you where it has stopped matching reality.
groom
2 outcomes · 12 projects · 19/24 done
◆ Activation is something users hit on day one ██████████ 100% 2026-07-20 → 2026-09-30
├─ ● P-1 Frictionless sign-up ██████████ 100% 2026-07-20 → 2026-07-22
│ ├─ ● P-5 OAuth flow
│ └─ ● P-6 Session management
└─ ● P-2 Local web UI ██████████ 100% 2026-07-25 → 2026-08-05
◆ groom is something people can install and trust ░░░░░░░░░░ 0% 2026-08-01 → 2026-11-30
├─ ○ P-3 Packaging and distribution ░░░░░░░░░░ 0% 2026-08-01 → 2026-09-15
└─ ○ P-4 Docs and onboarding ░░░░░░░░░░ 0% 2026-08-15 → 2026-10-15The idea
Two altitudes:
- Outcome — a customer or business capability you want, with a metric. Not a feature.
- Project — a bounded body of work that materially advances that outcome. Projects nest: a project can sit under an outcome or under a parent project, to whatever depth the work needs.
Only leaf projects report truth. A leaf project — one with no sub-projects —
is either done or it isn't; every parent above it (a project with sub-projects,
or an outcome) is the weighted mean of its children. There is no field anywhere to
type "80% complete" — that number is the single most common lie in project
tracking, and groom simply doesn't offer it. Started work earns no partial credit.
If you want a number to move, close a leaf project.
Install
groom runs on Node 20 or newer:
npm install -g @punchsoftware/groomOr from source:
npm install
npm run build # → dist/groom.jsGetting started
groom init "Acme Platform"
groom outcome add "Grow activation 20%" --metric "12% → 20%" --target 2026-12-31
groom project add "Auth revamp" --parent O-1 --start 2026-06-01 --target 2026-09-30
groom project add "OAuth flow" --parent P-1 --status done
groom project add "Session mgmt" --parent P-1
groom # the outcome tree
groom timeline # a Gantt chart of dated work
groom drift # where the roadmap disagrees with reality
groom outlook # overdue work, the next 30 days, and undated active work
groom archive --done-before 90d --dry-run # preview cleanup of old finished workEverything is a file:
roadmap/
config.yml
outcomes/ O-1 - Grow activation 20%.md
projects/ P-1 - Auth revamp.md--- # projects/P-1 - Auth revamp.md
id: P-1
title: Auth revamp
parent: O-1
status: in-progress
start: 2026-06-01
target: 2026-09-30
owner: brdsmth
---
## Why
Sign-up drop-off is concentrated on the password step.A project's parent is an outcome or another project — that's all nesting is.
Edit these files in your editor, commit them with your code, review them in a PR.
groom reads whatever is on disk.
Autocommit
If you'd rather never think about committing the roadmap — especially when
agents are the ones editing it — set autocommit: true in roadmap/config.yml
(or start with groom init --autocommit). Every change groom makes is then
committed to the host repo on the spot, and the log reads like a changelog:
chore(roadmap): update P-1 (status in-progress → done)
chore(roadmap): create O-2 "Reduce churn"A batch of edits — an agent's burst of tool calls, a drag-reorder in the web
UI — lands as a single commit. Commits are scoped to roadmap/, so whatever
else you have staged stays staged and untouched, and git trouble (no repo yet,
no identity configured) never blocks the edit itself: the file is written, the
commit is skipped with a warning. groom never pushes.
Drift
The failure mode of every roadmap is quiet rot. groom drift names it:
| Rule | Fires when |
|---|---|
| slipping | Past its target date and still open |
| at-risk | More schedule spent than work done |
| stalled | A project marked in progress, but every project under it is still planned |
| childless | An outcome with no projects |
| orphaned | A project serving no outcome |
| stale | Untouched longer than staleAfterDays |
| upstream-closed | A linked GitHub issue closed while the project stayed open |
Every finding carries a remedy, not just a complaint. groom drift --strict
exits non-zero, so roadmap hygiene can gate CI.
Known findings can be acknowledged through a review date without deleting the underlying commitment:
groom drift ack P-12 slipping --until 2026-10-15 --note "pending October planning"
groom drift --all # includes active acknowledgements, marked acked
groom drift unack P-12 slippingThe acknowledgement is stored in the entity's drift frontmatter list. It is
suppressed from drift reports and summaries until the date passes; expired
entries fire again and validation suggests extending or removing them.
staleAfterDays (default 30) controls stale findings and when old stalled or
orphaned work escalates to high severity. slippingHighAfterDays (default 14)
controls when a missed target escalates to high severity.
Slack delivery can add shared thread links and resolve owners without putting delivery logic in the caller:
links:
roadmap: https://roadmap.example.com
outlook: https://roadmap.example.com/outlook
drift: https://roadmap.example.com/drift
owners:
bradley: { slack: U012345, name: Bradley }
backlogOutcome: O-19
maxBodyLines: 120Owner keys match frontmatter case-insensitively with surrounding whitespace
trimmed. backlogOutcome excludes that outcome from schedule risk and the
Gantt. maxBodyLines controls the body-oversized validation warning so
roadmap files do not become notebooks. A non-empty owners map makes unknown
owners a validation warning. It also defines the People filter and owner
suggestions in the web UI, including people with no assigned work. Historical
owners outside that roster remain on their items but do not become options.
Slack user mentions appear only on attention and overdue lines; other sections
use the configured display name.
Planning, not just tracking
Keep outcome, project, and milestone titles succinct, professional, and
explicit. Use a short phrase in sentence case that names the capability,
deliverable, or gate. For example, use Add invoice downloads to billing
history instead of Billing improvements. Avoid filler, hype, and owner
or status notes; put implementation details and acceptance criteria in the
body. The same guidance applies when renaming items and is included in
groom guide, the web Guide, and the MCP workflow and tool instructions.
Hierarchy and rollup are not enough for an agent to decompose work well. Left alone, an agent turns every checkpoint in a plan into a project, because the create call accepts any title under any parent. groom now carries the structure the decision needs, and pushes back at the moment it matters.
| Command | What it answers |
|---|---|
| groom next | Which leaf projects are executable now, and what the rest wait on |
| groom outlook [--days 30] | What is overdue, promised next, blocked, or active without a date |
| groom critical-path [id] | The longest chain of dependent open work |
| groom validate --strict --json | Everything structurally wrong with the files, with a stable code each |
| groom project classify "<title>" --parent P-1 | Is this a project, a milestone, a step, or a duplicate? |
| groom project decomposition P-1 | Has this parent been split well? |
| groom lanes [id] / groom collisions | What can safely run at once, and what must not |
| groom context P-7 | Everything needed to execute one project, in one packet |
| groom task plan P-1 | Proposed waves and bundles for handing a program out |
| groom project complete P-7 --commit <sha> --test "npm test=passed" | Close with evidence, and see what it unblocked |
The data behind them lives in project frontmatter and is optional throughout:
dependsOn: # execution order; parent is hierarchy only
- id: P-4
reason: contracts must be frozen first
milestones: # gates inside the project — never in the rollup
- id: shadow-green
dependsOn: [behavior-frozen]
approvalRequired: true
execution: # what must not run alongside this
lane: invoice
mode: serial
resources: [invoice-ingestion]
implementation: # the seams, so collisions are caught before merges
owns: [invoice-ingestion]
touches: [shared-document-outcomes]
relevantFiles: [server/invoiceOps.ts]
executionPlan: # on a parent: waves its sub-projects run in
- { mode: serial, projects: [P-5, P-6] }
- { mode: parallel, projects: [P-7, P-8] }
completion: # recorded when it closed
commit: 00cbb245
tests: [{ command: npm test, result: passed }]
acceptance: { "Processing is idempotent": passed }Creating a project runs an overlap and decomposition check first. A title
that duplicates open work, is phase-shaped ("Phase 3"), restates its parent,
names a stage of it ("Testing", "Rollout"), or extends a strict chain of
sibling checkpoints is refused with a recommendation — add a milestone, put
it in the parent's body, update the existing project. --force or a stored
creationReason overrides it, on the record.
Every entity carries a revision; pass it back with --if-revision (or
ifRevision over MCP) and an edit fails with a conflict instead of
overwriting a change made since you read. Edits always patch over the
freshest copy on disk, so two agents changing different fields both land.
groom project claim P-7 --by <name> tells other agents a project is taken.
Three config keys tune the structural checks: maxActiveChildren (default
8), maxProjectDepth (default 2), and requireCompletionEvidence (default
false, which makes a done-without-evidence a note rather than a warning).
Ratcheting existing validation debt
A roadmap with known validation issues can still gate CI without accepting new ones. Capture the current issue identities, then run strict validation against that baseline:
groom validate --write-baseline roadmap/.validate-baseline.json
groom validate --strict --baseline--baseline with no path uses roadmap/.validate-baseline.json; an explicit
path is also accepted. The baseline keys issues by stable check code and entity
id, so wording changes do not create false failures. Fixed entries are printed
under "can be pruned" and do not fail the run. Use --prune to remove them.
Sharing it
groom export -o roadmap.html # static: no scripts, no fonts, no CDN
groom export --interactive -o team.html # one read-only file with the full app
groom export --format md # narrow enough to paste into a PR or email
groom gantt --format svg # large-type timeline for an image post
groom gantt --format html # self-contained page for Chromium screenshotsThe static export is the roadmap as a document: the outcomes, the timeline,
the board, and the needs-attention list, with <details> for collapsing and
not a line of script. --interactive is how you hand the roadmap to someone
who doesn't run groom: the same five views as groom web, the inspector
included, with the snapshot inlined and editing removed. It makes no network
requests and needs no server. Both render from the same snapshot the web UI
uses, so an export never disagrees with the live view about a bar or a card.
--no-drift omits drift findings from the export.
gantt defaults to 60 days before today and 45 after it. Use --days-before,
--days-after, --max-projects, --width, and --theme light|dark to tune the
image. It shows undated active work explicitly rather than inventing dates, and
omits the outcome configured as backlogOutcome.
The web UI
groom web --openFive views over one snapshot, with a slide-over inspector:
- Timeline (the default) — one row per project, grouped by outcome in priority order. Choose This week, This month, or This quarter in the horizon strip to zoom the timeline; it stays a timeline, and remembers the range. The window begins on Monday of this week and ends at the selected horizon (quarter by default). Short windows show daily ticks. Bars end on the target date; a project with no start fades in from the left, one whose dates come from its sub-projects is dashed, and one past its target runs to today with a red cap. A project with no target gets no bar — it's listed under No target date instead. Dates are never invented.
- Board — Backlog (the backlog outcome, later targets, no target) at the far left, then Next (due this quarter), Now (due this month, or in progress), and Shipped (closed in the last 30 days, dated from git). Every column uses the same level: projects directly under outcomes, plus unattached projects. Each card labels the project and the outcome it supports; parent cards show their sub-project count and computed progress. Open a card for its sub-projects, or use View all items for the full hierarchy. Completed sub-projects remain inside their parent rather than appearing as separate Shipped cards.
- This week — what's due in the window, what's in progress, and what's
ready to start next (the same rule as
groom next), with key dates and owner load alongside. - All items — every outcome and project in an expanded, indented hierarchy, including undated, completed, and unattached work. Search by title, ID, owner, or tag; filter by kind or status. Matching items keep their ancestors visible for context. Check one or more items (or select all matching) to move them to a project or outcome, detach them, or permanently delete them. Selected parents keep their children nested when moved. Delete confirms the full list of affected items, including children. Selections persist across filters; the selection count calls out hidden selections. Drift findings remain in each item's inspector. Right-click a project (or use its ⋯ button) to move that project directly to an outcome. The menu marks its current parent; children keep their nesting. Keyboard users can press Shift+F10, navigate with arrow keys, and press Enter to move or Escape to dismiss. Drag a project's title onto an outcome or project to move it; drag a checked project to move the selected projects together. Drop on Unattached to remove their parent. Children keep their nesting, and self/descendant drops are blocked. Drag outcomes above or below one another to reorder them. Hover an outcome to reveal + beside its title and create a project under it directly. The button also appears on keyboard focus and stays visible on touch.
- Changes — a change log read from git for the last 7 or 30 days: created, status changed, closed, re-targeted, re-parented, retitled, archived, deleted. Filter by kind or search.
The horizon strip under the title shows This week, This month, and This
quarter with explicit dates, rolled forward when the current period is nearly
over (fewer than 3 working days, 7 days, or 14 days left). The owner filter
persists across the planning views; All items has its own filters. Views
deep-link as #timeline, #board, #items, #week, #changes, and an entity
as #P-12.
Clicking any row, bar, or card opens the inspector. Status and owner are
menus, dates and the title are fields, and every edit is written immediately
with a revision check — if someone else changed the file first, you get a
toast and a reload, never a silent overwrite. Notes are editable markdown.
Add comma-separated Tags in the New dialog or edit them in the inspector;
clear the field to remove all tags. Tags appear below titles in All items and
can be found through its search. The Tags dropdown filters All items,
Timeline, Board, and This week by an exact tag or Untagged. The choice
follows you between these views and combines with their other filters;
All tags clears it. Matching nested items keep their ancestor context.
Drag a project onto an outcome or another project to re-parent it; drag an
outcome above or below another to rank it, which persists as priority.
Binds to localhost only.
Linking to GitHub
A project can point at an issue or PR:
groom project edit P-1 --ref gh:owner/repo#412
groom sync # report what closed upstream
groom sync --apply # write itAuth comes from the gh CLI, so there are no tokens for groom to store or leak.
Pulling summaries into another app
If you already have crons and a Slack bot, groom doesn't need its own. It produces the content and you deliver it:
groom summary --format slack # blocks ready for chat.postMessage
groom summary --delta # only changes since the lookback point
groom summary --cadence weekly # seven-day deltas plus a 14-day outlook
groom summary --format json # structured, format it yourself
groom summary --format markdown # for a PR comment or emailThe daily Slack shape is image-first: upload a PNG rendered from groom gantt
--format html with summary text as its message, then post each array in
thread as a reply. The complete Landed, Newly at risk, Resolved, Status
changes, and In progress lists live in the thread without caps.
Or over HTTP, if groom web is already running:
GET /api/summary?format=slack&hours=24
GET /api/outlook?days=30&format=jsonCheck isEmpty before posting — it's true when nothing landed, nothing is in
progress, and nothing needs attention. landed is read from git history, so
it needs committed roadmap changes and a full clone.
Delta summaries compare the current roadmap with the commit at or before
--hours ago. Their structured data adds since, every statusChanges
transition, newFindings, resolvedFindings, and per-outcome outcomeDeltas.
Outside Git history or in a shallow clone these arrays are empty and since is
absent, so scheduled integrations degrade quietly.
--cadence daily|weekly defaults to daily. Weekly cadence uses a 168-hour
lookback unless --hours is explicit, leads its Slack layout with outcome
progress deltas, then shows the next 14 days, landed work, and attention.
Finished leaves can be moved out of the live roadmap in bulk with groom
archive --done-before <duration|date>. The cutoff accepts values such as 90d
or 2026-06-01; --dry-run lists the exact ids first. Completion age comes
from the last Git status transition, falling back to file mtime when history is
unavailable. Empty project parents follow their children. Any live parent or
dependsOn reference refuses that entity and names the referrer.
Full contract, including failure modes and a worked example: docs/INTEGRATION.md.
Working with agents
claude mcp add groom -- groom mcp startTwenty-five tools over the same core the CLI uses, plus
groom://workflow/overview — one authoritative description of how to groom a
roadmap, served to the agent rather than copied into instruction files that go
stale. It encodes the rules that keep the data worth reading: never invent a
target date, never close a parent to make a number look right, close the leaf
projects instead — and the creation decision: classify proposed work before
creating it, and default to updating or combining.
The tools an agent reaches for most, in the order it uses them:
classify_proposed_work and find_related_projects before creating anything;
get_ready_projects to choose work; get_project_context before starting it;
project_claim so other agents see it is taken; reconcile_project_after_merge
to close it with evidence and learn what it unblocked; validate_roadmap after
a batch of edits; propose_execution_bundles to hand a program out.
Every read command takes --json with a versioned envelope, so scripts and
agents that prefer the CLI get a stable contract too.
Prompts that work
Copy-pasteable starting points. They exist because each one encodes something groom cares about that an agent won't infer on its own.
Turn an idea into roadmap structure
Read the groom roadmap, then break this into projects: . Attach everything to an existing outcome if one fits — don't create a parallel structure. Split a project into sub-projects only along code-ownership lines — one per module or surface an agent can own end to end — never into sequential phases. Each leaf must be executable on its own. Don't set target dates; I'll add them.
The middle lines matter because agents over-split: they turn one body of work into "design / implement / test" checkpoints that can't run in parallel and never quite get closed. The last line matters because agents will happily invent a Q3 deadline you never agreed to, and once it's written down someone will hold you to it.
Keep the roadmap current as you work
Before you start, get the groom context for the project covering this, claim it, and set it to in-progress. When the code is merged, reconcile the project in the same turn with the commit, the tests you ran, and each acceptance criterion marked passed or failed. If no project covers this work, classify it first — create one only if groom says it is a project.
"In the same turn" is the whole trick. A roadmap updated later is a roadmap updated never, and a done with no evidence is a claim nobody can check.
Hand a program out to several agents
Run groom task plan on and show me the waves and bundles. Don't create one task per sub-project. If two bundles collide on a resource or the grouping looks wrong, fix the dependencies or resources and re-plan before assigning anything.
Clean up after a work session
Run groom drift. Fix what you can without guessing: attach orphans to the right parent, close projects whose work is merged, drop work we've abandoned. For anything needing a decision — a slipping target, an outcome with no projects — list it and stop.
Write up a project
Fill in the notes for : why it matters, the approach, and acceptance criteria as a checklist. Read the surrounding code first — I want the real constraints, not a generic plan.
Weekly review
Give me the groom summary for the last 7 days, then tell me which outcome moved least and why. Check whether its projects have work underneath them at all.
Development
npm test # the suite
npm run check # biome
npm run dev # run the CLI from source (Node 22.18+ runs TypeScript directly)groom's own roadmap lives in roadmap/. If planning groom in groom
ever feels awkward, that's a bug in the model.
Prior art
The file-as-database shape, the ID - Title.md convention, and the
CLI + TUI + web + MCP surface are borrowed from
Backlog.md, which does the same thing one
altitude lower, for tickets. Use it if tickets are what you need; use groom if the
question is what the work is for.
License
MIT
