dotmd-cli 0.49.4 → 0.49.5

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
@@ -128,7 +128,7 @@ Every document can have a `type` field in its frontmatter. Types determine which
128
128
  |------|---------|----------------|
129
129
  | `plan` | Execution plans | `in-session`, `active`, `planned`, `blocked`, `partial`, `paused`, `awaiting`, `queued-after`, `archived` |
130
130
  | `doc` | Design docs, specs, ADRs, RFCs, reference material | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
131
- | `prompt` | Saved prompts that seed future Claude sessions | `pending`, `shelved`, `claimed`, `archived` |
131
+ | `prompt` | Saved prompts that seed future Claude sessions | `pending`, `held`, `shelved`, `claimed`, `archived` |
132
132
 
133
133
  Documents without a `type` field use the global `statuses.order` from config.
134
134
 
@@ -206,7 +206,7 @@ dotmd glossary <term> Look up domain terms + related docs
206
206
  dotmd watch [command] Re-run a command on file changes
207
207
  dotmd diff [file] Show changes since last updated date
208
208
  dotmd new <type> <name> Create a new doc (type: doc, plan, or prompt)
209
- dotmd prompts [sub] Manage saved prompts (list, next, use, shelve, archive, new)
209
+ dotmd prompts [sub] Manage saved prompts (list, next, use, hold, archive, new)
210
210
  dotmd journal [flags] View opt-in command-usage journal (DOTMD_JOURNAL=1)
211
211
  dotmd init Create starter config + docs directory
212
212
  dotmd completions <shell> Output shell completion script (bash, zsh)
@@ -303,16 +303,17 @@ dotmd prompts # list pending prompts (default)
303
303
  dotmd prompts list --all # all statuses
304
304
  dotmd prompts next # print body of oldest pending + auto-archive (one-shot)
305
305
  dotmd prompts use <file> # print body of a specific prompt + auto-archive
306
- dotmd prompts shelve <file> # park a prompt (status → shelved): kept in list,
307
- # hidden from hud/briefing, skipped by `next`
308
- dotmd prompts unshelve <file> # move a shelved prompt back to pending
306
+ dotmd prompts hold <file> # park a prompt (status → held) under prompts/held/:
307
+ # kept in list, hidden from hud/briefing, skipped by `next`
308
+ dotmd prompts unhold <file> # move a held prompt back to pending
309
+ dotmd prompts shelve <file> # legacy alias for `hold`
309
310
  dotmd prompts archive <file> # archive without printing the body
310
311
  dotmd prompts new <name> [body] # alias for `dotmd new prompt`
311
312
  ```
312
313
 
313
- `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. Shelved prompts are kept out of the SessionStart surface — use them for "saved but not next."
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
315
 
315
- Statuses: `pending` (drafted, awaiting a session), `shelved` (saved but parked — visible in `prompts list`, hidden from `hud`/`briefing`, skipped by `prompts next`), `archived` (consumed or filed away). `claimed` is reserved for a future "in-flight" state but is currently a synonym for archived in practice.
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.
316
317
 
317
318
  ### Command Journal (opt-in)
318
319
 
package/bin/dotmd.mjs CHANGED
@@ -44,7 +44,7 @@ const FLAG_SPECS = {
44
44
  prompts: {
45
45
  flags: new Set(['--json', '--status', '--include-archived', '--sort', '--limit', '--all', '--no-index', '--show-files', '--body', '--message', '--title']),
46
46
  values: new Set(['--status', '--sort', '--limit', '--body', '--message', '--title']),
47
- subcommands: new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'shelve', 'unshelve', 'status']),
47
+ subcommands: new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve', 'status']),
48
48
  },
49
49
  };
50
50
 
@@ -65,12 +65,12 @@ const HELP = {
65
65
 
66
66
  Common commands:
67
67
  plans Live plans (excludes archived)
68
- prompts Prompt queue/admin (list, next, archive, new, shelve)
68
+ prompts Prompt queue/admin (list, next, archive, new, hold)
69
69
  briefing Full briefing with plan counts + next steps
70
70
  agent-context Compact bounded JSON context for agents
71
71
  set <status> [file] Transition status (start work, finish, archive — all via target status)
72
72
  new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
73
- use [<file>] Open a doc by type: prompt → consume, plan → start, doc → read
73
+ use [<file-or-prompt-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
74
74
  (no file: consume oldest pending prompt)
75
75
  archive <file> Close out a plan (status → archived, move, update refs)
76
76
 
@@ -95,8 +95,8 @@ View & Query:
95
95
  focus [status] [--json] Detailed view for one status group
96
96
  query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
97
97
  plans Live plans (excludes archived; --include-archived for all)
98
- use [<file>] Open a doc by type: prompt → consume, plan → start, doc → read
99
- prompts [list|archive|new|shelve] Prompt admin (list / archive / save / shelve). Use \`dotmd use\` to consume.
98
+ use [<file-or-prompt-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
99
+ prompts [list|archive|new|hold] Prompt admin (list / archive / save / hold). Use \`dotmd use\` to consume.
100
100
  stale Stale docs (preset)
101
101
  actionable Docs with next steps (preset)
102
102
 
@@ -235,9 +235,13 @@ prompt statuses
235
235
  \`dotmd prompts use <file>\` prints body + archives atomically.
236
236
  \`dotmd prompts next\` does the same for the oldest pending.
237
237
 
238
- shelved Saved but hidden from \`hud\` / \`briefing\` / \`prompts next\`.
238
+ held Saved under prompts/held/ and hidden from \`hud\` /
239
+ \`briefing\` / \`prompts next\`.
239
240
  Still listed by \`dotmd prompts list\`.
240
- \`dotmd prompts unshelve <file>\` → pending.
241
+ \`dotmd prompts unhold <file>\` → pending.
242
+
243
+ shelved Legacy spelling for held prompts. \`dotmd prompts shelve\`
244
+ now writes \`status: held\`.
241
245
 
242
246
  claimed Legacy intermediate state (atomic use → archived now).
243
247
 
@@ -920,10 +924,11 @@ Subcommands:
920
924
  resume <file-or-slug> Alias for \`use\` — same behavior, easier name
921
925
  when continuing a session
922
926
  archive <file-or-slug> Archive a prompt without printing its body
923
- shelve <file-or-slug> Park a prompt (status → shelved): kept in list,
924
- hidden from hud/briefing pending surfaces, skipped
925
- by \`prompts next\`. Use for "saved but not next."
926
- unshelve <file-or-slug> Move a shelved prompt back to pending.
927
+ hold <file-or-slug> Park a prompt (status → held) under prompts/held/:
928
+ kept in list, hidden from hud/briefing pending
929
+ surfaces, skipped by \`prompts next\`.
930
+ unhold <file-or-slug> Move a held prompt back to pending.
931
+ shelve / unshelve Legacy aliases for hold / unhold.
927
932
  new <slug> [body] Create a new prompt (alias for
928
933
  \`dotmd new prompt <slug> [body]\`)
929
934
 
@@ -931,7 +936,7 @@ Subcommands:
931
936
  slug matching a prompt basename, or a unique substring of a prompt
932
937
  path. Ambiguous substrings error with the candidate list.
933
938
 
934
- Default prompt statuses: pending, shelved, claimed, archived.
939
+ Default prompt statuses: pending, held, shelved, claimed, archived.
935
940
 
936
941
  Examples:
937
942
  dotmd prompts # pending prompts (default)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.49.4",
3
+ "version": "0.49.5",
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",
package/src/config.mjs CHANGED
@@ -11,6 +11,7 @@ const CONFIG_FILENAMES = ['dotmd.config.mjs', '.dotmd.config.mjs', 'dotmd.config
11
11
  const REPLACE_KEYS = new Set([
12
12
  'statuses.staleDays',
13
13
  'statuses.rootStatuses',
14
+ 'lifecycle.filedStatuses',
14
15
  'presets',
15
16
  'context',
16
17
  ]);
@@ -36,8 +37,8 @@ const DEFAULTS = {
36
37
  staleDays: { draft: 30, active: 14, review: 14 },
37
38
  },
38
39
  prompt: {
39
- statuses: ['pending', 'shelved', 'claimed', 'archived'],
40
- context: { expanded: ['pending'], listed: ['shelved'], counted: ['claimed', 'archived'] },
40
+ statuses: ['pending', 'held', 'shelved', 'claimed', 'archived'],
41
+ context: { expanded: ['pending'], listed: ['held', 'shelved'], counted: ['claimed', 'archived'] },
41
42
  staleDays: { pending: 30 },
42
43
  },
43
44
  },
@@ -58,10 +59,10 @@ const DEFAULTS = {
58
59
  skipStaleFor: ['archived', 'reference', 'partial', 'queued-after'],
59
60
  skipWarningsFor: ['archived', 'partial', 'queued-after'],
60
61
  terminalStatuses: ['archived', 'deprecated', 'reference'],
61
- // F15: opt-in per-status `{ filed: true }` in `types.<type>.statuses`
62
- // populates this map (status dirName). Empty by default archive: true
63
- // remains a separate primitive untouched.
64
- filedStatuses: {},
62
+ // F15: per-status filing buckets (status dirName). Built-in held/paused
63
+ // statuses file under the owning type folder; archive remains a separate
64
+ // primitive untouched.
65
+ filedStatuses: { held: 'held', shelved: 'held', paused: 'held' },
65
66
  },
66
67
 
67
68
  taxonomy: {
package/src/lifecycle.mjs CHANGED
@@ -26,6 +26,23 @@ function findFileRoot(filePath, config) {
26
26
  return roots.find(r => filePath.startsWith(r + '/')) ?? config.docsRoot;
27
27
  }
28
28
 
29
+ function defaultTypeDir(docType, config) {
30
+ if (docType === 'plan') return 'plans';
31
+ if (docType === 'prompt') return 'prompts';
32
+ const templateDir = config.raw?.templates?.[docType]?.dir;
33
+ return typeof templateDir === 'string' && templateDir ? templateDir : null;
34
+ }
35
+
36
+ function findFilingRoot(filePath, fileRoot, docType, config) {
37
+ const dirName = defaultTypeDir(docType, config);
38
+ if (!dirName) return fileRoot;
39
+ if (path.basename(fileRoot) === dirName) return fileRoot;
40
+
41
+ const relSegments = path.relative(fileRoot, filePath).split(path.sep);
42
+ if (relSegments[0] === dirName) return path.join(fileRoot, dirName);
43
+ return fileRoot;
44
+ }
45
+
29
46
  // Best-effort index regen for any doc-set or doc-status mutation. The
30
47
  // generated block groups by status and embeds per-doc snapshots, so any
31
48
  // change that affects what would render leaves the index stale. Wrapped
@@ -167,8 +184,9 @@ export async function runStatus(argv, config, opts = {}) {
167
184
 
168
185
  const today = nowIso();
169
186
  const archiveDir = path.join(fileRoot, config.archiveDir);
170
- const relFromRoot = path.relative(fileRoot, filePath);
171
- const relSegments = relFromRoot.split(path.sep);
187
+ const filingRoot = findFilingRoot(filePath, fileRoot, docType, config);
188
+ const relFromFilingRoot = path.relative(filingRoot, filePath);
189
+ const relSegments = relFromFilingRoot.split(path.sep);
172
190
  const inArchive = isArchivedPath(toRepoPath(filePath, config.repoRoot), config);
173
191
  const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !inArchive;
174
192
  const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && inArchive;
@@ -200,12 +218,12 @@ export async function runStatus(argv, config, opts = {}) {
200
218
  finalPath = targetPath;
201
219
  }
202
220
  if (isFiling) {
203
- const targetPath = path.join(fileRoot, newFiledDir, path.basename(filePath));
221
+ const targetPath = path.join(filingRoot, newFiledDir, path.basename(filePath));
204
222
  process.stdout.write(`${prefix} Would file: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
205
223
  finalPath = targetPath;
206
224
  }
207
225
  if (isUnfiling) {
208
- const targetPath = path.join(fileRoot, path.basename(filePath));
226
+ const targetPath = path.join(filingRoot, path.basename(filePath));
209
227
  process.stdout.write(`${prefix} Would unfile: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
210
228
  finalPath = targetPath;
211
229
  }
@@ -236,7 +254,7 @@ export async function runStatus(argv, config, opts = {}) {
236
254
  }
237
255
 
238
256
  if (isFiling) {
239
- const targetDir = path.join(fileRoot, newFiledDir);
257
+ const targetDir = path.join(filingRoot, newFiledDir);
240
258
  mkdirSync(targetDir, { recursive: true });
241
259
  const targetPath = path.join(targetDir, path.basename(filePath));
242
260
  if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
@@ -246,7 +264,7 @@ export async function runStatus(argv, config, opts = {}) {
246
264
  }
247
265
 
248
266
  if (isUnfiling) {
249
- const targetPath = path.join(fileRoot, path.basename(filePath));
267
+ const targetPath = path.join(filingRoot, path.basename(filePath));
250
268
  if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
251
269
  const result = gitMv(filePath, targetPath, config.repoRoot);
252
270
  if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
package/src/prompts.mjs CHANGED
@@ -11,7 +11,7 @@ import { green, dim } from './color.mjs';
11
11
  // `resume` is an alias for `use` — agents reach for "resume" when continuing a
12
12
  // session; `use` reads as internal mechanics. Both names stay valid; the
13
13
  // canonical output ("Consumed: …") is unchanged.
14
- const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'shelve', 'unshelve']);
14
+ const SUBCOMMANDS = new Set(['list', 'next', 'use', 'resume', 'archive', 'new', 'hold', 'unhold', 'shelve', 'unshelve']);
15
15
 
16
16
  export async function runPrompts(argv, config, opts = {}) {
17
17
  const sub = argv[0];
@@ -28,8 +28,10 @@ export async function runPrompts(argv, config, opts = {}) {
28
28
  case 'resume': return runPromptsUse(rest, config, opts);
29
29
  case 'archive': return runPromptsArchive(rest, config, opts);
30
30
  case 'new': return runPromptsNew(rest, config, opts);
31
- case 'shelve': return runPromptsShelve(rest, config, opts);
32
- case 'unshelve': return runPromptsUnshelve(rest, config, opts);
31
+ case 'hold': return runPromptsHold(rest, config, opts);
32
+ case 'unhold': return runPromptsUnhold(rest, config, opts);
33
+ case 'shelve': return runPromptsHold(rest, config, opts);
34
+ case 'unshelve': return runPromptsUnhold(rest, config, opts);
33
35
  }
34
36
  }
35
37
 
@@ -190,7 +192,8 @@ function runPromptsNext(argv, config, opts = {}) {
190
192
  // path + '.md', exact basename match across type: prompt docs, substring
191
193
  // match across type: prompt docs. Returns the absolute path or dies with a
192
194
  // helpful message (no match / ambiguous match).
193
- function resolvePromptInput(input, config) {
195
+ export function resolvePromptInput(input, config, options = {}) {
196
+ const dieOnMiss = options.dieOnMiss !== false;
194
197
  const direct = resolveDocPath(input, config);
195
198
  if (direct) return direct;
196
199
 
@@ -201,7 +204,10 @@ function resolvePromptInput(input, config) {
201
204
 
202
205
  const index = buildIndex(config);
203
206
  const prompts = index.docs.filter(d => d.type === 'prompt');
204
- if (prompts.length === 0) die(`No prompts in the index.`);
207
+ if (prompts.length === 0) {
208
+ if (dieOnMiss) die(`No prompts in the index.`);
209
+ return null;
210
+ }
205
211
 
206
212
  const slug = input.replace(/\.md$/, '');
207
213
 
@@ -219,7 +225,8 @@ function resolvePromptInput(input, config) {
219
225
  die(`Multiple prompts match "${input}":\n${bySubstring.map(d => ' ' + d.path).join('\n')}`);
220
226
  }
221
227
 
222
- die(`No prompt found matching: ${input}`);
228
+ if (dieOnMiss) die(`No prompt found matching: ${input}`);
229
+ return null;
223
230
  }
224
231
 
225
232
  function runPromptsUse(argv, config, opts = {}) {
@@ -300,16 +307,16 @@ async function runPromptsNew(argv, config, opts = {}) {
300
307
  return runNew(['prompt', ...argv], config, opts);
301
308
  }
302
309
 
303
- async function runPromptsShelve(argv, config, opts = {}) {
310
+ async function runPromptsHold(argv, config, opts = {}) {
304
311
  const input = argv.find(a => !a.startsWith('-'));
305
- if (!input) die('Usage: dotmd prompts shelve <file-or-slug>');
312
+ if (!input) die('Usage: dotmd prompts hold <file-or-slug>');
306
313
  const filePath = resolvePromptInput(input, config);
307
- return runStatus([filePath, 'shelved'], config, opts);
314
+ return runStatus([filePath, 'held'], config, opts);
308
315
  }
309
316
 
310
- async function runPromptsUnshelve(argv, config, opts = {}) {
317
+ async function runPromptsUnhold(argv, config, opts = {}) {
311
318
  const input = argv.find(a => !a.startsWith('-'));
312
- if (!input) die('Usage: dotmd prompts unshelve <file-or-slug>');
319
+ if (!input) die('Usage: dotmd prompts unhold <file-or-slug>');
313
320
  const filePath = resolvePromptInput(input, config);
314
321
  return runStatus([filePath, 'pending'], config, opts);
315
322
  }
package/src/use.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
3
3
  import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
4
- import { consumePrompt, pendingPromptsOldestFirst } from './prompts.mjs';
4
+ import { consumePrompt, pendingPromptsOldestFirst, resolvePromptInput } from './prompts.mjs';
5
5
  import { runPickup } from './lifecycle.mjs';
6
6
 
7
7
  // Top-level `dotmd use [file]` — the single "start engaging with this doc"
@@ -22,7 +22,7 @@ export async function runUse(argv, config, opts = {}) {
22
22
  return consumePrompt(head.abs, config, opts);
23
23
  }
24
24
 
25
- const filePath = resolveDocPath(positional, config);
25
+ const filePath = resolveDocPath(positional, config) ?? resolvePromptInput(positional, config, { dieOnMiss: false });
26
26
  if (!filePath) die(`File not found: ${positional}`);
27
27
 
28
28
  const raw = readFileSync(filePath, 'utf8');
package/src/validate.mjs CHANGED
@@ -49,6 +49,11 @@ function liveTypeDirsForRoots(config) {
49
49
  for (const dirName of filedDirs) {
50
50
  if (path.basename(rootRel) === dirName) continue;
51
51
  set.add(rootRel ? `${rootRel}/${dirName}` : dirName);
52
+ for (const typeDir of BUILTIN_TYPE_DIR_NAMES) {
53
+ if (path.basename(rootRel) === typeDir) continue;
54
+ const typeRoot = rootRel ? `${rootRel}/${typeDir}` : typeDir;
55
+ set.add(`${typeRoot}/${dirName}`);
56
+ }
52
57
  }
53
58
  }
54
59
  return set;