dotmd-cli
v0.87.0
Published
CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.
Maintainers
Readme
runlist
CLI for managing Markdown documents with YAML frontmatter.
runlist (formerly dotmd) indexes, queries, validates, graphs, exports, and lifecycle-manages plans, ADRs, RFCs, design docs, and other structured Markdown. It is built for AI-assisted development workflows where documents need to remain current and safe to mutate.
- Zero runtime dependencies
- Node.js 20 or newer
- Runtime support for Linux, macOS, and Windows
- Type-aware lifecycle rules for plans, docs, and saved prompts
Install
npm install -g dotmd-cli # global CLI and Claude Code plugin hooks
npm install -D dotmd-cli # project scripts via node_modules/.bin
npx dotmd-cli init # try it without installingrunlist is the canonical executable, rl is its short convenience alias, and
dotmd remains supported during the compatibility window. All three invoke the
same CLI. The package is still published as dotmd-cli, and the Claude Code
plugin is still dotmd@dotmd; those names change in a later release. Legacy
dotmd.config.* files, DOTMD_* variables and .dotmd/ state keep working.
Maintainer release automation is POSIX-only because it uses Bash and POSIX command-line tools. The published Node.js CLI remains cross-platform.
Agent host setup
The CLI alone gives an agent no orientation and no session identity. Install the integration for whichever host you run:
runlist install # what's installed for each host
runlist install claude # Claude Code plugin (marketplace + plugin)
runlist install opencode # OpenCode plugin (one auto-discovered file)
runlist doctor --session # what identity runlist sees here, and from whereBoth are one-time and global; runlist update keeps them in step with the CLI.
Codex needs no install for identity: it exports CODEX_THREAD_ID to every tool
shell, and runlist reads it as a per-session identity automatically.
Claude Code Plugin
runlist install claude runs the two steps below for you. From inside a session:
/plugin marketplace add reowens/dotmd
/plugin install dotmd@dotmdThe plugin provides SessionStart and SubagentStart orientation, a
UserPromptSubmit hint that gives the exact runlist baton form when you ask for
a handoff, a PreToolUse guard, the canonical workflow skill, and /plans,
/docs, /prompts, and /baton commands.
OpenCode Plugin
runlist install opencode writes one plugin file into OpenCode's global config
directory, where OpenCode auto-discovers it — no opencode.json edit. It
supplies the two things the CLI cannot get on its own:
- Per-session plan ownership. OpenCode exports no session id to a tool
shell. Without the plugin, runlist falls back to
OPENCODE_PID, which names the OpenCode process — so every session in one OpenCode instance shares an identity and can release the others' in-session plans. - A session-start briefing, the equivalent of Claude Code's SessionStart
hook. OpenCode's Claude Code compatibility covers skills and the system
prompt, not hooks, so nothing else runs
runlist hud.
Restart OpenCode after installing. The file is version-stamped (runlist-generated:, or
dotmd-generated: from older releases); a dotmd.js without either stamp is
treated as hand-authored and is never overwritten.
The plugin requires a global CLI install because its hooks resolve runlist (or dotmd) from
PATH. A project devDependency is useful for npm scripts but does not put the
CLI on the hook's PATH.
Keep the CLI and plugin aligned with:
runlist update
runlist update --check
runlist update --cli-only
runlist update --plugin-onlyRestart Claude Code, or run /reload-plugins, after a plugin update.
Quick Start
runlist init # create config, docs/, and the generated index
runlist new plan auth-refresh # scaffold a typed document
runlist briefing # compact active-work orientation
runlist plans # live plan dashboard
runlist check # validate schema, references, and lifecycle shape
runlist doctor # preview repairs; add --apply to writerunlist briefing is the compact orientation view. runlist context is the fuller
human/LLM briefing, while runlist agent-context emits bounded structured JSON for
agent integrations.
Core Workflow
runlist briefing
runlist use docs/plans/auth-refresh.md
runlist set awaiting docs/plans/auth-refresh.md --note "Need API owner decision"
runlist set active docs/plans/auth-refresh.md --note "Decision received"
runlist archive docs/plans/auth-refresh.md --note "Shipped and verified"Use runlist set <status> [<file>] for lifecycle changes rather than editing a
status: line. It validates the status for the document type, updates history,
runs lifecycle hooks, repairs references after moves, and synchronizes the
index.
For unfinished session work, save the handoff and release the owned plan in one operation:
runlist baton @/tmp/resume.mdBaton refuses when a handoff for the same work is already pending, so one piece of work never has two resume prompts.
Saved prompts are local session state. Consume them with runlist use; inspect
without consuming via runlist prompts show. Consuming a baton prompt also claims
its plan; runlist use --no-claim reads and archives it without starting the plan.
New plans are created planned; runlist use starts one, and
runlist new plan <name> --status <status> sets a different starting status.
Document Format
---
type: plan
status: active
updated: 2026-07-13
modules:
- auth
surfaces:
- backend
current_state: Token validation is complete.
next_step: Wire refresh rotation into middleware.
related_docs:
- ./auth-design.md
---
# Auth Refresh
- [x] Validate tokens
- [ ] Rotate refresh tokensstatus is the only universally required field. A type enables type-specific
statuses, validation, templates, and briefing behavior. Explicit frontmatter
wins, but runlist can also derive titles, summaries, state, next steps, checklist
progress, and Markdown links from the body.
Use plural modules: and surfaces: arrays. The old singular keys remain
readable for compatibility and can be migrated with runlist lint --fix.
Built-In Types
| Type | Purpose | Default statuses |
|---|---|---|
| plan | Executable work | in-session, active, planned, blocked, partial, paused, awaiting, queued-after, archived |
| doc | Specs, ADRs, audits, and reference material | draft, active, review, reference, deprecated, archived |
| prompt | Saved future-session instructions | pending, archived |
Status definitions can be customized per type. Rich status objects co-locate display, staleness, validation, terminal, and archive behavior in one place.
Runlists And Roadmaps
A sprint runlist is an ordered runlist: array on a hub plan. Scaffold a hub
and children together:
runlist new plan auth-revamp --runlist extract,rewrite,cleanup
runlist runlist auth-revamp
runlist runlist next auth-revampMutate the structure through the CLI so the array, child parent_plan refs, and
body order list remain synchronized:
runlist runlist add auth-revamp docs/plans/existing-plan.md
runlist runlist add auth-revamp follow-up
runlist runlist reorder auth-revamp follow-up --before cleanup
runlist runlist remove auth-revamp extract --clear-parentArchived children count as complete. Parked children (blocked, partial,
paused, awaiting, and queued-after) are skipped when choosing the next
pickup but do not count as done.
For a larger prose-first domain map, create a coordination runlist:
runlist new plan platform-work --coordination
runlist runlistsrunlist new hub platform-work makes the same coordination hub; add
--runlist a,b,c or --roadmap for the other two shapes.
For progress across several runlists, create a roadmap:
runlist new plan platform-roadmap --roadmap
runlist roadmap platform-roadmap
runlist roadmap platform-roadmap nextRoadmaps roll up progress recursively and choose the first startable plan across their child runlists. Runlists and roadmaps are held out of actionable plan counts so dashboards do not double-count their children.
Decisions
A decision is an entry in its plan, not a document of its own. Write the record (the situation, what exists today, what each answer leaves in place) to a file and add it:
runlist new decision auth-revamp --question "Which token store?" @record.mdIt takes the next id (D1, D2, …) that no decision item in that plan uses,
lands at the end of the plan's top-level decisions section (created before
## Version History when there is none) with a Disposition: OPEN. line, and
refuses an empty record. --disposition held parks it instead.
A corpus that indexes its decisions in one register numbers them in one sequence. Name the register, and the id follows the highest the register or the plan uses, and the register gets the entry's row in the same locked write:
export const decisions = {
section: 'Decisions',
prefix: 'D',
register: { file: 'docs/plans/register.md', statusLine: 'waiting on you:' },
};The register block is the fenced block whose first line carries statusLine.
The row is the question plus --answers (what each answer leaves in place),
which is required when a register is configured.
Flags
A flag is something someone found that the person should know about when they come back: a plan that contradicts another, a decision open in one place and ruled in another, a citation that no longer says what it claims. Any session, person or check can add one, and no model is needed:
runlist flag add docs/plans/auth.md:42 "says tokens expire in 1h; the spec says 24h" --severity problem
runlist flags # open flags, problems first, newest first
runlist flag accept F3 --note "real, owner agrees"
runlist flag reject F4 # not a problem; closed
runlist flag resolve F3 # fixed
runlist check --flag # the check's errors become flags, attributed to itEach flag keeps the text of the line it points at, so the list says when that
line has moved or changed since. A repeat of an open flag on the same place is
merged. The log is append-only, .runlist/flags.jsonl by default
(export const flags = { file } moves it), and triage is recorded as events
beside the flag, never over it. Every session start shows a count and the top
open flags.
Safety Model
- Mutation commands support
--dry-run/-n. - Managed writes are confined to configured document roots.
- Lifecycle and multi-file moves use atomic, conflict-aware mutation paths.
- Session ownership is durable local state, not inferred from telemetry.
- Passive orientation commands do not mutate repository state.
- Repository paths in machine and human output use stable slash-normalized identities across supported operating systems.
Command Reference
The CLI is the source of truth for command syntax and options:
runlist --help
runlist help all
runlist help statuses
runlist <command> --helpShell completion is generated from the same command registry:
eval "$(runlist completions bash)"
eval "$(runlist completions zsh)"This README intentionally documents onboarding and concepts instead of duplicating the complete command catalog.
Configuration
Run runlist init to create runlist.config.mjs (a legacy dotmd.config.mjs is still read). A minimal typed configuration:
export const root = 'docs';
export const archiveDir = 'archived';
export const types = {
plan: {
statuses: {
'in-session': { context: 'expanded', staleDays: 1 },
active: { context: 'expanded', staleDays: 14 },
planned: { context: 'listed', staleDays: 30 },
archived: {
context: 'counted',
archive: true,
terminal: true,
skipStale: true,
skipWarnings: true,
},
},
},
};Code references
Archive and rename repair every reference inside the doc roots. codeRoots
extends that to source files that cite a document by its repo-relative path in
a comment, a docstring or a string literal:
export const codeRoots = ['packages', 'scripts', 'services'];
export const codeRefsUntouched = ['scripts/guards/plan-baseline.json'];runlist refs <old> <new> reports every citation by file and line and writes
only on --fix; runlist refs repair does the same for every archived
document still cited at its previous path. Archive and rename print the count
afterwards and take --fix-refs. Only the full repo-relative path matches,
never a bare basename, and only that segment is replaced, so a trailing § or
# anchor survives. A citation inside a string literal waits for --strings,
a file in codeRefsUntouched is never written, and with no codeRoots set
nothing is scanned.
Configuration supports multiple roots, custom types and templates, taxonomy,
reference fields, presets, rendering, lifecycle hooks, validation hooks, and
AI summarization hooks. See runlist.config.example.mjs
for the complete annotated reference.
Hooks
Functions exported from runlist.config.mjs are detected as hooks. They can add
validation, customize rendering and summaries, or react to lifecycle events.
Hooks receive the resolved config and command context; mutation hooks participate
in the command's dry-run and failure contracts.
License
MIT
