dotmd-cli 0.44.0 → 0.45.1

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/bin/dotmd.mjs CHANGED
@@ -19,8 +19,9 @@ Common commands:
19
19
  briefing Full briefing with plan counts + next steps
20
20
  set <status> [file] Transition status (start work, finish, archive — all via target status)
21
21
  new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
22
+ use [<file>] Open a doc by type: prompt → consume, plan → start, doc → read
23
+ (no file: consume oldest pending prompt)
22
24
  archive <file> Close out a plan (status → archived, move, update refs)
23
- prompts [next|use|new] Manage saved prompts
24
25
 
25
26
  More help:
26
27
  dotmd help all Full command list
@@ -42,8 +43,8 @@ View & Query:
42
43
  focus [status] [--json] Detailed view for one status group
43
44
  query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
44
45
  plans Live plans (excludes archived; --include-archived for all)
45
- prompts [list|next|use|archive|new]
46
- Manage saved prompts (default: list pending)
46
+ use [<file>] Open a doc by type: prompt → consume, plan → start, doc → read
47
+ prompts [list|archive|new|shelve] Prompt admin (list / archive / save / shelve). Use \`dotmd use\` to consume.
47
48
  stale Stale docs (preset)
48
49
  actionable Docs with next steps (preset)
49
50
 
@@ -67,7 +68,7 @@ Validate & Fix:
67
68
  fix-refs [--dry-run] Auto-fix broken reference paths + body links
68
69
 
69
70
  Lifecycle:
70
- set <status> [<file>] Unified transition: archive/release/start/transition in one verb
71
+ set <status> [<file>] Unified transition: start work, change status, close out, archive — all via target status
71
72
  runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
72
73
  status <file> <status> Transition document status (deprecated; prefer \`set\`)
73
74
  archive <file> Archive (status + move + update refs)
@@ -1179,6 +1180,24 @@ async function main() {
1179
1180
  await runPrompts(restArgs, config, { dryRun, verbose });
1180
1181
  return;
1181
1182
  }
1183
+ // Top-level `dotmd use [file]` — the single "start engaging with this doc"
1184
+ // verb. Dispatches by the target doc's type: prompt → consume + archive,
1185
+ // plan → acquire lease + print, doc → print. With no file: consume oldest
1186
+ // pending prompt. See src/use.mjs for the dispatch table.
1187
+ if (command === 'use') {
1188
+ const { runUse } = await import('../src/use.mjs');
1189
+ await runUse(restArgs, config, { dryRun });
1190
+ return;
1191
+ }
1192
+ // `dotmd next` is a top-level alias for `dotmd use` with no arg — consume
1193
+ // the oldest pending prompt. Wired separately so agents who reach for the
1194
+ // literal verb "next" don't bounce off an Unknown-command. Any positional
1195
+ // arg is ignored (a named file goes through `use`).
1196
+ if (command === 'next') {
1197
+ const { runUse } = await import('../src/use.mjs');
1198
+ await runUse(restArgs.filter(a => a.startsWith('-')), config, { dryRun });
1199
+ return;
1200
+ }
1182
1201
 
1183
1202
  // Commands that handle their own index building
1184
1203
  if (command === 'diff') { const { runDiff } = await import('../src/diff.mjs'); runDiff(restArgs, config); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
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",
@@ -19,9 +19,9 @@ function markerFor(version) { return `<!-- dotmd-generated: ${version} -->`; }
19
19
  // gets a per-type status vocab appended at generation time so agents arrive
20
20
  // with the valid `dotmd status` / `dotmd archive` values already in context.
21
21
  const SLASH_DESCRIPTIONS = {
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 pick up / release / archive a plan.",
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 held plan and release the lease — the minimum handoff. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
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
25
  };
26
26
 
27
27
  const VOCAB_TRUNCATE_AT = 12;
@@ -64,15 +64,15 @@ function generatePlansCommand(config, version) {
64
64
  lines.push('Plan-specific commands:');
65
65
  lines.push('- `dotmd context` — briefing with active/paused/ready plans, age tags, next steps');
66
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 (acquires the lease + prints body)');
68
- lines.push(' - `dotmd set <status> [<file>]` — transition to any other status; if you held the lease it releases automatically');
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');
69
69
  lines.push(' - `dotmd set archived <file>` — close out (same as `dotmd archive`)');
70
70
  lines.push('- `dotmd archive <file>` — explicit archive with ref-fixing (equivalent to `set archived`)');
71
71
  lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
72
72
  lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
73
- lines.push('- `dotmd prompts new <name>` — save a resume-prompt to docs/prompts/ (pipe stdin or @path for body)');
74
- lines.push('- `dotmd prompts next` — consume oldest pending prompt (prints body, auto-archives)');
75
- lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
73
+ lines.push('- `dotmd new prompt <name>` — save a resume-prompt to docs/prompts/ (pipe stdin or @path for body)');
74
+ 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');
76
76
  lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
77
77
  lines.push('- `dotmd actionable` — ready plans with next steps (what to promote)');
78
78
  lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
@@ -87,7 +87,7 @@ function generatePlansCommand(config, version) {
87
87
  lines.push('If the user asks to change a plan\'s status, use `dotmd set <status> <file>`.');
88
88
  lines.push('If the user asks to archive a plan, use `dotmd set archived <file>` (or `dotmd archive <file>`).');
89
89
  lines.push('');
90
- lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt", "load that one" — consume it with `dotmd prompts use <file>` (atomically prints the body and archives the prompt so it cannot be double-consumed). Do NOT `cat` it, read it with the file-reading tool, or copy its body into chat. To pick the oldest pending prompt without naming a file, use `dotmd prompts next`.');
90
+ lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt", "load that one" — consume it with `dotmd use <file>` (atomically prints the body and archives the prompt so it cannot be double-consumed). Do NOT `cat` it, read it with the file-reading tool, or copy its body into chat. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
91
91
  lines.push('');
92
92
 
93
93
  return lines.join('\n');
@@ -100,12 +100,12 @@ function generateBatonCommand(config, version) {
100
100
  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.');
101
101
  lines.push('');
102
102
  lines.push('2. **Close out via `dotmd set <status>`.** Pick the status that matches reality:');
103
- lines.push(' - `dotmd set active <file>` — work continues, release the lease back to the queue');
103
+ lines.push(' - `dotmd set active <file>` — work continues, return the plan to the active queue');
104
104
  lines.push(' - `dotmd set archived <file>` — fully shipped (also: `dotmd archive <file>`)');
105
105
  lines.push(' - `dotmd set paused <file>` / `awaiting <file>` / `partial <file>` / `blocked <file>` — when the status really changed');
106
- lines.push(' `set` releases the held lease automatically when transitioning out of `in-session`.');
106
+ lines.push(' `set` clears the in-session marker automatically when transitioning to any other status.');
107
107
  lines.push('');
108
- lines.push('If you don\'t already know which plan you hold: `dotmd hud --json` and read `.owned`. Do NOT use `dotmd plans --status in-session` — that lists every session\'s holdings, not just yours.');
108
+ 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.');
109
109
  lines.push('');
110
110
  lines.push('The next session\'s `dotmd hud` (SessionStart hook) surfaces the pending prompt automatically.');
111
111
  lines.push('');
@@ -149,10 +149,10 @@ function generateDocsCommand(config, version) {
149
149
  lines.push('Lifecycle:');
150
150
  lines.push('- `dotmd new plan <name>` — scaffold new plan');
151
151
  lines.push('- `dotmd new doc <name>` — scaffold reference doc');
152
- lines.push('- `dotmd prompts new <name> "<body>"` — save a resume-prompt');
153
- lines.push('- `dotmd prompts next` — consume oldest pending prompt (prints body, auto-archives)');
154
- lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
155
- lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / release / status bump; infers path from held lease)');
152
+ lines.push('- `dotmd new prompt <name>` — save a resume-prompt (pipe stdin or @path for body)');
153
+ lines.push('- `dotmd use` — consume oldest pending prompt (prints body, auto-archives)');
154
+ lines.push('- `dotmd use <file>` — open any doc by type: prompt consume, plan → start work, doc → read');
155
+ lines.push('- `dotmd set <status> [<file>]` — unified transition (archive / status bump; infers path from your active in-session plan)');
156
156
  lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
157
157
  lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
158
158
  lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
@@ -161,7 +161,7 @@ function generateDocsCommand(config, version) {
161
161
  lines.push('- `dotmd fix-refs` — repair broken references and body links');
162
162
  lines.push('- `dotmd rename <old> <new>` — rename doc + update all references');
163
163
  lines.push('');
164
- lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt" — consume it with `dotmd prompts use <file>` (prints the body and archives atomically). Do NOT `cat` it or read it with the file-reading tool. To pick the oldest pending prompt without naming a file, use `dotmd prompts next`.');
164
+ lines.push('**Saved prompts (`docs/prompts/*.md`):** if the user references a file under `docs/prompts/` — e.g. "resume via docs/prompts/foo.md", "use this prompt" — consume it with `dotmd use <file>` (prints the body and archives atomically). Do NOT `cat` it or read it with the file-reading tool. To pick the oldest pending prompt without naming a file, run `dotmd use` with no arg.');
165
165
  lines.push('');
166
166
 
167
167
  return lines.join('\n');
package/src/commands.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // templates points at a real command.
5
5
  export const KNOWN_COMMANDS = [
6
6
  'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context', 'hud',
7
- 'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'status', 'set', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
7
+ 'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'status', 'set', 'use', 'next', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
8
8
  'unblocks', 'health', 'glossary', 'modules', 'module',
9
9
  'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
10
10
  'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
package/src/hud.mjs CHANGED
@@ -122,7 +122,7 @@ export function runHud(argv, config) {
122
122
  // hud's job is purely to remind the agent which verbs exist, since that's
123
123
  // the one thing the agent reaches for `--help` to recover. Keep it tight:
124
124
  // one line, the minimum verb set.
125
- lines.push(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> prompts next|use|new archive <file>'));
125
+ lines.push(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] → oldest pending prompt)'));
126
126
 
127
127
  if (refreshed.length > 0) {
128
128
  const from = refreshed[0].from;
package/src/lifecycle.mjs CHANGED
@@ -115,7 +115,7 @@ export async function runStatus(argv, config, opts = {}) {
115
115
  let newStatus = argv[1];
116
116
 
117
117
  if (!opts.suppressDeprecation) {
118
- process.stderr.write(dim('`dotmd status <file> <status>` is deprecated; prefer `dotmd set <status> [<file>]` (note: <status> first, <file> optional when a lease is held). Removed in a future major.\n'));
118
+ process.stderr.write(dim('`dotmd status <file> <status>` is deprecated; prefer `dotmd set <status> [<file>]` (note: <status> first; <file> optional when a plan is in-session). Removed in a future major.\n'));
119
119
  }
120
120
 
121
121
  if (!input) { die('Usage: dotmd status <file> <new-status>'); }
@@ -326,11 +326,11 @@ export async function runPickup(argv, config, opts = {}) {
326
326
  leaseOutcome = result.outcome;
327
327
  if (result.outcome === 'conflict-alive') {
328
328
  const c = result.conflict;
329
- die(`Held by ${c.host}/${c.session} (pid ${c.pid}) since ${c.pickedUpAt}.\nUse --takeover to override.\n ${repoPath}`);
329
+ die(`Plan is in-session with ${c.host}/${c.session} (pid ${c.pid}) since ${c.pickedUpAt}.\nUse --takeover to override.\n ${repoPath}`);
330
330
  }
331
331
  if (result.outcome === 'conflict-stale') {
332
332
  const c = result.conflict;
333
- die(`Stale in-session lease from ${c.host}/${c.session} since ${c.pickedUpAt} (>${STALE_LEASE_AGE_HOURS}h old).\nUse --takeover to claim.\n ${repoPath}`);
333
+ die(`Plan flagged in-session by ${c.host}/${c.session} since ${c.pickedUpAt} (>${STALE_LEASE_AGE_HOURS}h ago, looks abandoned).\nUse --takeover to claim.\n ${repoPath}`);
334
334
  }
335
335
  if (oldStatus !== 'in-session') {
336
336
  updateFrontmatter(filePath, { status: 'in-session', updated: today });
@@ -370,7 +370,7 @@ export async function runPickup(argv, config, opts = {}) {
370
370
  process.stderr.write(`${green('▶ Picked up')}: ${repoPath} (${oldStatus ?? 'unset'} → in-session)\n\n`);
371
371
  }
372
372
  if (fullBody) {
373
- const header = `[dotmd] holding ${repoPath} — close with: dotmd set <status> ${repoPath}\n---\n`;
373
+ const header = `[dotmd] in-session: ${repoPath} — close with: dotmd set <status> ${repoPath}\n---\n`;
374
374
  process.stdout.write(header);
375
375
  const content = (body ?? '').trim();
376
376
  if (content) process.stdout.write(content + '\n');
@@ -194,7 +194,7 @@ export function buildCard(filePath, raw, config) {
194
194
  // Render the card to human-format string.
195
195
  export function renderCard(card) {
196
196
  const lines = [];
197
- lines.push(`[dotmd] holding ${card.path} — close with: dotmd set <status> ${card.path}`);
197
+ lines.push(`[dotmd] in-session: ${card.path} — close with: dotmd set <status> ${card.path}`);
198
198
  lines.push('---');
199
199
  lines.push(`# ${card.title}`);
200
200
  const meta = [card.status, card.updated && `updated ${card.updated}`].filter(Boolean).join(' · ');
package/src/prompts.mjs CHANGED
@@ -118,7 +118,7 @@ function renderPromptsVerbose(index, config, { hasStatusFlag, includeArchived })
118
118
  }
119
119
  }
120
120
 
121
- function pendingPromptsOldestFirst(config) {
121
+ export function pendingPromptsOldestFirst(config) {
122
122
  const index = buildIndex(config);
123
123
  const prompts = index.docs.filter(d => d.type === 'prompt' && d.status === 'pending');
124
124
 
@@ -192,7 +192,7 @@ function runPromptsUse(argv, config, opts = {}) {
192
192
  consumePrompt(filePath, config, { ...opts, noIndex, showFiles });
193
193
  }
194
194
 
195
- function consumePrompt(filePath, config, opts) {
195
+ export function consumePrompt(filePath, config, opts) {
196
196
  const { dryRun, noIndex, showFiles } = opts;
197
197
  const raw = readFileSync(filePath, 'utf8');
198
198
  const { frontmatter, body } = extractFrontmatter(raw);
package/src/use.mjs ADDED
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
3
+ import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
4
+ import { consumePrompt, pendingPromptsOldestFirst } from './prompts.mjs';
5
+ import { runPickup } from './lifecycle.mjs';
6
+
7
+ // Top-level `dotmd use [file]` — the single "start engaging with this doc"
8
+ // verb. Dispatches by the target doc's `type:` so agents don't have to know
9
+ // the verb-per-type rule:
10
+ // - prompt → print body + archive (one-shot consume)
11
+ // - plan → acquire lease + print body (same as `set in-session`)
12
+ // - doc → print body (read-only)
13
+ // With no argument, consumes the oldest pending prompt.
14
+ export async function runUse(argv, config, opts = {}) {
15
+ const positional = argv.find(a => !a.startsWith('-'));
16
+
17
+ if (!positional) {
18
+ const queue = pendingPromptsOldestFirst(config);
19
+ if (queue.length === 0) die('No pending prompts. Pass a file to use a plan or doc.');
20
+ const head = queue[0];
21
+ if (!head.abs) die(`Could not resolve path: ${head.doc.path}`);
22
+ return consumePrompt(head.abs, config, opts);
23
+ }
24
+
25
+ const filePath = resolveDocPath(positional, config);
26
+ if (!filePath) die(`File not found: ${positional}`);
27
+
28
+ const raw = readFileSync(filePath, 'utf8');
29
+ const { frontmatter } = extractFrontmatter(raw);
30
+ const parsed = parseSimpleFrontmatter(frontmatter);
31
+ const type = asString(parsed.type);
32
+
33
+ if (type === 'prompt') {
34
+ return consumePrompt(filePath, config, opts);
35
+ }
36
+ if (type === 'plan') {
37
+ // Delegate to the lease-acquisition path. Same flow as `set in-session`.
38
+ return runPickup([filePath, ...argv.filter(a => a !== positional)], config, opts);
39
+ }
40
+ // Anything else (doc, untyped, custom): print the body. The frontmatter
41
+ // already names everything an agent needs to know about lifecycle for that
42
+ // type, so the verb stays a no-op read for non-actionable types.
43
+ process.stdout.write(raw);
44
+ if (!raw.endsWith('\n')) process.stdout.write('\n');
45
+ process.stderr.write(`${toRepoPath(filePath, config.repoRoot)} (${type ?? 'untyped'})\n`);
46
+ }
package/src/validate.mjs CHANGED
@@ -186,14 +186,14 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
186
186
  doc.warnings.push({
187
187
  path: doc.path,
188
188
  level: 'warning',
189
- message: `\`status: in-session\` but no active lease found (last session may have crashed without releasing). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
189
+ message: `\`status: in-session\` but no session is actually working on this (previous session may have crashed). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
190
190
  });
191
191
  } else if (isLeaseStale(lease)) {
192
192
  const ageHours = Math.floor((Date.now() - new Date(lease.pickedUpAt).getTime()) / (1000 * 60 * 60));
193
193
  doc.warnings.push({
194
194
  path: doc.path,
195
195
  level: 'warning',
196
- message: `\`status: in-session\` but lease is stale (last touched ${ageHours}h ago, >${STALE_LEASE_AGE_HOURS}h threshold). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
196
+ message: `\`status: in-session\` but last activity was ${ageHours}h ago (>${STALE_LEASE_AGE_HOURS}h, looks abandoned). Run \`dotmd set active ${doc.path}\` to clear and re-queue.`,
197
197
  });
198
198
  }
199
199
  }