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 +22 -80
- package/bin/dotmd.mjs +112 -125
- package/package.json +2 -2
- package/src/claude-commands.mjs +5 -28
- package/src/commands.mjs +1 -1
- package/src/completions.mjs +1 -5
- package/src/doctor.mjs +1 -1
- package/src/hints.mjs +1 -6
- package/src/hud.mjs +2 -25
- package/src/journal.mjs +1 -1
- package/src/lifecycle.mjs +18 -275
- package/src/rename.mjs +0 -3
- package/src/render.mjs +0 -8
- package/src/runlist.mjs +12 -13
- package/src/ship.mjs +1 -13
- package/src/use.mjs +4 -4
- package/src/util.mjs +10 -0
- package/src/validate.mjs +0 -26
- package/src/lease-scrub.mjs +0 -49
- package/src/lease.mjs +0 -232
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
|
|
191
|
-
dotmd
|
|
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
|
|
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
|
-
###
|
|
578
|
+
### Open & Closeout
|
|
579
579
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
611
|
-
dotmd
|
|
612
|
-
dotmd
|
|
613
|
-
dotmd
|
|
614
|
-
dotmd
|
|
615
|
-
dotmd
|
|
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
|
-
`
|
|
619
|
-
|
|
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
|
-
**
|
|
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
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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', '--
|
|
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]
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
191
|
-
\`dotmd
|
|
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
|
|
216
|
-
in-session → active \`dotmd set active
|
|
217
|
-
in-session → partial \`dotmd set partial
|
|
218
|
-
in-session → awaiting \`dotmd set awaiting
|
|
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
|
|
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
|
|
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,
|
|
394
|
-
|
|
395
|
-
3. Commit with an auto-generated
|
|
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>
|
|
406
|
+
set: `dotmd set <status> <file> — change a document's status
|
|
410
407
|
|
|
411
|
-
|
|
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
|
-
-
|
|
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
|
|
430
|
-
dotmd set
|
|
431
|
-
dotmd set
|
|
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
|
|
518
|
-
|
|
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
|
|
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>
|
|
1077
|
-
Stops if it's not in a
|
|
1078
|
-
(active / planned / in-session)
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
Flags (only meaningful with \`next
|
|
1082
|
-
--
|
|
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 →
|
|
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'
|
|
1296
|
-
|
|
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.
|
|
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": "
|
|
44
|
+
"postversion": "bash scripts/postversion.sh"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=20"
|
package/src/claude-commands.mjs
CHANGED
|
@@ -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>
|
|
67
|
-
lines.push(' - `dotmd set in-session <file>` —
|
|
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 →
|
|
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
|
|
80
|
-
lines.push('- `dotmd runlist next <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) {
|