dotmd-cli 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ dotmd new my-feature # scaffold a new doc with frontmatter
33
33
  dotmd list # index all docs grouped by status
34
34
  dotmd check # validate frontmatter and references
35
35
  dotmd context # compact briefing (great for LLM context)
36
- dotmd doctor # auto-fix everything in one pass
36
+ dotmd doctor # preview fixes for everything (--apply to write)
37
37
  ```
38
38
 
39
39
  ### Shell Completion
@@ -83,7 +83,9 @@ Explicit frontmatter always wins. Body extraction is a cushion for partially-tag
83
83
  ## What It Does
84
84
 
85
85
  - **Index** — group docs by status, with auto-detected progress bars (from `- [ ]` checklists) and next steps
86
- - **Query** — filter by status, keyword, module, surface, owner, staleness
86
+ - **Query** — filter by status, keyword, module, surface, owner, staleness; `dotmd grep` searches document bodies too
87
+ - **Resume handoff** — `dotmd baton` saves a resume prompt for the next session and releases the in-session plan in one verb
88
+ - **Runlists** — group plans into an ordered sequence on a hub plan; `dotmd runlist next` picks up the next one
87
89
  - **Validate** — check for missing fields, broken references, broken body links, stale dates
88
90
  - **Stats** — health dashboard with staleness, completeness, audit coverage
89
91
  - **Graph** — visualize document relationships as text, Graphviz DOT, or JSON
@@ -129,7 +131,7 @@ Design doc content here...
129
131
  - [ ] Add tests
130
132
  ```
131
133
 
132
- The only required field is `status`. Everything else is optional but unlocks more features. The `type` field (`plan`, `doc`, or `research`) enables type-specific statuses and smarter context briefings.
134
+ The only required field is `status`. Everything else is optional but unlocks more features. The `type` field (`plan`, `doc`, or `prompt`) enables type-specific statuses and smarter context briefings.
133
135
 
134
136
  > **Note:** `module:` and `surface:` (singular) are deprecated as of 0.36.3 — use the plural array forms (`modules:`, `surfaces:`). Run `dotmd lint --fix` to migrate existing docs.
135
137
 
@@ -177,6 +179,34 @@ Each *quiet* status (`partial`, `queued-after`, `archived`) is exempt from stale
177
179
 
178
180
  > **Heads-up:** versions before 0.15 included a `done` plan status in the defaults. It saw effectively zero real-world use (plans went `in-session`/`active` → `archived` directly), so it was dropped from the built-in vocabulary. To finish a plan, run `dotmd archive <plan-file>` — or, if you preferred the previous behavior, add `done` back via the `types.plan.statuses` key in your config.
179
181
 
182
+ ### Runlists: ordered groups of plans
183
+
184
+ When several plans must ship in a known order (an "auth revamp" sprint with extract → rewrite → cleanup phases), declare a `runlist:` array on a hub plan instead of chaining `queued-after` per pair or keeping the order in prose:
185
+
186
+ ```yaml
187
+ ---
188
+ type: plan
189
+ status: active
190
+ title: Auth Revamp
191
+ runlist:
192
+ - auth-revamp-01-extract.md
193
+ - auth-revamp-02-rewrite.md
194
+ - auth-revamp-03-cleanup.md
195
+ ---
196
+ ```
197
+
198
+ ```bash
199
+ dotmd runlist <hub> # children + statuses in order; first non-archived child marked →
200
+ dotmd runlist next <hub> # pick up the next child (marks in-session + prints it)
201
+ dotmd runlists # dashboard of coordination-hub runlists (--json, --limit N)
202
+ ```
203
+
204
+ `runlist next` stops with a runlist-aware error if the next child isn't in a workable status (`active` / `planned` / `in-session`), so you resolve the blocker before continuing. Each child should set `parent_plan:` pointing back at the hub — `dotmd check` warns when it doesn't. There's no separate doc type: a runlist hub is just a plan with the array.
205
+
206
+ In `dotmd plans`, hubs are tagged `[RUNLIST]` (not `[ACTIVE]`) with their children folded underneath — the hub row shows `done/total` progress and the next-pickup `→`, so a multi-plan sprint reads as one runlist instead of cluttering the triage list. A child whose hub is filtered out of the view (e.g. `--status active` when the hub is `planned`) still renders on its own.
207
+
208
+ **Coordination runlists.** A `runlist:` array fits a small ordered *sprint*. For a large, prose-first *coordination map* (a domain hub pointing at many plans, with gating/sequence rationale, sometimes unordered), set `execution_mode: coordination` instead — or just name it `*-runlist`. These aren't folded: `dotmd plans` lifts them into a pinned `Runlists` section and out of the active count, and `dotmd runlists` shows that dashboard standalone. `dotmd briefing` and `dotmd health` do the same — coordination hubs are pulled out of the live/active counts into a `runlists` bucket (briefing) and a held-out `Runlists:` tally (health), so they don't inflate the actionable-plan or aging numbers. The per-hub "N related" count comes from `related_plans:`. `dotmd check` nudges a `*-runlist` hub missing `execution_mode: coordination`.
209
+
180
210
  ## Commands
181
211
 
182
212
  ```
@@ -191,27 +221,36 @@ dotmd unblocks <file> Show what depends on this doc
191
221
  dotmd health [--json] Plan velocity, aging, and pipeline
192
222
  dotmd briefing Compact summary for session start
193
223
  dotmd context [--summarize] Full briefing (LLM-oriented)
224
+ dotmd agent-context Compact bounded JSON context for agents
194
225
  dotmd focus [status] Detailed view for one status group
195
- dotmd query [filters] Filtered search
196
- dotmd plans List all plans
226
+ dotmd query [filters] Filtered search (--body scans document bodies)
227
+ dotmd grep <term> Keyword search incl. bodies — "which doc discussed X?"
228
+ dotmd plans List live plans (excludes archived)
197
229
  dotmd modules Module dashboard (plans grouped by module)
198
230
  dotmd module <name> Plans for one module, grouped by status
231
+ dotmd surfaces List configured surface taxonomy
199
232
  dotmd stale List stale docs
200
233
  dotmd actionable List docs with next steps
201
234
  dotmd index [--print] Generate/update docs.md index block
202
235
  dotmd hud Actionable triage (silent when clean — ideal SessionStart hook)
203
- dotmd use <file> Open a plan (in-session + print it) or consume a prompt
204
- dotmd set <status> <file> Change a document's status (frontmatter write)
236
+ dotmd use [<file-or-slug>] Open by type: prompt → consume, plan → start, doc → read
237
+ (no arg: consume the oldest pending prompt)
238
+ dotmd set <status> <file> Change a document's status (--note appends why to Version History)
239
+ dotmd baton [<plan>|<slug>] <@draft|-> Save a resume prompt; releases the in-session plan
240
+ dotmd runlist <hub> [next] Show or walk an ordered group of plans
205
241
  dotmd status <file> <status> Transition document status (deprecated; prefer set)
206
242
  dotmd archive <file> Archive (status + move + update refs)
207
243
  dotmd bulk archive <files> Archive multiple files at once
244
+ dotmd bulk-tag [files] Tag pre-existing untagged .md files
208
245
  dotmd touch <file> Bump updated date
209
246
  dotmd touch --git Bulk-sync dates from git history
210
- dotmd doctor Auto-fix everything in one pass
247
+ dotmd doctor [--apply] Fix refs, lint, dates, index (previews by default)
248
+ dotmd self-check Project/version skew diagnostic
211
249
  dotmd fix-refs Auto-fix broken reference paths
212
250
  dotmd lint [--fix] Check and auto-fix frontmatter issues
213
251
  dotmd rename <old> <new> Rename doc and update references
214
252
  dotmd migrate <f> <old> <new> Batch update a frontmatter field
253
+ dotmd ship [patch|minor|major] Regen + commit + bump in one step
215
254
  dotmd notion <sub> [db-id] Notion import/export/sync
216
255
  dotmd export [file] Export docs as md, html, or json
217
256
  dotmd summary <file> AI summary of a document
@@ -219,19 +258,22 @@ dotmd glossary <term> Look up domain terms + related docs
219
258
  dotmd watch [command] Re-run a command on file changes
220
259
  dotmd diff [file] Show changes since last updated date
221
260
  dotmd new <type> <name> Create a new doc (type: doc, plan, or prompt)
222
- dotmd prompts [sub] Manage saved prompts (list, next, use, hold, archive, new)
261
+ dotmd prompts [sub] Manage saved prompts (list, show, hold, archive, new)
262
+ dotmd statuses [sub] Manage per-project status taxonomy
223
263
  dotmd journal [flags] View opt-in command-usage journal (DOTMD_JOURNAL=1)
224
264
  dotmd init Create starter config + docs directory
225
265
  dotmd completions <shell> Output shell completion script (bash, zsh)
226
266
  ```
227
267
 
268
+ Run `dotmd help all` for the always-current version of this list, `dotmd help statuses` for the status vocabulary, and `dotmd <cmd> --help` for per-command details.
269
+
228
270
  ### Global Flags
229
271
 
230
272
  ```
231
273
  --config <path> Explicit config file path
232
274
  --dry-run, -n Preview changes without writing anything
233
275
  --root <name> Filter to a specific docs root
234
- --type <t1,t2> Filter by document type (plan, doc, research)
276
+ --type <t1,t2> Filter by document type (plan, doc, prompt, or custom)
235
277
  --verbose Show resolved config details
236
278
  --help, -h Show help (per-command with: dotmd <cmd> --help)
237
279
  --version, -v Show version
@@ -246,9 +288,12 @@ dotmd query --stale --sort updated --all
246
288
  dotmd query --surface backend --checklist-open
247
289
  dotmd query --status active --summarize # AI summaries
248
290
  dotmd query --status active --summarize --summarize-limit 3
291
+ dotmd query --keyword "retries" --body # scan document bodies too
249
292
  ```
250
293
 
251
- Flags: `--type`, `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`, `--summarize`, `--summarize-limit`, `--model`.
294
+ Flags: `--type`, `--status`, `--keyword`, `--body`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`, `--summarize`, `--summarize-limit`, `--model`.
295
+
296
+ `dotmd grep <term>` is the "which doc discussed X?" shorthand — an alias for `dotmd query --keyword <term> --body --all` that prints doc cards plus line-numbered excerpts per body hit. Bodies are read lazily (frontmatter filters run first), and it composes with the usual query flags.
252
297
 
253
298
  ### Create Documents
254
299
 
@@ -311,9 +356,19 @@ dotmd new prompt from-file @/tmp/draft.md
311
356
 
312
357
  Manage them with the `prompts` command family:
313
358
 
359
+ Consume one with `dotmd use` — it atomically prints the body and archives the prompt so it can't be double-consumed:
360
+
361
+ ```bash
362
+ dotmd use # consume the oldest pending prompt
363
+ dotmd use <file-or-slug> # consume a specific prompt
364
+ ```
365
+
366
+ Admin verbs live under the `prompts` namespace:
367
+
314
368
  ```bash
315
369
  dotmd prompts # list pending prompts (default)
316
370
  dotmd prompts list --all # all statuses
371
+ dotmd prompts show <file> # read-only peek: print the body WITHOUT consuming
317
372
  dotmd prompts next # print body of oldest pending + auto-archive (one-shot)
318
373
  dotmd prompts use <file> # print body of a specific prompt + auto-archive
319
374
  dotmd prompts hold <file> # park a prompt (status → held) under prompts/held/:
@@ -328,6 +383,31 @@ dotmd prompts new <name> [body] # alias for `dotmd new prompt`
328
383
 
329
384
  Statuses: `pending` (drafted, awaiting a session), `held` (saved but parked under `prompts/held/` — visible in `prompts list`, hidden from `hud`/`briefing`, skipped by `prompts next`), `archived` (consumed or filed away). `shelved` is a legacy spelling accepted for older files; `claimed` is reserved for a future "in-flight" state but is currently a synonym for archived in practice.
330
385
 
386
+ ### Baton: resume prompts & session handoff
387
+
388
+ `dotmd baton` is the "save a resume prompt" verb — the way one session hands work to the next without pasting resume text into chat. Write a short draft (the next concrete decision plus any gotchas, not a recap), then:
389
+
390
+ ```bash
391
+ dotmd baton @/tmp/draft.md # plan mode: a plan is in-session
392
+ ```
393
+
394
+ In plan mode, baton does the whole closeout in one call:
395
+
396
+ 1. Saves a resume prompt named `resume-<plan-slug>` (collision-safe: `-2`, `-3`, …) under `docs/prompts/` with `status: pending`.
397
+ 2. Releases the plan: one status flip, `in-session` → `active` by default (`--status paused|awaiting|partial|blocked` to override, `--note "why"` to record the reason in `## Version History`).
398
+ 3. Prints the exact `git commit` command for the plan's frontmatter change — the prompt stays out of the pathspec, because saved prompts are session-local.
399
+
400
+ Baton resolves *your* plan via the command journal (or takes it explicitly: `dotmd baton <plan-file> @draft`), falling back to the only in-session plan.
401
+
402
+ No plan involved? Slug mode saves the prompt and touches nothing else:
403
+
404
+ ```bash
405
+ dotmd baton checkout-fixes @/tmp/draft.md # saves resume-checkout-fixes; no status changes
406
+ cat /tmp/draft.md | dotmd baton # body from stdin
407
+ ```
408
+
409
+ Either way, the next session's `dotmd hud` surfaces the pending prompt, and `dotmd use` consumes it.
410
+
331
411
  ### Command Journal (opt-in)
332
412
 
333
413
  dotmd's primary user is an agent. Every CLI invocation can be journaled
@@ -403,8 +483,8 @@ Shows: status counts, staleness, errors/warnings, freshness (today/week/month),
403
483
  ### Doctor
404
484
 
405
485
  ```bash
406
- dotmd doctor # fix refs → lint → sync git dates → regen index
407
- dotmd doctor --dry-run # preview all changes
486
+ dotmd doctor # preview: fix refs → lint → sync git dates → regen index
487
+ dotmd doctor --apply # actually write the fixes (previews by default since 0.37.0)
408
488
  dotmd doctor --statuses # detect overloaded status buckets (read-only)
409
489
  dotmd doctor --statuses --json # machine-readable suggestions
410
490
  ```
@@ -607,6 +687,15 @@ dotmd archive docs/plans/my-plan.md --closeout-template # also inject ## Close
607
687
  `in-session` is a status like any other — `dotmd set <status> <file>` writes it
608
688
  to the file's frontmatter and does nothing else.
609
689
 
690
+ Add `--note "why"` to any `set` or `archive` to append the reason to the doc's
691
+ `## Version History` section in the same call (creates the section if missing) —
692
+ it saves the status-change + worklog-edit round-trip. `set partial` without a
693
+ note or successor link prints a reminder.
694
+
695
+ To stop mid-work and hand off to a future session, use `dotmd baton` (see
696
+ [Baton](#baton-resume-prompts--session-handoff)) — it saves the resume prompt
697
+ and releases the plan in one verb.
698
+
610
699
  **Recommended Claude Code hook** — add to `~/.claude/settings.json`
611
700
  (or your project's `.claude/settings.json`):
612
701
 
package/bin/dotmd.mjs CHANGED
@@ -36,16 +36,19 @@ const FLAG_SPECS = {
36
36
  context: { flags: new Set(['--json', '--compact', '--summarize', '--model']), values: new Set(['--model']) },
37
37
  'agent-context': { flags: new Set(['--json']), values: new Set() },
38
38
  hud: { flags: new Set(['--json', '--subagent']), values: new Set() },
39
+ // '-' is the stdin marker (a positional, not a flag) — listed so validation lets it through.
40
+ baton: { flags: new Set(['--status', '--note', '--body', '--message', '--dry-run', '-n', '-']), values: new Set(['--status', '--note', '--body', '--message']) },
39
41
  guard: { flags: new Set(), values: new Set() },
40
42
  misuse: { flags: new Set(['--json', '--tail', '--by-rule', '--repo']), values: new Set(['--tail', '--repo']) },
41
43
  update: { flags: new Set(['--check', '--cli-only', '--plugin-only']), values: new Set() },
42
44
  check: { flags: new Set(['--fix', '--errors-only', '--no-collapse', '--json', '--verbose']), values: new Set() },
43
45
  doctor: { flags: new Set(['--apply', '--yes', '--dry-run', '-n', '--statuses', '--migrate-template', '--migrate-prompts', '--frontmatter-fix', '--project', '--json', '--include-archived']), values: new Set() },
44
46
  runlist: { flags: new Set(['--json', '--full', '--no-index', '--show-files']), values: new Set(), subcommands: new Set(['next']) },
47
+ runlists: { flags: new Set(['--json', '--limit', '--sort']), values: new Set(['--limit', '--sort']) },
45
48
  prompts: {
46
49
  flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
47
50
  values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
48
- subcommands: new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve', 'status']),
51
+ subcommands: new Set(['list', 'next', 'use', 'resume', 'show', 'peek', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve', 'status']),
49
52
  },
50
53
  };
51
54
 
@@ -133,6 +136,7 @@ Common commands:
133
136
  set <status> [file] Transition status (start work, finish, archive — all via target status)
134
137
  new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
135
138
  use [<file-or-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
139
+ baton [<plan>|<slug>] <@draft|-> Save a resume prompt (+ release the plan, if one is in-session)
136
140
  (no file: consume oldest pending prompt)
137
141
  archive <file> Close out a plan (status → archived, move, update refs)
138
142
 
@@ -203,7 +207,8 @@ View & Query:
203
207
  grep <term> Keyword search incl. document bodies (query --keyword --body --all)
204
208
  plans Live plans (excludes archived; --include-archived for all)
205
209
  use [<file-or-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
206
- prompts [list|archive|new|hold] Prompt admin (list / archive / save / hold). Use \`dotmd use\` to consume.
210
+ baton [<plan>|<slug>] <@draft|-> Save a resume prompt; releases the plan + prints the commit when one is in-session
211
+ prompts [list|show|archive|new|hold] Prompt admin (list / peek / archive / save / hold). Use \`dotmd use\` to consume.
207
212
  stale Stale docs (preset)
208
213
  actionable Docs with next steps (preset)
209
214
 
@@ -231,6 +236,7 @@ Lifecycle:
231
236
  use <file> Open a plan (mark in-session + print it) or consume a prompt
232
237
  set <status> <file> Change a document's status (frontmatter write; archive also moves the file)
233
238
  runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
239
+ runlists List coordination-hub runlists (the Runlists dashboard)
234
240
  status <file> <status> Transition document status (deprecated; prefer \`set\`)
235
241
  archive <file> Archive (status + move + update refs)
236
242
  bulk archive <f1> <f2> ... Archive multiple files at once
@@ -259,7 +265,7 @@ Setup:
259
265
  Global Options:
260
266
  --config <path> Explicit config file path
261
267
  --root <name> Filter to a specific docs root
262
- --type <t1,t2> Filter by document type (plan, doc, research)
268
+ --type <t1,t2> Filter by document type (plan, doc, prompt)
263
269
  --dry-run, -n Preview changes without writing anything
264
270
  --verbose Show config details and doc count
265
271
  --help, -h Show help (per-command: dotmd <cmd> --help)
@@ -406,7 +412,7 @@ Examples:
406
412
  query: `dotmd query — filtered document search
407
413
 
408
414
  Filters:
409
- --type <t1,t2> Filter by type (plan, doc, research)
415
+ --type <t1,t2> Filter by type (plan, doc, prompt)
410
416
  --status <s1,s2> Filter by status (comma-separated)
411
417
  --keyword <term> Search title, summary, state, path
412
418
  --body Extend --keyword into document bodies (lazy scan, shows matching-line excerpts)
@@ -603,7 +609,8 @@ Recommended SessionStart hook (in ~/.claude/settings.json):
603
609
  "SessionStart": [{ "hooks": [{ "type": "command", "command": "dotmd hud", "timeout": 5 }] }]
604
610
 
605
611
  Options:
606
- --json Output as JSON ({ owned, queued, prompts, stale })`,
612
+ --json Output as JSON ({ owned, prompts, errors, previousSelf,
613
+ fleet, recentRejections, misuseRecap, drift })`,
607
614
 
608
615
  briefing: `dotmd briefing — compact summary for session start
609
616
 
@@ -882,7 +889,7 @@ Options:
882
889
  --format <md|html|json> Output format (default: md)
883
890
  --output <path> Write to file/directory (default: stdout for md/json)
884
891
  --status <s1,s2> Filter by status
885
- --type <t1,t2> Filter by type (plan, doc, research)
892
+ --type <t1,t2> Filter by type (plan, doc, prompt)
886
893
  --module <name> Filter by module
887
894
  --root <name> Filter by root
888
895
  --dry-run, -n Preview without writing`,
@@ -993,6 +1000,8 @@ Subcommands:
993
1000
  targets the named prompt instead of picking oldest)
994
1001
  resume <file-or-slug> Alias for \`use\` — same behavior, easier name
995
1002
  when continuing a session
1003
+ show <file-or-slug> Read-only peek: print the body WITHOUT consuming
1004
+ (triage). \`peek\` is an alias.
996
1005
  archive <file-or-slug> Archive a prompt without printing its body
997
1006
  hold <file-or-slug> Park a prompt (status → held) under prompts/held/:
998
1007
  kept in list, hidden from hud/briefing pending
@@ -1023,10 +1032,50 @@ Examples:
1023
1032
  claude "$(dotmd prompts resume resume-foo)" # \`resume\` is an alias for \`use\`
1024
1033
  dotmd prompt list # singular alias for \`dotmd prompts list\`
1025
1034
 
1035
+ dotmd prompts show resume-foo # peek without consuming (triage)
1026
1036
  dotmd prompts next --dry-run # preview without consuming
1027
1037
  dotmd prompts archive old-thing
1028
1038
  dotmd prompts new my-prompt "Body text here"`,
1029
1039
 
1040
+ baton: `dotmd baton — save a resume prompt for whatever you're doing (and release the plan, if there is one)
1041
+
1042
+ The "save a resume prompt" verb. Works mid-anything:
1043
+
1044
+ Plan mode (a plan is in-session, or you pass one):
1045
+ 1. Saves a resume prompt named resume-<plan-slug> (collision-safe: -2, -3, …).
1046
+ The prompt is session-local — the next session's hud surfaces it; never
1047
+ paste resume text into chat.
1048
+ 2. Releases the plan: one status flip, in-session → active by default
1049
+ (--status to override, --note to record why in ## Version History).
1050
+ 3. Prints the exact \`git commit\` for the plan's frontmatter change — the
1051
+ prompt stays OUT of the pathspec (it's session-local, often gitignored).
1052
+ Which plan? Pass it explicitly, or baton resolves the one THIS session marked
1053
+ in-session (via the journal), falling back to the only in-session plan.
1054
+
1055
+ Slug mode (no plan involved — "save a resume prompt for this"):
1056
+ dotmd baton <slug> @/tmp/draft.md → saves resume-<slug>, touches NOTHING
1057
+ else: no status changes, no commit, no plan required. Reference any relevant
1058
+ plans/docs inside the draft body.
1059
+
1060
+ Usage:
1061
+ dotmd baton [<plan-file> | <slug>] [@draft.md | - | --message "..."]
1062
+
1063
+ Options:
1064
+ --status <s> Target status for the plan (default: active; plan mode only)
1065
+ --note "why" Append the reason to ## Version History (plan mode only)
1066
+ --message / --body Inline body (one-liners; prefer @path or stdin)
1067
+ --dry-run, -n Preview without writing
1068
+
1069
+ Examples:
1070
+ dotmd baton @/tmp/draft.md # owned plan, body from file
1071
+ dotmd baton checkout-fixes @/tmp/draft.md # no plan: just save resume-checkout-fixes
1072
+ cat /tmp/draft.md | dotmd baton # body from stdin
1073
+ dotmd baton docs/plans/auth.md @/tmp/draft.md # explicit plan
1074
+ dotmd baton --status paused --note "blocked on review" @/tmp/d.md
1075
+
1076
+ Write the draft FIRST (10–20 lines): the next concrete decision plus any
1077
+ gotchas — not a recap of the plan body.`,
1078
+
1030
1079
  stale: `dotmd stale — list stale documents
1031
1080
 
1032
1081
  Shows docs that haven't been updated within their staleness threshold.
@@ -1165,7 +1214,33 @@ Common shape:
1165
1214
  ---
1166
1215
 
1167
1216
  Child plans should set \`parent_plan:\` back at the hub — \`dotmd check\` warns
1168
- when they don't.`,
1217
+ when they don't.
1218
+
1219
+ In \`dotmd plans\`, a hub is tagged \`[RUNLIST]\` (not \`[ACTIVE]\`) and its
1220
+ children fold underneath it — progress (\`done/total\`) and the next pickup
1221
+ \`→\` show on the hub row, so a sprint reads as one runlist instead of N loose
1222
+ plans. Children whose hub is filtered out of the view (e.g. \`--status active\`
1223
+ when the hub is \`planned\`) still render on their own.
1224
+
1225
+ Larger, prose-first "coordination" runlists (a domain map pointing at many
1226
+ plans, marked \`execution_mode: coordination\` or named \`*-runlist\`) aren't
1227
+ folded — they're lifted into a separate \`Runlists\` section in \`dotmd plans\`
1228
+ and out of the active count. \`dotmd runlists\` shows that dashboard on its own.`,
1229
+
1230
+ runlists: `dotmd runlists — the coordination-hub dashboard
1231
+
1232
+ Lists every *coordination runlist*: a prose-first plan that sits above a
1233
+ cluster of others (a domain map), detected by \`execution_mode: coordination\`
1234
+ or a \`*-runlist\` / \`runlist\` slug. Each row shows the hub, its age, the rough
1235
+ size of its \`related_plans:\` cluster, and a one-line descriptor.
1236
+
1237
+ This is the standalone form of the \`Runlists\` section that \`dotmd plans\`
1238
+ pins beneath the leaf-plan triage list.
1239
+
1240
+ dotmd runlists All runlists (a small bounded set), most stale first.
1241
+ dotmd runlists --sort recent Order by recency instead (age|recent|related|title|status).
1242
+ dotmd runlists --limit N Cap the list at N.
1243
+ dotmd runlists --json Structured rows (path, status, childCount, …).`,
1169
1244
 
1170
1245
  'bulk-tag': `dotmd bulk-tag [files...] — fill in type/status frontmatter on pre-existing markdown
1171
1246
 
@@ -1325,6 +1400,16 @@ async function main() {
1325
1400
  runQuery(index, [...defaults, ...extras], config, { preset: 'plans' });
1326
1401
  return;
1327
1402
  }
1403
+ // `dotmd runlists` (plural) — the coordination-hub dashboard (the `Runlists`
1404
+ // section of `dotmd plans`, standalone). Distinct from `dotmd runlist <hub>`
1405
+ // (singular), which walks one hub's children.
1406
+ if (command === 'runlists') {
1407
+ const { buildIndex } = await import('../src/index.mjs');
1408
+ const { runRunlists } = await import('../src/query.mjs');
1409
+ const index = buildIndex(config);
1410
+ runRunlists(index, restArgs, config);
1411
+ return;
1412
+ }
1328
1413
  if (command === 'prompts') {
1329
1414
  const { runPrompts } = await import('../src/prompts.mjs');
1330
1415
  await runPrompts(restArgs, config, { dryRun, verbose });
@@ -1339,6 +1424,14 @@ async function main() {
1339
1424
  await runUse(restArgs, config, { dryRun });
1340
1425
  return;
1341
1426
  }
1427
+ // `dotmd baton [plan] <@draft|->` — the one-command handoff: save the resume
1428
+ // prompt, release the plan (one status flip), print the exact commit. See
1429
+ // src/baton.mjs for why this is a single verb and not a skill choreography.
1430
+ if (command === 'baton') {
1431
+ const { runBaton } = await import('../src/baton.mjs');
1432
+ await runBaton(restArgs, config, { dryRun });
1433
+ return;
1434
+ }
1342
1435
  // `dotmd next` is a top-level alias for `dotmd use` with no arg — consume
1343
1436
  // the oldest pending prompt. Wired separately so agents who reach for the
1344
1437
  // literal verb "next" don't bounce off an Unknown-command. Any positional
@@ -1667,8 +1760,16 @@ async function main() {
1667
1760
  const docs = index.docs.filter(d => d.type === 'doc');
1668
1761
  const research = index.docs.filter(d => d.type === 'research');
1669
1762
  const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status)).length;
1763
+ // Coordination hubs are runlists, not actionable plans — split them out of
1764
+ // inSession/active into their own `runlists` array so the JSON mirrors the
1765
+ // rendered briefing. Empty on repos with no coordination hubs.
1766
+ const { buildCoordinationIndex } = await import('../src/runlist.mjs');
1767
+ const coordination = buildCoordinationIndex(index, config);
1768
+ const isHub = (d) => coordination.has(d.path);
1769
+ const closedStatuses = new Set([...config.lifecycle.archiveStatuses, ...config.lifecycle.terminalStatuses]);
1770
+ const isLiveHub = (d) => isHub(d) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
1670
1771
  process.stdout.write(JSON.stringify({
1671
- plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session').map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active').map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })) },
1772
+ plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), runlists: plans.filter(isLiveHub).map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0 })) },
1672
1773
  docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
1673
1774
  research: { total: research.length, active: research.filter(d => d.status === 'active').length },
1674
1775
  stale, errorCount: index.errors.length, warningCount: index.warnings.length,
@@ -193,7 +193,11 @@ export const presets = {
193
193
  // Properties:
194
194
  // description: string — shown in `dotmd new --list-types`
195
195
  // defaultStatus: string — initial status if `--status` not passed
196
- // requiresBody: boolean — error if no body input (see `prompt` builtin)
196
+ // acceptsBody: boolean — allow body input (inline / --body / @file / piped stdin).
197
+ // REQUIRED if you want `cat draft.md | dotmd new <type> <slug>` (or @path,
198
+ // --body, heredoc) to work. Your `body` fn must also interpolate the input,
199
+ // e.g. `${ctx?.bodyInput?.trim() ?? ''}`. See the body-acceptance guard below.
200
+ // requiresBody: boolean — error if no body input (implies acceptsBody; see `prompt` builtin)
197
201
  // targetRoot: string — name (basename or suffix) of the root this type lives in.
198
202
  // In flat-array `root` configs (e.g. ['docs/plans', 'docs/prompts']),
199
203
  // the new doc lands in the matching root. Falls back to `config.docsRoot`
@@ -203,7 +207,17 @@ export const presets = {
203
207
  // under `docsRoot='docs'`, `dir` puts files in `docs/plans/` and `docs/prompts/`;
204
208
  // under flat-array roots, `targetRoot` routes directly to the type-specific root.
205
209
  // frontmatter: (status, isoTime, ctx) => string
206
- // body: (title, ctx) => string
210
+ // body: (title, ctx) => string — slot user-supplied body via `ctx.bodyInput`
211
+ //
212
+ // Body-acceptance guard (the #1 custom-template gotcha):
213
+ // When you override a builtin and supply your OWN `body` fn that NEVER references
214
+ // `bodyInput`, dotmd assumes the fn would silently discard piped input — so it strips
215
+ // the inherited `acceptsBody`/`requiresBody` and rejects body input with a fail-fast
216
+ // error. Two ways to keep piped/@path/heredoc bodies working in a custom template:
217
+ // 1. interpolate `${ctx?.bodyInput?.trim() ?? ''}` somewhere in your `body` fn, OR
218
+ // 2. set `acceptsBody: true` explicitly (do BOTH if you want the input to actually land).
219
+ // A `body: (t) =>` that ignores `ctx` is the classic trap — it scaffolds fine but
220
+ // `dotmd new <type> <slug> < draft.md` errors until you wire in `bodyInput`.
207
221
  //
208
222
  // Custom type example — adds a `spike` type that lives in the `spikes` root (or
209
223
  // under `docs/spikes/` in single-root layouts):
@@ -218,8 +232,11 @@ export const presets = {
218
232
  // },
219
233
  //
220
234
  // // Override a builtin (e.g. project-specific prompt frontmatter shape).
221
- // // IMPORTANT: builtin properties are NOT inherited — re-declare `targetRoot`, `dir`,
222
- // // `defaultStatus`, `requiresBody`, etc. that you want preserved.
235
+ // // Overrides shallow-merge OVER the builtin: any property you omit is inherited
236
+ // // (`targetRoot`, `dir`, `defaultStatus`, `requiresBody`, …), and anything you declare
237
+ // // wins. EXCEPTION: if you supply your own `body` fn that doesn't reference `bodyInput`,
238
+ // // the inherited `acceptsBody`/`requiresBody` are dropped (see the guard above) — so
239
+ // // re-declare `acceptsBody: true` and wire in `ctx.bodyInput` if you want piped bodies.
223
240
  // // prompt: {
224
241
  // // description: 'Project resume prompt',
225
242
  // // defaultStatus: 'pending',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",