dotmd-cli 0.61.0 → 0.63.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 +102 -13
- package/bin/dotmd.mjs +56 -5
- package/package.json +1 -1
- package/src/commands.mjs +1 -1
- package/src/completions.mjs +1 -1
- package/src/health.mjs +56 -9
- package/src/index.mjs +8 -1
- package/src/lifecycle.mjs +45 -13
- package/src/query.mjs +315 -46
- package/src/render.mjs +22 -3
- package/src/runlist.mjs +202 -7
- package/src/validate.mjs +27 -0
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 #
|
|
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 `
|
|
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
|
|
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>
|
|
204
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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 --
|
|
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
|
@@ -44,6 +44,7 @@ const FLAG_SPECS = {
|
|
|
44
44
|
check: { flags: new Set(['--fix', '--errors-only', '--no-collapse', '--json', '--verbose']), values: new Set() },
|
|
45
45
|
doctor: { flags: new Set(['--apply', '--yes', '--dry-run', '-n', '--statuses', '--migrate-template', '--migrate-prompts', '--frontmatter-fix', '--project', '--json', '--include-archived']), values: new Set() },
|
|
46
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']) },
|
|
47
48
|
prompts: {
|
|
48
49
|
flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
|
|
49
50
|
values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
|
|
@@ -235,6 +236,7 @@ Lifecycle:
|
|
|
235
236
|
use <file> Open a plan (mark in-session + print it) or consume a prompt
|
|
236
237
|
set <status> <file> Change a document's status (frontmatter write; archive also moves the file)
|
|
237
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)
|
|
238
240
|
status <file> <status> Transition document status (deprecated; prefer \`set\`)
|
|
239
241
|
archive <file> Archive (status + move + update refs)
|
|
240
242
|
bulk archive <f1> <f2> ... Archive multiple files at once
|
|
@@ -263,7 +265,7 @@ Setup:
|
|
|
263
265
|
Global Options:
|
|
264
266
|
--config <path> Explicit config file path
|
|
265
267
|
--root <name> Filter to a specific docs root
|
|
266
|
-
--type <t1,t2> Filter by document type (plan, doc,
|
|
268
|
+
--type <t1,t2> Filter by document type (plan, doc, prompt)
|
|
267
269
|
--dry-run, -n Preview changes without writing anything
|
|
268
270
|
--verbose Show config details and doc count
|
|
269
271
|
--help, -h Show help (per-command: dotmd <cmd> --help)
|
|
@@ -410,7 +412,7 @@ Examples:
|
|
|
410
412
|
query: `dotmd query — filtered document search
|
|
411
413
|
|
|
412
414
|
Filters:
|
|
413
|
-
--type <t1,t2> Filter by type (plan, doc,
|
|
415
|
+
--type <t1,t2> Filter by type (plan, doc, prompt)
|
|
414
416
|
--status <s1,s2> Filter by status (comma-separated)
|
|
415
417
|
--keyword <term> Search title, summary, state, path
|
|
416
418
|
--body Extend --keyword into document bodies (lazy scan, shows matching-line excerpts)
|
|
@@ -887,7 +889,7 @@ Options:
|
|
|
887
889
|
--format <md|html|json> Output format (default: md)
|
|
888
890
|
--output <path> Write to file/directory (default: stdout for md/json)
|
|
889
891
|
--status <s1,s2> Filter by status
|
|
890
|
-
--type <t1,t2> Filter by type (plan, doc,
|
|
892
|
+
--type <t1,t2> Filter by type (plan, doc, prompt)
|
|
891
893
|
--module <name> Filter by module
|
|
892
894
|
--root <name> Filter by root
|
|
893
895
|
--dry-run, -n Preview without writing`,
|
|
@@ -1212,7 +1214,38 @@ Common shape:
|
|
|
1212
1214
|
---
|
|
1213
1215
|
|
|
1214
1216
|
Child plans should set \`parent_plan:\` back at the hub — \`dotmd check\` warns
|
|
1215
|
-
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
|
+
For these, \`runlist\`/\`runlist next\` also read order from the body when there's
|
|
1230
|
+
no \`runlist:\` array — a \`## Ranked queue\` table or \`## Order of operations\`
|
|
1231
|
+
list of markdown links (the first \`.md\` link per row/item, in order).`,
|
|
1232
|
+
|
|
1233
|
+
runlists: `dotmd runlists — the coordination-hub dashboard
|
|
1234
|
+
|
|
1235
|
+
Lists every *coordination runlist*: a prose-first plan that sits above a
|
|
1236
|
+
cluster of others (a domain map), detected by \`execution_mode: coordination\`
|
|
1237
|
+
or a \`*-runlist\` / \`runlist\` slug. Each row shows the hub, its age, the rough
|
|
1238
|
+
size of its \`related_plans:\` cluster, a \`next → <child>\` when the hub's body
|
|
1239
|
+
encodes order as markdown links (\`## Ranked queue\` table / \`## Order of
|
|
1240
|
+
operations\` list), and a one-line descriptor.
|
|
1241
|
+
|
|
1242
|
+
This is the standalone form of the \`Runlists\` section that \`dotmd plans\`
|
|
1243
|
+
pins beneath the leaf-plan triage list.
|
|
1244
|
+
|
|
1245
|
+
dotmd runlists All runlists (a small bounded set), most stale first.
|
|
1246
|
+
dotmd runlists --sort recent Order by recency instead (age|recent|related|title|status).
|
|
1247
|
+
dotmd runlists --limit N Cap the list at N.
|
|
1248
|
+
dotmd runlists --json Structured rows (path, status, childCount, nextPickup, …).`,
|
|
1216
1249
|
|
|
1217
1250
|
'bulk-tag': `dotmd bulk-tag [files...] — fill in type/status frontmatter on pre-existing markdown
|
|
1218
1251
|
|
|
@@ -1372,6 +1405,16 @@ async function main() {
|
|
|
1372
1405
|
runQuery(index, [...defaults, ...extras], config, { preset: 'plans' });
|
|
1373
1406
|
return;
|
|
1374
1407
|
}
|
|
1408
|
+
// `dotmd runlists` (plural) — the coordination-hub dashboard (the `Runlists`
|
|
1409
|
+
// section of `dotmd plans`, standalone). Distinct from `dotmd runlist <hub>`
|
|
1410
|
+
// (singular), which walks one hub's children.
|
|
1411
|
+
if (command === 'runlists') {
|
|
1412
|
+
const { buildIndex } = await import('../src/index.mjs');
|
|
1413
|
+
const { runRunlists } = await import('../src/query.mjs');
|
|
1414
|
+
const index = buildIndex(config);
|
|
1415
|
+
runRunlists(index, restArgs, config);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1375
1418
|
if (command === 'prompts') {
|
|
1376
1419
|
const { runPrompts } = await import('../src/prompts.mjs');
|
|
1377
1420
|
await runPrompts(restArgs, config, { dryRun, verbose });
|
|
@@ -1722,8 +1765,16 @@ async function main() {
|
|
|
1722
1765
|
const docs = index.docs.filter(d => d.type === 'doc');
|
|
1723
1766
|
const research = index.docs.filter(d => d.type === 'research');
|
|
1724
1767
|
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status)).length;
|
|
1768
|
+
// Coordination hubs are runlists, not actionable plans — split them out of
|
|
1769
|
+
// inSession/active into their own `runlists` array so the JSON mirrors the
|
|
1770
|
+
// rendered briefing. Empty on repos with no coordination hubs.
|
|
1771
|
+
const { buildCoordinationIndex } = await import('../src/runlist.mjs');
|
|
1772
|
+
const coordination = buildCoordinationIndex(index, config);
|
|
1773
|
+
const isHub = (d) => coordination.has(d.path);
|
|
1774
|
+
const closedStatuses = new Set([...config.lifecycle.archiveStatuses, ...config.lifecycle.terminalStatuses]);
|
|
1775
|
+
const isLiveHub = (d) => isHub(d) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
|
|
1725
1776
|
process.stdout.write(JSON.stringify({
|
|
1726
|
-
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 })) },
|
|
1777
|
+
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 })) },
|
|
1727
1778
|
docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
|
|
1728
1779
|
research: { total: research.length, active: research.filter(d => d.status === 'active').length },
|
|
1729
1780
|
stale, errorCount: index.errors.length, warningCount: index.warnings.length,
|
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// templates points at a real command.
|
|
5
5
|
export const KNOWN_COMMANDS = [
|
|
6
6
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context', 'agent-context', 'hud',
|
|
7
|
-
'focus', 'query', 'grep', 'plans', 'prompts', 'stale', 'actionable', 'index', 'status', 'set', 'use', 'next', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor', 'runlist',
|
|
7
|
+
'focus', 'query', 'grep', 'plans', 'prompts', 'stale', 'actionable', 'index', 'status', 'set', 'use', 'next', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor', 'runlist', 'runlists',
|
|
8
8
|
'unblocks', 'health', 'glossary', 'modules', 'module',
|
|
9
9
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
10
10
|
'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
|
package/src/completions.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { die } from './util.mjs';
|
|
|
2
2
|
|
|
3
3
|
const COMMANDS = [
|
|
4
4
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'unblocks', 'health', 'glossary', 'briefing', 'context', 'focus', 'query', 'grep',
|
|
5
|
-
'plans', 'stale', 'actionable', 'index', 'status', 'set', 'archive', 'bulk', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
5
|
+
'plans', 'runlist', 'runlists', 'stale', 'actionable', 'index', 'status', 'set', 'archive', 'bulk', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
6
6
|
'fix-refs', 'notion', 'export', 'summary', 'watch', 'diff', 'init', 'new', 'completions', 'journal',
|
|
7
7
|
];
|
|
8
8
|
|
package/src/health.mjs
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { buildIndex } from './index.mjs';
|
|
3
3
|
import { bold, dim, green, yellow, red } from './color.mjs';
|
|
4
|
+
import { buildCoordinationIndex, hubLabel } from './runlist.mjs';
|
|
5
|
+
import { isArchivedPath } from './util.mjs';
|
|
4
6
|
|
|
5
7
|
export function runHealth(argv, config) {
|
|
6
8
|
const json = argv.includes('--json');
|
|
7
9
|
const index = buildIndex(config);
|
|
8
10
|
|
|
9
11
|
// Only plans (type: plan or untyped docs in plans root)
|
|
10
|
-
const
|
|
12
|
+
const allPlans = index.docs.filter(d => d.type === 'plan' || (!d.type && d.root?.includes('plan')));
|
|
13
|
+
// Coordination hubs (prose-first runlists) are navigation maps, not execution
|
|
14
|
+
// units — they carry no checklist and skew active-plan aging — so lift the
|
|
15
|
+
// LIVE ones out of the pipeline + active set into a dedicated Runlists tally,
|
|
16
|
+
// mirroring `dotmd plans` / `dotmd runlists`. Archived hubs stay in `plans` so
|
|
17
|
+
// the archived/velocity counts are unchanged. No coordination hubs → `plans`
|
|
18
|
+
// equals the full set and every count below is identical to before.
|
|
19
|
+
const coordination = buildCoordinationIndex(index, config);
|
|
20
|
+
const closedStatuses = new Set([
|
|
21
|
+
...(config.lifecycle?.archiveStatuses ?? []),
|
|
22
|
+
...(config.lifecycle?.terminalStatuses ?? []),
|
|
23
|
+
]);
|
|
24
|
+
const isLiveHub = (d) => coordination.has(d.path) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
|
|
25
|
+
const runlistHubs = allPlans.filter(isLiveHub)
|
|
26
|
+
// Most stale first — health is an aging lens, and it matches `dotmd runlists`'
|
|
27
|
+
// default. Unknown-age hubs sort last so they never top the list.
|
|
28
|
+
.sort((a, b) => (b.daysSinceUpdate ?? -1) - (a.daysSinceUpdate ?? -1));
|
|
29
|
+
const plans = allPlans.filter(d => !isLiveHub(d));
|
|
11
30
|
const now = Date.now();
|
|
12
31
|
|
|
13
32
|
// Status distribution
|
|
@@ -68,24 +87,52 @@ export function runHealth(argv, config) {
|
|
|
68
87
|
ready: { count: readyPlans.length },
|
|
69
88
|
planned: { count: plannedPlans.length },
|
|
70
89
|
recentlyArchived: { count: recentlyArchived.length, last30d: recentlyArchived.map(d => path.basename(d.path, '.md')) },
|
|
90
|
+
runlists: { count: runlistHubs.length, hubs: runlistHubs.map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0, nextPickup: coordination.get(d.path)?.nextPickup ?? null })) },
|
|
71
91
|
}, null, 2) + '\n');
|
|
72
92
|
return;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
process.stdout.write(bold('Plan Health') + '\n\n');
|
|
76
96
|
|
|
77
|
-
// Pipeline
|
|
97
|
+
// Pipeline — ordered by the configured status vocab, then any present-but-
|
|
98
|
+
// unconfigured statuses (custom ones a repo defines, by count). Deriving from
|
|
99
|
+
// the live status set means in-session/partial/awaiting/etc. all show, and a
|
|
100
|
+
// dead status never leaves an empty row — unlike the old hand-kept list that
|
|
101
|
+
// drifted out of sync with the vocabulary.
|
|
78
102
|
process.stdout.write(bold('Pipeline:') + '\n');
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
const statusOrder = config.statusOrder ?? [];
|
|
104
|
+
const present = Object.keys(byStatus).filter(s => byStatus[s] > 0);
|
|
105
|
+
const ordered = [
|
|
106
|
+
...statusOrder.filter(s => present.includes(s)),
|
|
107
|
+
...present.filter(s => !statusOrder.includes(s)).sort((a, b) => byStatus[b] - byStatus[a]),
|
|
108
|
+
];
|
|
109
|
+
const pad = Math.max(10, ...ordered.map(s => s.length));
|
|
110
|
+
for (const s of ordered) {
|
|
111
|
+
const count = byStatus[s];
|
|
112
|
+
const bar = '█'.repeat(Math.min(count, 40));
|
|
113
|
+
process.stdout.write(` ${s.padEnd(pad)} ${String(count).padStart(4)} ${dim(bar)}\n`);
|
|
86
114
|
}
|
|
87
115
|
process.stdout.write('\n');
|
|
88
116
|
|
|
117
|
+
// Runlists (coordination hubs) — held out of the leaf-plan pipeline above and
|
|
118
|
+
// surfaced as their own tally so they don't inflate the active count. Newest
|
|
119
|
+
// first, mirroring `dotmd runlists`; capped with a "more" footer.
|
|
120
|
+
if (runlistHubs.length > 0) {
|
|
121
|
+
process.stdout.write(`${bold('Runlists:')} ${runlistHubs.length} ${dim('· dotmd runlists')}\n`);
|
|
122
|
+
for (const doc of runlistHubs.slice(0, 8)) {
|
|
123
|
+
const slug = hubLabel(doc).padEnd(28);
|
|
124
|
+
const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : '?d';
|
|
125
|
+
const info = coordination.get(doc.path);
|
|
126
|
+
const relStr = info?.childCount ? ` ${dim(`${info.childCount} related`)}` : '';
|
|
127
|
+
const nextStr = info?.nextPickup ? ` ${green('→')} ${info.nextPickup.label}` : '';
|
|
128
|
+
process.stdout.write(` ${slug} ${dim(age.padStart(4))}${relStr}${nextStr}\n`);
|
|
129
|
+
}
|
|
130
|
+
if (runlistHubs.length > 8) {
|
|
131
|
+
process.stdout.write(` ${dim(`...and ${runlistHubs.length - 8} more`)}\n`);
|
|
132
|
+
}
|
|
133
|
+
process.stdout.write('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
89
136
|
// Active plan health
|
|
90
137
|
if (activePlans.length > 0) {
|
|
91
138
|
process.stdout.write(bold('Active plans:') + '\n');
|
package/src/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
4
|
import { extractFirstHeading, extractSummary, extractStatusSnapshot, extractNextStep, extractChecklistCounts, extractBodyLinks } from './extractors.mjs';
|
|
5
5
|
import { asString, normalizeStringList, normalizeBlockers, mergeUniqueStrings, toRepoPath, warn, die, resolveDocPath, suggestCandidates } from './util.mjs';
|
|
6
|
-
import { validateDoc, validatePlanShape, validateDocShape, checkBidirectionalReferences, checkGitStaleness, checkRunlistBackPointers, computeDaysSinceUpdate, computeIsStale, computeChecklistCompletionRate, enrichRefErrorSuggestions } from './validate.mjs';
|
|
6
|
+
import { validateDoc, validatePlanShape, validateDocShape, checkBidirectionalReferences, checkGitStaleness, checkRunlistBackPointers, checkCoordinationHubExecutionMode, computeDaysSinceUpdate, computeIsStale, computeChecklistCompletionRate, enrichRefErrorSuggestions } from './validate.mjs';
|
|
7
7
|
import { checkIndex } from './index-file.mjs';
|
|
8
8
|
import { checkClaudeCommands } from './claude-commands.mjs';
|
|
9
9
|
import { checkGlossaryConfig } from './glossary-check.mjs';
|
|
@@ -120,6 +120,13 @@ export function buildIndex(config, opts = {}) {
|
|
|
120
120
|
if (child) child.warnings.push(w);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
const coordHubWarnings = checkCoordinationHubExecutionMode(transformedDocs, config);
|
|
124
|
+
warnings.push(...coordHubWarnings);
|
|
125
|
+
for (const w of coordHubWarnings) {
|
|
126
|
+
const hub = transformedDocs.find(d => d.path === w.path);
|
|
127
|
+
if (hub) hub.warnings.push(w);
|
|
128
|
+
}
|
|
129
|
+
|
|
123
130
|
const gitWarnings = checkGitStaleness(transformedDocs, config);
|
|
124
131
|
warnings.push(...gitWarnings);
|
|
125
132
|
|
package/src/lifecycle.mjs
CHANGED
|
@@ -236,6 +236,10 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
236
236
|
process.stdout.write(`${prefix} Would unfile: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
237
237
|
finalPath = targetPath;
|
|
238
238
|
}
|
|
239
|
+
if (finalPath !== filePath) {
|
|
240
|
+
const refCount = countRefsToUpdate(filePath, finalPath, config);
|
|
241
|
+
if (refCount > 0) process.stdout.write(`${prefix} Would update references in ${refCount} file(s)\n`);
|
|
242
|
+
}
|
|
239
243
|
if ((isArchiving || isUnarchiving || isFiling || isUnfiling) && config.indexPath) {
|
|
240
244
|
process.stdout.write(`${prefix} Would regenerate index\n`);
|
|
241
245
|
}
|
|
@@ -286,6 +290,22 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
286
290
|
finalPath = targetPath;
|
|
287
291
|
}
|
|
288
292
|
|
|
293
|
+
// Any of the four moves above shifts the file's directory, which breaks
|
|
294
|
+
// relative refs in both directions — links FROM the moved file and inbound
|
|
295
|
+
// refs TO it from other docs. runArchive repairs both; mirror that here so
|
|
296
|
+
// the deprecated `dotmd status <file> archived` path and the `dotmd set`
|
|
297
|
+
// unarchive/file/unfile transitions (which route through runStatus, not
|
|
298
|
+
// runArchive) don't silently leave dangling links.
|
|
299
|
+
let selfRefsFixed = false;
|
|
300
|
+
let inboundRefCount = 0;
|
|
301
|
+
let inboundRefPaths = [];
|
|
302
|
+
if (finalPath !== filePath) {
|
|
303
|
+
selfRefsFixed = updateRefsFromMovedFile(filePath, finalPath, config) > 0;
|
|
304
|
+
const inbound = updateRefsAfterMove(filePath, finalPath, config);
|
|
305
|
+
inboundRefCount = inbound.count;
|
|
306
|
+
inboundRefPaths = inbound.paths;
|
|
307
|
+
}
|
|
308
|
+
|
|
289
309
|
// Regen the index on every status change — `active → planned` etc. drift
|
|
290
310
|
// the per-status sections just as much as archive crossings. Archive paths
|
|
291
311
|
// also benefit (replaces the previously-gated regen). `--no-index` skips
|
|
@@ -298,10 +318,13 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
298
318
|
}
|
|
299
319
|
|
|
300
320
|
process.stdout.write(`${green(toRepoPath(finalPath, config.repoRoot))}: ${oldStatus ?? 'unknown'} → ${newStatus}\n`);
|
|
321
|
+
if (selfRefsFixed) process.stdout.write('Updated references in moved file.\n');
|
|
322
|
+
if (inboundRefCount > 0) process.stdout.write(`Updated references in ${inboundRefCount} file(s).\n`);
|
|
301
323
|
|
|
302
324
|
if (showFiles) {
|
|
303
325
|
const touched = [filePath];
|
|
304
326
|
if (finalPath !== filePath) touched.push(finalPath);
|
|
327
|
+
touched.push(...inboundRefPaths);
|
|
305
328
|
if (config.indexPath && !noIndex) touched.push(config.indexPath);
|
|
306
329
|
emitFilesFooter(touched, config);
|
|
307
330
|
}
|
|
@@ -749,6 +772,26 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
749
772
|
try { config.hooks.onTouch?.({ path: toRepoPath(filePath, config.repoRoot) }, { path: toRepoPath(filePath, config.repoRoot), date: today }); } catch (err) { warn(`Hook 'onTouch' threw: ${err.message}`); }
|
|
750
773
|
}
|
|
751
774
|
|
|
775
|
+
// Rewrite every frontmatter ref token (a `*.md` path in a YAML list item or an
|
|
776
|
+
// inline scalar, quoted or `>`-prefixed) that points at `oldPath` so it points
|
|
777
|
+
// at `newPath`. Each token is resolved doc-relative *and* repo-relative and
|
|
778
|
+
// compared to oldPath by absolute path — mirroring how the body-link branch and
|
|
779
|
+
// `updateRefsFromMovedFile` resolve refs. This replaces an older substring
|
|
780
|
+
// rewrite (`fm.split(oldRelPath).join(newRelPath)`) that only knew doc-relative
|
|
781
|
+
// paths, so it: left repo-relative cross-dir refs (`docs/plans/child.md` from
|
|
782
|
+
// `docs/rfcs/spec.md`) broken; mangled same-dir repo-relative refs into
|
|
783
|
+
// `docs/plans/../archived/child.md`; and could corrupt a `grandchild.md` ref
|
|
784
|
+
// when archiving `child.md` (suffix match). oldPath no longer exists on disk
|
|
785
|
+
// post-`git mv`, so existsSync-based resolveRefPath can't be used here.
|
|
786
|
+
function rewriteFrontmatterRefs(fm, docDir, oldPath, newPath, repoRoot) {
|
|
787
|
+
return fm.replace(/[^\s"'<>:]+\.md\b/g, (token) => {
|
|
788
|
+
const docRelAbs = path.resolve(docDir, token);
|
|
789
|
+
const repoRelAbs = path.resolve(repoRoot, token);
|
|
790
|
+
if (docRelAbs !== oldPath && repoRelAbs !== oldPath) return token;
|
|
791
|
+
return path.relative(docDir, newPath).split(path.sep).join('/');
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
752
795
|
/**
|
|
753
796
|
* After a file moves (archive/unarchive), update frontmatter references in all
|
|
754
797
|
* docs that pointed to the old location so they point to the new one.
|
|
@@ -766,17 +809,7 @@ function updateRefsAfterMove(oldPath, newPath, config) {
|
|
|
766
809
|
if (!fm) continue;
|
|
767
810
|
|
|
768
811
|
const docDir = path.dirname(docFile);
|
|
769
|
-
const
|
|
770
|
-
const newRelPath = path.relative(docDir, newPath).split(path.sep).join('/');
|
|
771
|
-
|
|
772
|
-
let newFm = fm;
|
|
773
|
-
if (newFm.includes(oldRelPath)) {
|
|
774
|
-
newFm = newFm.split(oldRelPath).join(newRelPath);
|
|
775
|
-
}
|
|
776
|
-
const dotSlashOld = './' + oldRelPath;
|
|
777
|
-
if (newFm.includes(dotSlashOld)) {
|
|
778
|
-
newFm = newFm.split(dotSlashOld).join(newRelPath);
|
|
779
|
-
}
|
|
812
|
+
const newFm = rewriteFrontmatterRefs(fm, docDir, oldPath, newPath, config.repoRoot);
|
|
780
813
|
|
|
781
814
|
// Body markdown links [text](path.md) or [text](path.md#anchor) pointing
|
|
782
815
|
// at oldPath. resolveRefPath can't be used here: oldPath no longer exists
|
|
@@ -856,8 +889,7 @@ function countRefsToUpdate(oldPath, newPath, config) {
|
|
|
856
889
|
if (!fm) continue;
|
|
857
890
|
|
|
858
891
|
const docDir = path.dirname(docFile);
|
|
859
|
-
const
|
|
860
|
-
const fmHit = fm.includes(oldRelPath) || fm.includes('./' + oldRelPath);
|
|
892
|
+
const fmHit = rewriteFrontmatterRefs(fm, docDir, oldPath, newPath, config.repoRoot) !== fm;
|
|
861
893
|
|
|
862
894
|
let bodyHit = false;
|
|
863
895
|
if (!fmHit) {
|