dotmd-cli 0.50.2 → 0.52.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
 
@@ -34,13 +34,12 @@ const FLAG_SPECS = {
34
34
  briefing: { flags: new Set(['--json']), values: new Set() },
35
35
  context: { flags: new Set(['--json', '--compact', '--summarize', '--model']), values: new Set(['--model']) },
36
36
  'agent-context': { flags: new Set(['--json']), values: new Set() },
37
- hud: { flags: new Set(['--json']), values: new Set() },
37
+ hud: { flags: new Set(['--json', '--subagent']), values: new Set() },
38
+ guard: { flags: new Set(), values: new Set() },
39
+ misuse: { flags: new Set(['--json', '--tail', '--by-rule', '--repo']), values: new Set(['--tail', '--repo']) },
38
40
  check: { flags: new Set(['--fix', '--errors-only', '--no-collapse', '--json', '--verbose']), values: new Set() },
39
41
  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']) },
42
+ runlist: { flags: new Set(['--json', '--full', '--no-index', '--show-files']), values: new Set(), subcommands: new Set(['next']) },
44
43
  prompts: {
45
44
  flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
46
45
  values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
@@ -60,6 +59,67 @@ function validateKnownFlags(command, argv, config) {
60
59
  }
61
60
  }
62
61
 
62
+ function resolveExistingPath(input, config) {
63
+ if (!input) return null;
64
+ const candidates = [];
65
+ if (path.isAbsolute(input)) {
66
+ candidates.push(input);
67
+ if (!input.endsWith('.md')) candidates.push(`${input}.md`);
68
+ } else {
69
+ candidates.push(path.resolve(config.repoRoot, input));
70
+ if (!input.endsWith('.md')) candidates.push(path.resolve(config.repoRoot, `${input}.md`));
71
+ for (const root of config.docsRoots || [config.docsRoot]) {
72
+ candidates.push(path.resolve(root, input));
73
+ if (!input.endsWith('.md')) candidates.push(path.resolve(root, `${input}.md`));
74
+ }
75
+ }
76
+ return candidates.find(candidate => existsSync(candidate)) ?? null;
77
+ }
78
+
79
+ function applyPathScopeToIndex(index, config, inputs) {
80
+ if (!inputs.length) return;
81
+
82
+ const selected = new Set();
83
+ for (const input of inputs) {
84
+ const resolved = resolveExistingPath(input, config);
85
+ if (!resolved) die(`Could not resolve check path: ${input}`);
86
+
87
+ const stat = statSync(resolved);
88
+ if (stat.isDirectory()) {
89
+ const dir = path.resolve(resolved);
90
+ const before = selected.size;
91
+ for (const doc of index.docs) {
92
+ const abs = path.resolve(config.repoRoot, doc.path);
93
+ if (abs === dir || abs.startsWith(dir + path.sep)) selected.add(doc.path);
94
+ }
95
+ if (selected.size === before) {
96
+ die(`No dotmd documents found under check path: ${toRepoPath(dir, config.repoRoot)}`);
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (!stat.isFile() || !resolved.endsWith('.md')) die(`Check path is not a markdown file or directory: ${input}`);
102
+ const repoPath = toRepoPath(resolved, config.repoRoot);
103
+ if (!index.docs.some(d => d.path === repoPath)) {
104
+ die(`Check path is outside configured docs roots: ${repoPath}`);
105
+ }
106
+ selected.add(repoPath);
107
+ }
108
+
109
+ index.docs = index.docs.filter(d => selected.has(d.path));
110
+ index.errors = index.errors.filter(e => selected.has(e.path));
111
+ index.warnings = index.warnings.filter(w => selected.has(w.path));
112
+ index.countsByStatus = {};
113
+ index.countsByType = {};
114
+ for (const doc of index.docs) {
115
+ const status = doc.status ?? 'unknown';
116
+ index.countsByStatus[status] = (index.countsByStatus[status] ?? 0) + 1;
117
+ const type = doc.type || 'unknown';
118
+ if (!index.countsByType[type]) index.countsByType[type] = {};
119
+ index.countsByType[type][status] = (index.countsByType[type][status] ?? 0) + 1;
120
+ }
121
+ }
122
+
63
123
  const HELP = {
64
124
  _main: `dotmd v${pkg.version} — frontmatter markdown document manager
65
125
 
@@ -81,13 +141,39 @@ More help:
81
141
 
82
142
  Global flags: --config <path> --root <name> --type <t,…> --dry-run/-n --verbose --version`,
83
143
 
144
+ guard: `dotmd guard — PreToolUse hook handler (reads the tool-call JSON on stdin)
145
+
146
+ Wire it into Claude Code as a PreToolUse hook to intercept the wrong-moves
147
+ sessions keep making, and to log every one for audit:
148
+
149
+ {"matcher":"Bash|Read|Edit|Write","hooks":[{"type":"command","command":"dotmd guard"}]}
150
+
151
+ Rules:
152
+ commit-prompt deny git add/commit of a (often gitignored) saved prompt
153
+ cat-prompt warn cat/less/head of a docs/prompts/*.md (use \`dotmd use\`)
154
+ read-prompt warn Read tool on a saved prompt (use \`dotmd use\`)
155
+ edit-status warn hand-edit of a \`status:\` field (use \`dotmd set\`)
156
+
157
+ Every catch is appended to the cross-repo misuse log. Disable with DOTMD_GUARD=0.
158
+ Read the log with \`dotmd misuse\`.`,
159
+
160
+ misuse: `dotmd misuse — read the cross-repo guard log (~/.claude/logs/dotmd-misuse.log)
161
+
162
+ dotmd misuse last 20 intercepted wrong-moves
163
+ dotmd misuse --tail 50 last N
164
+ dotmd misuse --by-rule counts per rule (deny/warn split)
165
+ dotmd misuse --repo <name> filter by repo
166
+ dotmd misuse --json machine-readable
167
+
168
+ Populated by the \`dotmd guard\` PreToolUse hook — see \`dotmd help guard\`.`,
169
+
84
170
  // Full command list — opt-in via \`dotmd help all\`. Kept exhaustive so the
85
171
  // top-level \`--help\` can stay terse without losing discoverability. When you
86
172
  // add a new command, add it here too.
87
173
  'help:all': `dotmd v${pkg.version} — full command list
88
174
 
89
175
  View & Query:
90
- hud [--json] Two-line actionable triage (held / prompts / stuck) — silent when clean
176
+ hud [--json] Command primer + pending-prompt triage — silent when clean
91
177
  list [--verbose] [--json] List docs grouped by status (default command)
92
178
  briefing [--json] Full briefing with plan status counts + next steps
93
179
  context [--summarize] [--json] Full briefing (LLM-oriented; use --json --compact for bounded JSON)
@@ -121,11 +207,9 @@ Validate & Fix:
121
207
  fix-refs [--dry-run] Auto-fix broken reference paths + body links
122
208
 
123
209
  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
210
+ use <file> Open a plan (mark in-session + print it) or consume a prompt
211
+ set <status> <file> Change a document's status (frontmatter write; archive also moves the file)
127
212
  runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
128
- unpickup [<file>] Release held in-session work
129
213
  status <file> <status> Transition document status (deprecated; prefer \`set\`)
130
214
  archive <file> Archive (status + move + update refs)
131
215
  bulk archive <f1> <f2> ... Archive multiple files at once
@@ -184,11 +268,10 @@ the status taxonomy in a specific project, use \`dotmd statuses list\`.
184
268
  plan statuses (each maps to a distinct unstuck-action)
185
269
 
186
270
  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\`.
271
+ \`dotmd use <file>\` marks it in-session and prints the plan.
189
272
 
190
- active Ready to be picked up.
191
- \`dotmd pickup <file>\` → in-session.
273
+ active Ready to be worked on.
274
+ \`dotmd use <file>\` → in-session.
192
275
 
193
276
  planned Queued for future work, not yet ready to execute.
194
277
  Transition to active when ready to start.
@@ -212,10 +295,10 @@ plan statuses (each maps to a distinct unstuck-action)
212
295
  archived No longer relevant; auto-moved to archive directory.
213
296
 
214
297
  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)
298
+ active → in-session \`dotmd use <file>\` (or \`dotmd set in-session <file>\`)
299
+ in-session → active \`dotmd set active <file>\`
300
+ in-session → partial \`dotmd set partial <file>\`
301
+ in-session → awaiting \`dotmd set awaiting <file>\`
219
302
  any → archived \`dotmd set archived <file>\` (or \`dotmd archive\`)
220
303
 
221
304
  ────────────────────────────────────────────────────────────────────
@@ -253,7 +336,7 @@ Related commands:
253
336
  dotmd status <f> <new> Transition a document's status
254
337
  dotmd briefing See plans grouped by status
255
338
  dotmd plans --status <s> Filter live plans by status
256
- dotmd hud Two-line actionable triage (held / prompts / stuck)
339
+ dotmd hud Command primer + pending-prompt triage
257
340
 
258
341
  Run \`dotmd statuses list --type plan\` to see the full set (including any
259
342
  project-specific custom statuses) with their flags.`,
@@ -324,76 +407,18 @@ Filters:
324
407
  --summarize-limit <n> Max docs to summarize (default: 5)
325
408
  --model <name> Model for AI summaries`,
326
409
 
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
410
  ship: `dotmd ship [patch|minor|major] — regen + commit + bump in one step
385
411
 
386
- Bundles the multi-step release dance into a single command:
412
+ Bundles the release steps into a single command:
387
413
  1. Regenerate \`.claude/commands/*.md\` with the TARGET version stamp
388
414
  (the post-bump version, so the slash-command files match the new
389
415
  release and no dirty tree lingers after).
390
416
  2. Auto-stage every dirty file matching the release allowlist
391
417
  (src/, test/, bin/, docs/, .claude/commands/, package*.json,
392
418
  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).
419
+ outside the allowlist is left dirty — secrets, WIP, etc. never get
420
+ bundled in.
421
+ 3. Commit with an auto-generated \`chore: release <version>\` message.
397
422
  4. Run \`npm version <bump>\` to bump package.json, tag, push, run
398
423
  the publish workflow, and reinstall locally.
399
424
 
@@ -406,19 +431,12 @@ Network failures mid-bump (e.g. \`git push\` fails) leave the local
406
431
  commit + tag intact. Inspect with \`git log -1\` and rerun
407
432
  \`git push origin main --tags\` to recover.`,
408
433
 
409
- set: `dotmd set <status> [<file>] — unified status-transition verb
434
+ set: `dotmd set <status> <file> — change a document's status
410
435
 
411
- Routes to the right plumbing based on the target status:
436
+ Writes the new status into the file's frontmatter. Nothing else — no plan
437
+ checkout, no session locks.
412
438
  - 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.
439
+ - everything else → plain frontmatter status bump
422
440
 
423
441
  Options:
424
442
  --no-index Skip index regen (see \`dotmd archive --help\`).
@@ -426,9 +444,11 @@ Options:
426
444
  --dry-run, -n Preview without writing.
427
445
 
428
446
  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`,
447
+ dotmd set in-session docs/plans/x # mark a plan in-session
448
+ dotmd set partial docs/plans/x # mark partial
449
+ dotmd set archived docs/plans/x # archive a specific plan
450
+
451
+ To open a plan (mark in-session AND print its body), use \`dotmd use <file>\`.`,
432
452
 
433
453
  status: `dotmd status <file> <new-status> — transition document status
434
454
 
@@ -514,12 +534,10 @@ Options:
514
534
 
515
535
  hud: `dotmd hud — actionable triage for session start
516
536
 
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\`)
537
+ Prints the dotmd command primer (the verb cheat-sheet) plus, in --json mode,
538
+ pending prompts and the check-error count for programmatic callers.
521
539
 
522
- Silent when all three are empty — designed for SessionStart hooks where
540
+ Silent when there's nothing actionable — designed for SessionStart hooks where
523
541
  zero noise is the right default. Distinct from \`dotmd briefing\`, which
524
542
  dumps the full plan-status pipeline and per-plan next_step bodies (kilobytes
525
543
  on large repos). Use hud for ergonomic session boot; use briefing for
@@ -1073,14 +1091,13 @@ the order of the children comes from the array.
1073
1091
  Usage:
1074
1092
  dotmd runlist <hub> Show children + their statuses, in order.
1075
1093
  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.
1094
+ dotmd runlist next <hub> Open the first non-archived child (marks it
1095
+ in-session + prints it). Stops if it's not in a
1096
+ workable status (active / planned / in-session)
1097
+ so you resolve the blocker first.
1098
+
1099
+ Flags (only meaningful with \`next\`):
1100
+ --full Print full plan body instead of the card.
1084
1101
  --no-index Skip index regeneration.
1085
1102
  --show-files Emit \`files: …\` footer.
1086
1103
 
@@ -1226,8 +1243,6 @@ async function main() {
1226
1243
  if (config.presets[command]) {
1227
1244
  const { buildIndex } = await import('../src/index.mjs');
1228
1245
  const { runQuery } = await import('../src/query.mjs');
1229
- const { scrubStaleSilently } = await import('../src/lease-scrub.mjs');
1230
- scrubStaleSilently(config);
1231
1246
  const index = buildIndex(config);
1232
1247
  runQuery(index, [...config.presets[command], ...restArgs], config, { preset: command });
1233
1248
  return;
@@ -1240,8 +1255,6 @@ async function main() {
1240
1255
  if (command === 'plans') {
1241
1256
  const { buildIndex } = await import('../src/index.mjs');
1242
1257
  const { runQuery } = await import('../src/query.mjs');
1243
- const { scrubStaleSilently } = await import('../src/lease-scrub.mjs');
1244
- scrubStaleSilently(config);
1245
1258
  const index = buildIndex(config);
1246
1259
  const sub = restArgs[0];
1247
1260
  let defaults;
@@ -1262,7 +1275,7 @@ async function main() {
1262
1275
  }
1263
1276
  // Top-level `dotmd use [file]` — the single "start engaging with this doc"
1264
1277
  // verb. Dispatches by the target doc's type: prompt → consume + archive,
1265
- // plan → acquire lease + print, doc → print. With no file: consume oldest
1278
+ // plan → mark in-session + print, doc → print. With no file: consume oldest
1266
1279
  // pending prompt. See src/use.mjs for the dispatch table.
1267
1280
  if (command === 'use') {
1268
1281
  const { runUse } = await import('../src/use.mjs');
@@ -1291,9 +1304,12 @@ async function main() {
1291
1304
 
1292
1305
  // Lifecycle commands
1293
1306
  if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
1307
+ if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config); return; }
1308
+ if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
1294
1309
  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; }
1310
+ if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
1311
+ 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`);
1312
+ }
1297
1313
  if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
1298
1314
  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
1315
  if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
@@ -1336,21 +1352,14 @@ async function main() {
1336
1352
  const { buildIndex } = await import('../src/index.mjs');
1337
1353
  const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } = await import('../src/render.mjs');
1338
1354
  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
1355
  // `dotmd check` is the one shared-buildIndex command that should auto-heal a
1348
1356
  // drifted index block (frontmatter edits by direct Edit/Write, `lint --fix`,
1349
1357
  // etc. leave the README out of sync; demanding the user run `dotmd index`
1350
1358
  // each time was pure noise). Print/dry-run/read-only callers (`json`, `list`,
1351
1359
  // `query`, `index --print`, ...) stay opt-out so they never mutate disk.
1360
+ const checkHasPathScope = command === 'check' && restArgs.some(arg => !arg.startsWith('-'));
1352
1361
  const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
1353
- const index = buildIndex(config, { autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) });
1362
+ const index = buildIndex(config, { autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) && !checkHasPathScope });
1354
1363
 
1355
1364
  // Apply --root and --type filters
1356
1365
  const rootFilter = rootArg;
@@ -1405,6 +1414,11 @@ async function main() {
1405
1414
  const errorsOnly = args.includes('--errors-only');
1406
1415
  const noCollapse = args.includes('--no-collapse');
1407
1416
  const verbose = args.includes('--verbose');
1417
+ const checkTargets = restArgs.filter(arg => !arg.startsWith('-'));
1418
+
1419
+ if (fix && checkTargets.length > 0) {
1420
+ 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.');
1421
+ }
1408
1422
 
1409
1423
  if (fix) {
1410
1424
  // Auto-fix: broken refs, then lint, then rebuild index
@@ -1425,6 +1439,7 @@ async function main() {
1425
1439
  // Show remaining issues
1426
1440
  const freshIndex = buildIndex(config);
1427
1441
  applyIndexFilters(freshIndex);
1442
+ applyPathScopeToIndex(freshIndex, config, checkTargets);
1428
1443
  if (args.includes('--json')) {
1429
1444
  process.stdout.write(JSON.stringify({
1430
1445
  docsScanned: freshIndex.docs.length,
@@ -1441,6 +1456,8 @@ async function main() {
1441
1456
  return;
1442
1457
  }
1443
1458
 
1459
+ applyPathScopeToIndex(index, config, checkTargets);
1460
+
1444
1461
  if (args.includes('--json')) {
1445
1462
  process.stdout.write(JSON.stringify({
1446
1463
  docsScanned: index.docs.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.50.2",
3
+ "version": "0.52.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",