dotmd-cli 0.50.1 → 0.51.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
@@ -187,9 +187,9 @@ dotmd stale List stale docs
187
187
  dotmd actionable List docs with next steps
188
188
  dotmd index [--print] Generate/update docs.md index block
189
189
  dotmd hud Actionable triage (silent when clean — ideal SessionStart hook)
190
- dotmd pickup <file> Pick up a plan (in-session + print body)
191
- dotmd release [<file>] Release in-session lease (aliases: unpickup, finish)
192
- dotmd status <file> <status> Transition document status
190
+ dotmd use <file> Open a plan (in-session + print it) or consume a prompt
191
+ dotmd set <status> <file> Change a document's status (frontmatter write)
192
+ dotmd status <file> <status> Transition document status (deprecated; prefer set)
193
193
  dotmd archive <file> Archive (status + move + update refs)
194
194
  dotmd bulk archive <files> Archive multiple files at once
195
195
  dotmd touch <file> Bump updated date
@@ -311,7 +311,7 @@ dotmd prompts archive <file> # archive without printing the body
311
311
  dotmd prompts new <name> [body] # alias for `dotmd new prompt`
312
312
  ```
313
313
 
314
- `dotmd hud` surfaces pending prompts on session start (alongside held leases), so a saved prompt acts as a self-addressed reminder: write it now, the next session sees it. Held prompts are kept out of the SessionStart surface — use them for "saved but not next."
314
+ `dotmd hud` surfaces pending prompts on session start, so a saved prompt acts as a self-addressed reminder: write it now, the next session sees it. Held prompts are kept out of the SessionStart surface — use them for "saved but not next."
315
315
 
316
316
  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.
317
317
 
@@ -575,60 +575,26 @@ dotmd bulk archive docs/old-a.md docs/old-b.md # archive multiple
575
575
  dotmd bulk archive docs/old-*.md -n # preview
576
576
  ```
577
577
 
578
- ### Pickup & Closeout
578
+ ### Open & Closeout
579
579
 
580
- ```bash
581
- dotmd pickup docs/plans/my-plan.md # set in-session + print body
582
- dotmd archive docs/plans/my-plan.md # fully shipped: archive + auto-release lease
583
- dotmd archive docs/plans/my-plan.md --closeout-template # also inject ## Closeout skeleton
584
- dotmd release docs/plans/my-plan.md # need more work: release lease, flip to prior status
585
- dotmd status docs/plans/my-plan.md partial # shipped + tail deferred (reference successors in body)
586
- dotmd status docs/plans/my-plan.md awaiting # stuck on a human decision
587
- ```
588
-
589
- `finish` is an alias for `release`, kept for older agent instructions that use
590
- that verb for closeout. To fully close shipped work, archive it. To keep working
591
- later, release it back to the prior status or use `dotmd set <status> <file>`.
592
-
593
- ### Session leases & release
594
-
595
- `dotmd pickup` records a lease at `<repoRoot>/.dotmd/in-session.json` that
596
- identifies which Claude session owns the plan. The lease enables three
597
- distinct outcomes when a plan is already `in-session`:
598
-
599
- - **Same session re-attach.** A fresh `dotmd pickup` of a plan you already
600
- hold (e.g., after `/clear` or auto-compaction) silently re-attaches and
601
- re-prints the body. No conflict.
602
- - **Cross-session conflict.** If another live session holds the plan,
603
- pickup refuses with `Held by <host>/<session> (pid <pid>) since <time>`.
604
- - **Reclaimable lease.** If the holder's same-host pid is dead, or the lease is
605
- older than 4 hours, pickup can reclaim it without `--takeover`.
606
-
607
- Releasing leases (both names work; `release` is the recommended verb):
580
+ Status is just frontmatter. There's no checkout, lock, or lease — opening a
581
+ plan, transitioning it, and closing it are all plain status writes (archive
582
+ also moves the file).
608
583
 
609
584
  ```bash
610
- dotmd release # release every lease owned by current session
611
- dotmd release docs/plans/foo.md # release that one (refuses cross-session)
612
- dotmd release --to planned # override target status (default: lease.oldStatus)
613
- dotmd release --stale # release leases with dead same-host pid or >4h old
614
- dotmd release --all # release every lease (administrative)
615
- dotmd release --json # { released: [...], skipped: [...] }
585
+ dotmd use docs/plans/my-plan.md # mark in-session + print the plan card
586
+ dotmd set in-session docs/plans/my-plan.md # set the status without printing
587
+ dotmd set active docs/plans/my-plan.md # need more work: flip back to active
588
+ dotmd set partial docs/plans/my-plan.md # shipped + tail deferred (reference successors in body)
589
+ dotmd set awaiting docs/plans/my-plan.md # stuck on a human decision
590
+ dotmd archive docs/plans/my-plan.md # fully shipped: archive + move + update refs
591
+ dotmd archive docs/plans/my-plan.md --closeout-template # also inject ## Closeout skeleton
616
592
  ```
617
593
 
618
- `finish` is the same as `release`. `archive` and `rename` auto-release or
619
- migrate the lease, so the common closeout paths are covered without ceremony.
594
+ `in-session` is a status like any other — `dotmd set <status> <file>` writes it
595
+ to the file's frontmatter and does nothing else.
620
596
 
621
- **Session id resolution** (in order, first wins):
622
-
623
- 1. `$CLAUDE_CODE_SESSION_ID` (set by Claude Code in Bash subprocess env)
624
- 2. `$CLAUDE_SESSION_ID` (legacy alias)
625
- 3. `$TERM_SESSION_ID` (macOS Terminal/iTerm — stable per window)
626
- 4. `shell:<user>@<host>` (last-resort coarse fallback)
627
-
628
- The session id survives `/clear` and auto-compaction, so a re-attach after
629
- either is silent.
630
-
631
- **Recommended Claude Code hooks** — add both to `~/.claude/settings.json`
597
+ **Recommended Claude Code hook** — add to `~/.claude/settings.json`
632
598
  (or your project's `.claude/settings.json`):
633
599
 
634
600
  ```json
@@ -640,44 +606,20 @@ either is silent.
640
606
  { "type": "command", "command": "dotmd hud", "timeout": 5 }
641
607
  ]
642
608
  }
643
- ],
644
- "SessionEnd": [
645
- {
646
- "hooks": [
647
- { "type": "command", "command": "dotmd release", "timeout": 10 }
648
- ]
649
- }
650
609
  ]
651
610
  }
652
611
  }
653
612
  ```
654
613
 
655
- - **SessionStart** runs `dotmd hud`, which prints up to three actionable
656
- lines (held leases, pending prompts, stale leases) and stays silent when
657
- nothing is queued. Use this instead of `dotmd briefing` for the hook role
658
- — `briefing` dumps per-plan next_step prose that can run to many kilobytes
659
- on large repos. `hud` is the zero-pollution surface.
660
- - **SessionEnd** runs `dotmd release` (the new name for `dotmd unpickup`;
661
- both still work), which releases every lease owned by the ending session
662
- and flips plans back to their prior status.
614
+ - **SessionStart** runs `dotmd hud`, which prints the command primer and stays
615
+ silent when nothing is queued. Use this instead of `dotmd briefing` for the
616
+ hook role — `briefing` dumps per-plan next_step prose that can run to many
617
+ kilobytes on large repos. `hud` is the zero-pollution surface.
663
618
 
664
619
  > The double-`hooks` nesting is correct: `hooks.<Event>[*].hooks[*]` is the
665
620
  > schema Claude Code requires. `Bash(dotmd:*)` should be in your
666
621
  > `permissions.allow` list as well, otherwise the hooks will be blocked.
667
622
 
668
- `dotmd hud` (and `dotmd briefing` for the verbose case) surface a
669
- `⚠ N stuck leases` line when stale leases exist, with a
670
- `dotmd release --stale` suggestion.
671
-
672
- `dotmd check` also catches the symmetric failure mode: a plan whose
673
- frontmatter claims `status: in-session` but whose lease either doesn't
674
- exist (last session crashed before releasing) or is stale (>4h since
675
- pickup). Each warning names the exact unstuck command
676
- (`dotmd release <plan>` or `dotmd status <plan> active`), so plans
677
- don't sit stuck in-session indefinitely. Always-on — legit concurrent
678
- sessions hold real leases, so the warning only fires on actual
679
- divergence.
680
-
681
623
  ### Touch
682
624
 
683
625
  ```bash
package/bin/dotmd.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync } from 'node:fs';
3
+ import { existsSync, readFileSync, statSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import path from 'node:path';
6
6
  import { resolveConfig } from '../src/config.mjs';
7
- import { die, warn, levenshtein, isArchivedPath } from '../src/util.mjs';
7
+ import { die, warn, levenshtein, isArchivedPath, toRepoPath } from '../src/util.mjs';
8
8
  import { recordCliInvocation, recordGlobalError } from '../src/journal.mjs';
9
9
  import { findRepeatFailureHint } from '../src/hints.mjs';
10
10
 
@@ -37,10 +37,7 @@ const FLAG_SPECS = {
37
37
  hud: { flags: new Set(['--json']), values: new Set() },
38
38
  check: { flags: new Set(['--fix', '--errors-only', '--no-collapse', '--json', '--verbose']), values: new Set() },
39
39
  doctor: { flags: new Set(['--apply', '--yes', '--dry-run', '-n', '--statuses', '--migrate-template', '--migrate-prompts', '--frontmatter-fix', '--project', '--json', '--include-archived']), values: new Set() },
40
- runlist: { flags: new Set(['--json', '--takeover', '--full', '--no-index', '--show-files']), values: new Set(), subcommands: new Set(['next']) },
41
- release: { flags: new Set(['--json', '--all', '--stale', '--to', '--force', '--no-index', '--show-files']), values: new Set(['--to']) },
42
- unpickup: { flags: new Set(['--json', '--all', '--stale', '--to', '--force', '--no-index', '--show-files']), values: new Set(['--to']) },
43
- finish: { flags: new Set(['--json', '--all', '--stale', '--to', '--force', '--no-index', '--show-files']), values: new Set(['--to']) },
40
+ runlist: { flags: new Set(['--json', '--full', '--no-index', '--show-files']), values: new Set(), subcommands: new Set(['next']) },
44
41
  prompts: {
45
42
  flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
46
43
  values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
@@ -60,6 +57,67 @@ function validateKnownFlags(command, argv, config) {
60
57
  }
61
58
  }
62
59
 
60
+ function resolveExistingPath(input, config) {
61
+ if (!input) return null;
62
+ const candidates = [];
63
+ if (path.isAbsolute(input)) {
64
+ candidates.push(input);
65
+ if (!input.endsWith('.md')) candidates.push(`${input}.md`);
66
+ } else {
67
+ candidates.push(path.resolve(config.repoRoot, input));
68
+ if (!input.endsWith('.md')) candidates.push(path.resolve(config.repoRoot, `${input}.md`));
69
+ for (const root of config.docsRoots || [config.docsRoot]) {
70
+ candidates.push(path.resolve(root, input));
71
+ if (!input.endsWith('.md')) candidates.push(path.resolve(root, `${input}.md`));
72
+ }
73
+ }
74
+ return candidates.find(candidate => existsSync(candidate)) ?? null;
75
+ }
76
+
77
+ function applyPathScopeToIndex(index, config, inputs) {
78
+ if (!inputs.length) return;
79
+
80
+ const selected = new Set();
81
+ for (const input of inputs) {
82
+ const resolved = resolveExistingPath(input, config);
83
+ if (!resolved) die(`Could not resolve check path: ${input}`);
84
+
85
+ const stat = statSync(resolved);
86
+ if (stat.isDirectory()) {
87
+ const dir = path.resolve(resolved);
88
+ const before = selected.size;
89
+ for (const doc of index.docs) {
90
+ const abs = path.resolve(config.repoRoot, doc.path);
91
+ if (abs === dir || abs.startsWith(dir + path.sep)) selected.add(doc.path);
92
+ }
93
+ if (selected.size === before) {
94
+ die(`No dotmd documents found under check path: ${toRepoPath(dir, config.repoRoot)}`);
95
+ }
96
+ continue;
97
+ }
98
+
99
+ if (!stat.isFile() || !resolved.endsWith('.md')) die(`Check path is not a markdown file or directory: ${input}`);
100
+ const repoPath = toRepoPath(resolved, config.repoRoot);
101
+ if (!index.docs.some(d => d.path === repoPath)) {
102
+ die(`Check path is outside configured docs roots: ${repoPath}`);
103
+ }
104
+ selected.add(repoPath);
105
+ }
106
+
107
+ index.docs = index.docs.filter(d => selected.has(d.path));
108
+ index.errors = index.errors.filter(e => selected.has(e.path));
109
+ index.warnings = index.warnings.filter(w => selected.has(w.path));
110
+ index.countsByStatus = {};
111
+ index.countsByType = {};
112
+ for (const doc of index.docs) {
113
+ const status = doc.status ?? 'unknown';
114
+ index.countsByStatus[status] = (index.countsByStatus[status] ?? 0) + 1;
115
+ const type = doc.type || 'unknown';
116
+ if (!index.countsByType[type]) index.countsByType[type] = {};
117
+ index.countsByType[type][status] = (index.countsByType[type][status] ?? 0) + 1;
118
+ }
119
+ }
120
+
63
121
  const HELP = {
64
122
  _main: `dotmd v${pkg.version} — frontmatter markdown document manager
65
123
 
@@ -87,7 +145,7 @@ Global flags: --config <path> --root <name> --type <t,…> --dry-run/-n --ve
87
145
  'help:all': `dotmd v${pkg.version} — full command list
88
146
 
89
147
  View & Query:
90
- hud [--json] Two-line actionable triage (held / prompts / stuck) — silent when clean
148
+ hud [--json] Command primer + pending-prompt triage — silent when clean
91
149
  list [--verbose] [--json] List docs grouped by status (default command)
92
150
  briefing [--json] Full briefing with plan status counts + next steps
93
151
  context [--summarize] [--json] Full briefing (LLM-oriented; use --json --compact for bounded JSON)
@@ -121,11 +179,9 @@ Validate & Fix:
121
179
  fix-refs [--dry-run] Auto-fix broken reference paths + body links
122
180
 
123
181
  Lifecycle:
124
- pickup <file> Acquire an in-session lease and start work
125
- release [<file>] Release held in-session work (alias: unpickup, finish)
126
- set <status> [<file>] Unified transition: start work, change status, close out, archive — all via target status
182
+ use <file> Open a plan (mark in-session + print it) or consume a prompt
183
+ set <status> <file> Change a document's status (frontmatter write; archive also moves the file)
127
184
  runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
128
- unpickup [<file>] Release held in-session work
129
185
  status <file> <status> Transition document status (deprecated; prefer \`set\`)
130
186
  archive <file> Archive (status + move + update refs)
131
187
  bulk archive <f1> <f2> ... Archive multiple files at once
@@ -184,11 +240,10 @@ the status taxonomy in a specific project, use \`dotmd statuses list\`.
184
240
  plan statuses (each maps to a distinct unstuck-action)
185
241
 
186
242
  in-session A Claude session is working on it now.
187
- Don't pick up unless you own it (auto-reattaches) or pass
188
- --takeover. Stale lease cleanup: \`dotmd release --stale\`.
243
+ \`dotmd use <file>\` marks it in-session and prints the plan.
189
244
 
190
- active Ready to be picked up.
191
- \`dotmd pickup <file>\` → in-session.
245
+ active Ready to be worked on.
246
+ \`dotmd use <file>\` → in-session.
192
247
 
193
248
  planned Queued for future work, not yet ready to execute.
194
249
  Transition to active when ready to start.
@@ -212,10 +267,10 @@ plan statuses (each maps to a distinct unstuck-action)
212
267
  archived No longer relevant; auto-moved to archive directory.
213
268
 
214
269
  Canonical transitions:
215
- active → in-session \`dotmd pickup <file>\`
216
- in-session → active \`dotmd set active\` (auto-releases lease)
217
- in-session → partial \`dotmd set partial\` (auto-releases lease)
218
- in-session → awaiting \`dotmd set awaiting\` (auto-releases lease)
270
+ active → in-session \`dotmd use <file>\` (or \`dotmd set in-session <file>\`)
271
+ in-session → active \`dotmd set active <file>\`
272
+ in-session → partial \`dotmd set partial <file>\`
273
+ in-session → awaiting \`dotmd set awaiting <file>\`
219
274
  any → archived \`dotmd set archived <file>\` (or \`dotmd archive\`)
220
275
 
221
276
  ────────────────────────────────────────────────────────────────────
@@ -253,7 +308,7 @@ Related commands:
253
308
  dotmd status <f> <new> Transition a document's status
254
309
  dotmd briefing See plans grouped by status
255
310
  dotmd plans --status <s> Filter live plans by status
256
- dotmd hud Two-line actionable triage (held / prompts / stuck)
311
+ dotmd hud Command primer + pending-prompt triage
257
312
 
258
313
  Run \`dotmd statuses list --type plan\` to see the full set (including any
259
314
  project-specific custom statuses) with their flags.`,
@@ -324,76 +379,18 @@ Filters:
324
379
  --summarize-limit <n> Max docs to summarize (default: 5)
325
380
  --model <name> Model for AI summaries`,
326
381
 
327
- pickup: `dotmd pickup <file> — pick up a plan and start working
328
-
329
- Sets the plan to in-session and prints its content (prefixed with a
330
- "[dotmd] holding <path>" line so the fresh session knows what it holds).
331
- Writes a session lease to <repoRoot>/.dotmd/in-session.json so the same
332
- Claude session can re-attach silently after compaction or /clear.
333
-
334
- If a plan is already in-session:
335
- - Same session → silent re-attach (prints body, no error).
336
- - Different session, live pid → refuses with "Held by …" message.
337
- - Different session, dead pid or >24h old → suggests --takeover.
338
-
339
- Options:
340
- --takeover Force-claim a plan held by another session
341
- --no-index Skip index regen (see \`dotmd archive --help\`)
342
- --show-files Append \`files: …\` line to stderr (see \`dotmd archive --help\`)
343
- --json Output as JSON
344
- --dry-run, -n Preview without writing
345
-
346
- If no file is given, prompts with a list of active/planned plans.`,
347
-
348
- unpickup: `dotmd unpickup [<file>] — release a plan from in-session
349
-
350
- With no file: releases every lease owned by the current session.
351
- This is the form intended for a Claude Code SessionEnd hook.
352
-
353
- With <file>: releases that one. Refuses if held by another session
354
- (use --force to override).
355
-
356
- Flips the plan's frontmatter status from in-session back to its
357
- prior status (recorded by pickup), or whatever --to specifies.
358
-
359
- Options:
360
- --to <status> Override target status (default: lease.oldStatus → fallback active)
361
- --all Release every lease in the file (administrative)
362
- --stale Release leases whose pid is dead or age >24h
363
- --force Override "not yours" refusal on a specific file
364
- --no-index Skip index regen (see \`dotmd archive --help\`)
365
- --show-files Append \`files: …\` line to stderr (see \`dotmd archive --help\`)
366
- --json Output as JSON ({ released, skipped })
367
- --dry-run, -n Preview without writing
368
-
369
- Manual-edit fallback: if the plan's status is in-session but no lease
370
- exists, --to <status> flips it anyway with a warning.`,
371
-
372
- release: `dotmd release [<file>] [--to <s>] — alias of dotmd unpickup
373
-
374
- Release the in-session lease(s) and flip frontmatter back to the prior
375
- status. With no file, releases every lease owned by the current session.
376
- Identical behavior to \`dotmd unpickup\`; both names route to the same
377
- implementation. See \`dotmd unpickup --help\` for full option list.`,
378
-
379
- finish: `dotmd finish [<file>] [--to <s>] — alias of dotmd release
380
-
381
- Compatibility alias for docs and agent loops that use "finish" for releasing
382
- in-session work. Same behavior as \`dotmd release\` / \`dotmd unpickup\`.`,
383
-
384
382
  ship: `dotmd ship [patch|minor|major] — regen + commit + bump in one step
385
383
 
386
- Bundles the multi-step release dance into a single command:
384
+ Bundles the release steps into a single command:
387
385
  1. Regenerate \`.claude/commands/*.md\` with the TARGET version stamp
388
386
  (the post-bump version, so the slash-command files match the new
389
387
  release and no dirty tree lingers after).
390
388
  2. Auto-stage every dirty file matching the release allowlist
391
389
  (src/, test/, bin/, docs/, .claude/commands/, package*.json,
392
390
  dotmd.config*.mjs, README.md, CLAUDE.md, .gitignore). Anything
393
- outside the allowlist is left dirty — secrets, sibling-session
394
- WIP, etc. never get bundled in.
395
- 3. Commit with an auto-generated message including the held plan
396
- title (if any).
391
+ outside the allowlist is left dirty — secrets, WIP, etc. never get
392
+ bundled in.
393
+ 3. Commit with an auto-generated \`chore: release <version>\` message.
397
394
  4. Run \`npm version <bump>\` to bump package.json, tag, push, run
398
395
  the publish workflow, and reinstall locally.
399
396
 
@@ -406,19 +403,12 @@ Network failures mid-bump (e.g. \`git push\` fails) leave the local
406
403
  commit + tag intact. Inspect with \`git log -1\` and rerun
407
404
  \`git push origin main --tags\` to recover.`,
408
405
 
409
- set: `dotmd set <status> [<file>] — unified status-transition verb
406
+ set: `dotmd set <status> <file> — change a document's status
410
407
 
411
- Routes to the right plumbing based on the target status:
408
+ Writes the new status into the file's frontmatter. Nothing else — no plan
409
+ checkout, no session locks.
412
410
  - target is an archive status → archive the file (move + ref update)
413
- - source is in-session → also releases the held lease
414
- - everything else → plain frontmatter status bump
415
-
416
- When <file> is omitted, dotmd infers it from the calling session's held
417
- lease. With zero or multiple leases, you must pass <file> explicitly.
418
-
419
- To acquire an in-session lease, use \`dotmd pickup <file>\` instead —
420
- \`dotmd set in-session\` is refused so the asymmetric lease-acquisition
421
- path is never skipped silently.
411
+ - everything else → plain frontmatter status bump
422
412
 
423
413
  Options:
424
414
  --no-index Skip index regen (see \`dotmd archive --help\`).
@@ -426,9 +416,11 @@ Options:
426
416
  --dry-run, -n Preview without writing.
427
417
 
428
418
  Examples:
429
- dotmd set partial # release current lease, mark partial
430
- dotmd set archived docs/plans/x # archive a specific plan
431
- dotmd set active # finish a held in-session plan`,
419
+ dotmd set in-session docs/plans/x # mark a plan in-session
420
+ dotmd set partial docs/plans/x # mark partial
421
+ dotmd set archived docs/plans/x # archive a specific plan
422
+
423
+ To open a plan (mark in-session AND print its body), use \`dotmd use <file>\`.`,
432
424
 
433
425
  status: `dotmd status <file> <new-status> — transition document status
434
426
 
@@ -514,12 +506,10 @@ Options:
514
506
 
515
507
  hud: `dotmd hud — actionable triage for session start
516
508
 
517
- Prints up to three lines, in order:
518
- ▶ You hold N plans: <slugs> (leases owned by current session)
519
- ▶ N pending prompts: <slugs> (saved prompts in docs/prompts/)
520
- ⚠ N stuck leases >24h (suggest \`dotmd release --stale\`)
509
+ Prints the dotmd command primer (the verb cheat-sheet) plus, in --json mode,
510
+ pending prompts and the check-error count for programmatic callers.
521
511
 
522
- Silent when all three are empty — designed for SessionStart hooks where
512
+ Silent when there's nothing actionable — designed for SessionStart hooks where
523
513
  zero noise is the right default. Distinct from \`dotmd briefing\`, which
524
514
  dumps the full plan-status pipeline and per-plan next_step bodies (kilobytes
525
515
  on large repos). Use hud for ergonomic session boot; use briefing for
@@ -1073,14 +1063,13 @@ the order of the children comes from the array.
1073
1063
  Usage:
1074
1064
  dotmd runlist <hub> Show children + their statuses, in order.
1075
1065
  The first non-archived child is marked \`→\`.
1076
- dotmd runlist next <hub> Pick up the first non-archived child.
1077
- Stops if it's not in a pickup-able status
1078
- (active / planned / in-session) so you resolve
1079
- the blocker before continuing the runlist.
1080
-
1081
- Flags (only meaningful with \`next\`, forwarded to pickup):
1082
- --takeover Override a held lease.
1083
- --full Print full plan body instead of the pickup card.
1066
+ dotmd runlist next <hub> Open the first non-archived child (marks it
1067
+ in-session + prints it). Stops if it's not in a
1068
+ workable status (active / planned / in-session)
1069
+ so you resolve the blocker first.
1070
+
1071
+ Flags (only meaningful with \`next\`):
1072
+ --full Print full plan body instead of the card.
1084
1073
  --no-index Skip index regeneration.
1085
1074
  --show-files Emit \`files: …\` footer.
1086
1075
 
@@ -1226,8 +1215,6 @@ async function main() {
1226
1215
  if (config.presets[command]) {
1227
1216
  const { buildIndex } = await import('../src/index.mjs');
1228
1217
  const { runQuery } = await import('../src/query.mjs');
1229
- const { scrubStaleSilently } = await import('../src/lease-scrub.mjs');
1230
- scrubStaleSilently(config);
1231
1218
  const index = buildIndex(config);
1232
1219
  runQuery(index, [...config.presets[command], ...restArgs], config, { preset: command });
1233
1220
  return;
@@ -1240,8 +1227,6 @@ async function main() {
1240
1227
  if (command === 'plans') {
1241
1228
  const { buildIndex } = await import('../src/index.mjs');
1242
1229
  const { runQuery } = await import('../src/query.mjs');
1243
- const { scrubStaleSilently } = await import('../src/lease-scrub.mjs');
1244
- scrubStaleSilently(config);
1245
1230
  const index = buildIndex(config);
1246
1231
  const sub = restArgs[0];
1247
1232
  let defaults;
@@ -1262,7 +1247,7 @@ async function main() {
1262
1247
  }
1263
1248
  // Top-level `dotmd use [file]` — the single "start engaging with this doc"
1264
1249
  // verb. Dispatches by the target doc's type: prompt → consume + archive,
1265
- // plan → acquire lease + print, doc → print. With no file: consume oldest
1250
+ // plan → mark in-session + print, doc → print. With no file: consume oldest
1266
1251
  // pending prompt. See src/use.mjs for the dispatch table.
1267
1252
  if (command === 'use') {
1268
1253
  const { runUse } = await import('../src/use.mjs');
@@ -1292,8 +1277,9 @@ async function main() {
1292
1277
  // Lifecycle commands
1293
1278
  if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
1294
1279
  if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
1295
- if (command === 'pickup') { const { runPickup } = await import('../src/lifecycle.mjs'); await runPickup(restArgs, config, { dryRun }); return; }
1296
- if (command === 'unpickup' || command === 'release' || command === 'finish') { const { runUnpickup } = await import('../src/lifecycle.mjs'); await runUnpickup(restArgs, config, { dryRun }); return; }
1280
+ if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
1281
+ die(`\`dotmd ${command}\` was removed — dotmd no longer checks plans in/out. Status is just frontmatter:\n dotmd use <file> # mark in-session + print the plan\n dotmd set <status> <file> # change status\n dotmd archive <file> # close out`);
1282
+ }
1297
1283
  if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
1298
1284
  if (command === 'handoff') { die('`dotmd handoff` was removed in 0.31.0. Use `dotmd prompts new <name>` to create a saved prompt instead. The .dotmd/handoffs/ sidecar mechanism no longer exists; see CHANGELOG.'); }
1299
1285
  if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
@@ -1336,21 +1322,14 @@ async function main() {
1336
1322
  const { buildIndex } = await import('../src/index.mjs');
1337
1323
  const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } = await import('../src/render.mjs');
1338
1324
  const { runFocus, runQuery } = await import('../src/query.mjs');
1339
- // Opportunistic stale-lease scrub for user-facing "what's actionable now"
1340
- // views. Diagnostic commands (`check`, `coverage`, `stats`, `index`) are
1341
- // intentionally excluded — they should surface drift, not silently fix it.
1342
- const SCRUB_READ_COMMANDS = new Set(['list', 'briefing', 'context', 'agent-context', 'focus', 'query', 'modules', 'module', 'surfaces']);
1343
- if (SCRUB_READ_COMMANDS.has(command)) {
1344
- const { scrubStaleSilently } = await import('../src/lease-scrub.mjs');
1345
- scrubStaleSilently(config);
1346
- }
1347
1325
  // `dotmd check` is the one shared-buildIndex command that should auto-heal a
1348
1326
  // drifted index block (frontmatter edits by direct Edit/Write, `lint --fix`,
1349
1327
  // etc. leave the README out of sync; demanding the user run `dotmd index`
1350
1328
  // each time was pure noise). Print/dry-run/read-only callers (`json`, `list`,
1351
1329
  // `query`, `index --print`, ...) stay opt-out so they never mutate disk.
1330
+ const checkHasPathScope = command === 'check' && restArgs.some(arg => !arg.startsWith('-'));
1352
1331
  const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
1353
- const index = buildIndex(config, { autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) });
1332
+ const index = buildIndex(config, { autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) && !checkHasPathScope });
1354
1333
 
1355
1334
  // Apply --root and --type filters
1356
1335
  const rootFilter = rootArg;
@@ -1405,6 +1384,11 @@ async function main() {
1405
1384
  const errorsOnly = args.includes('--errors-only');
1406
1385
  const noCollapse = args.includes('--no-collapse');
1407
1386
  const verbose = args.includes('--verbose');
1387
+ const checkTargets = restArgs.filter(arg => !arg.startsWith('-'));
1388
+
1389
+ if (fix && checkTargets.length > 0) {
1390
+ die('`dotmd check --fix` does not support path-scoped checks yet. Run `dotmd check <path>` to validate a subset, or `dotmd check --fix` to fix the whole docs tree.');
1391
+ }
1408
1392
 
1409
1393
  if (fix) {
1410
1394
  // Auto-fix: broken refs, then lint, then rebuild index
@@ -1425,6 +1409,7 @@ async function main() {
1425
1409
  // Show remaining issues
1426
1410
  const freshIndex = buildIndex(config);
1427
1411
  applyIndexFilters(freshIndex);
1412
+ applyPathScopeToIndex(freshIndex, config, checkTargets);
1428
1413
  if (args.includes('--json')) {
1429
1414
  process.stdout.write(JSON.stringify({
1430
1415
  docsScanned: freshIndex.docs.length,
@@ -1441,6 +1426,8 @@ async function main() {
1441
1426
  return;
1442
1427
  }
1443
1428
 
1429
+ applyPathScopeToIndex(index, config, checkTargets);
1430
+
1444
1431
  if (args.includes('--json')) {
1445
1432
  process.stdout.write(JSON.stringify({
1446
1433
  docsScanned: index.docs.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.50.1",
3
+ "version": "0.51.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",
@@ -41,7 +41,7 @@
41
41
  "test": "node --test test/*.test.mjs",
42
42
  "preversion": "npm test",
43
43
  "version": "node bin/dotmd.mjs hud >/dev/null 2>&1; git add .claude/commands docs/docs.md 2>/dev/null; true",
44
- "postversion": "git push origin main --tags && gh release create v$npm_package_version --generate-notes --title v$npm_package_version && sleep 5 && gh run watch $(gh run list --workflow=publish.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status && sleep 10 && npm cache clean --force && npm install -g dotmd-cli@$npm_package_version"
44
+ "postversion": "bash scripts/postversion.sh"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=20"
@@ -21,7 +21,6 @@ function markerFor(version) { return `<!-- dotmd-generated: ${version} -->`; }
21
21
  const SLASH_DESCRIPTIONS = {
22
22
  plans: "dotmd-managed plan briefing for this repo. Use when the user asks what's on the plate, references a plan slug, queues work, or wants to start / close / archive a plan.",
23
23
  docs: "dotmd-managed docs briefing for this repo. Use when the user asks to list, scaffold, query, validate, archive, or rename non-plan docs (reference docs, ADRs, RFCs, design notes), or asks how the dotmd doc lifecycle works here.",
24
- baton: "Save a resume prompt for the active plan and close it out — the minimum handoff. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
25
24
  };
26
25
 
27
26
  const VOCAB_TRUNCATE_AT = 12;
@@ -63,21 +62,20 @@ function generatePlansCommand(config, version) {
63
62
  lines.push('');
64
63
  lines.push('Plan-specific commands:');
65
64
  lines.push('- `dotmd context` — briefing with active/paused/ready plans, age tags, next steps');
66
- lines.push('- `dotmd set <status> [<file>]` — single status verb. Use this to start, transition, or close any plan:');
67
- lines.push(' - `dotmd set in-session <file>` — start work on a plan (marks in-session + prints body)');
68
- lines.push(' - `dotmd set <status> [<file>]` — transition to any other status; closes out the in-session marker automatically');
65
+ lines.push('- `dotmd set <status> <file>` — single status verb. Writes the new status to the plan\'s frontmatter. Use it to transition or close any plan:');
66
+ lines.push(' - `dotmd set in-session <file>` — mark a plan in-session (just a frontmatter status; use `dotmd use <file>` to also print the body)');
69
67
  lines.push(' - `dotmd set archived <file>` — close out (same as `dotmd archive`)');
70
68
  lines.push('- `dotmd archive <file>` — explicit archive with ref-fixing (equivalent to `set archived`)');
71
69
  lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
72
70
  lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
73
71
  lines.push('- `dotmd new prompt <name>` — save a resume-prompt to docs/prompts/ (pipe stdin or @path for body)');
74
72
  lines.push('- `dotmd use` — consume oldest pending prompt (prints body, auto-archives)');
75
- lines.push('- `dotmd use <file>` — open any doc by type: prompt → consume, plan → start work, doc → read');
73
+ lines.push('- `dotmd use <file>` — open any doc by type: prompt → consume, plan → mark in-session + print card, doc → read');
76
74
  lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
77
75
  lines.push('- `dotmd actionable` — ready plans with next steps (what to promote)');
78
76
  lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
79
- lines.push('- `dotmd runlist <hub>` — show ordered children of a runlist hub (→ marks next pickup)');
80
- lines.push('- `dotmd runlist next <hub>` — pick up the next non-archived child of a runlist hub');
77
+ lines.push('- `dotmd runlist <hub>` — show ordered children of a runlist hub (→ marks next)');
78
+ lines.push('- `dotmd runlist next <hub>` — open the next non-archived child of a runlist hub');
81
79
 
82
80
  if (config.raw?.glossary) {
83
81
  lines.push('- `dotmd glossary <term>` — domain term lookup with related plans');
@@ -96,26 +94,6 @@ function generatePlansCommand(config, version) {
96
94
  return lines.join('\n');
97
95
  }
98
96
 
99
- function generateBatonCommand(config, version) {
100
- const lines = [...frontmatterFor('baton', config), markerFor(version), ''];
101
- lines.push('Wrap this session. Two commands:');
102
- lines.push('');
103
- lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` — pipe stdin or pass `@path`. 10-20 line body: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
104
- lines.push('');
105
- lines.push('2. **Close out via `dotmd set <status>`.** Pick the status that matches reality:');
106
- lines.push(' - `dotmd set active <file>` — work continues, return the plan to the active queue');
107
- lines.push(' - `dotmd set archived <file>` — fully shipped (also: `dotmd archive <file>`)');
108
- lines.push(' - `dotmd set paused <file>` / `awaiting <file>` / `partial <file>` / `blocked <file>` — when the status really changed');
109
- lines.push(' `set` clears the in-session marker automatically when transitioning to any other status.');
110
- lines.push('');
111
- lines.push('If you don\'t already know which plan you have in-session: `dotmd hud --json` and read `.owned`. Do NOT use `dotmd plans --status in-session` — that lists every session\'s in-session plans, not just yours.');
112
- lines.push('');
113
- lines.push('The next session\'s `dotmd hud` (SessionStart hook) surfaces the pending prompt automatically.');
114
- lines.push('');
115
-
116
- return lines.join('\n');
117
- }
118
-
119
97
  function generateDocsCommand(config, version) {
120
98
  const roots = Array.isArray(config.raw?.root) ? config.raw.root : [config.raw?.root ?? 'docs'];
121
99
  const rootCount = roots.length;
@@ -188,7 +166,6 @@ export function scaffoldClaudeCommands(cwd, config, opts = {}) {
188
166
  const files = [
189
167
  { name: 'plans.md', generate: () => generatePlansCommand(config, version) },
190
168
  { name: 'docs.md', generate: () => generateDocsCommand(config, version) },
191
- { name: 'baton.md', generate: () => generateBatonCommand(config, version) },
192
169
  ];
193
170
 
194
171
  for (const { name, generate } of files) {