dotmd-cli 0.57.0 → 0.59.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
@@ -23,6 +23,8 @@ If you drive dotmd from Claude Code, install the **dotmd plugin**. It teaches ev
23
23
 
24
24
  The plugin bundles the hooks (`SessionStart`/`SubagentStart` priming, a `PreToolUse` guard) and a canonical workflow skill, so guidance travels to **every** repo automatically — no per-repo setup. It calls the `dotmd` CLI, so keep `npm install -g dotmd-cli` installed too. (Source: `plugins/dotmd/` in this repo.)
25
25
 
26
+ > **Upgrading to 0.57.0+:** per-repo `.claude/commands/{plans,docs,baton}.md` scaffolding is retired — that guidance now ships via the plugin's workflow skill and `/plans`, `/docs`, `/prompts`, `/baton` commands. On the next `dotmd hud` (SessionStart), dotmd removes those generated files (only banner-stamped `<!-- dotmd-generated -->` ones — your hand-authored command files are never touched). If you'd committed them, you'll see deletions to commit — that's expected. Run `claude plugin update dotmd@dotmd` to pick up `/baton`.
27
+
26
28
  ## Quick Start
27
29
 
28
30
  ```bash
@@ -803,6 +805,11 @@ export const lifecycle = {
803
805
  skipStaleFor: ['archived'],
804
806
  skipWarningsFor: ['archived'],
805
807
  terminalStatuses: ['archived'],
808
+ // Types that archive into their own <typeDir>/<archiveDir> (e.g.
809
+ // docs/prompts/archived/) instead of the shared <root>/<archiveDir>.
810
+ // Defaults to ['prompt'] so session-local prompt churn doesn't bury
811
+ // plans and docs in the shared archive. Set to [] to disable.
812
+ archiveNestedTypes: ['prompt'],
806
813
  };
807
814
 
808
815
  export const taxonomy = {
package/bin/dotmd.mjs CHANGED
@@ -507,11 +507,16 @@ Options:
507
507
  --json Output errors and warnings as JSON (always full detail)
508
508
  --dry-run, -n Preview fixes without writing (with --fix)`,
509
509
 
510
- archive: `dotmd archive <file> — archive a document
510
+ archive: `dotmd archive <file-or-slug> — archive a document
511
511
 
512
512
  Sets status to 'archived', moves to the archive directory, auto-updates
513
513
  references in other docs, and regenerates the index.
514
514
 
515
+ <file-or-slug> resolves like \`dotmd use\`: an exact path wins, but a bare
516
+ slug / basename (e.g. \`archive resume-foo\`) falls back to a recursive
517
+ basename match under the doc roots. An ambiguous basename (the same name in
518
+ two places) errors with the candidate list instead of guessing.
519
+
515
520
  Options:
516
521
  --no-index Skip index regen. Use when multiple sessions are
517
522
  working concurrently and you want a path-limited
@@ -130,6 +130,9 @@ export const statuses = {
130
130
  // skipStaleFor: ['archived'], // skip staleness checks
131
131
  // skipWarningsFor: ['archived'], // skip validation warnings (summary, etc.)
132
132
  // terminalStatuses: ['archived', 'deprecated', 'reference'], // skip current_state/next_step warnings, exclude from stats scope
133
+ // archiveNestedTypes: ['prompt'], // types that archive under <typeDir>/<archiveDir>
134
+ // // (e.g. docs/prompts/archived/) instead of the
135
+ // // shared <root>/<archiveDir>; set [] to disable
133
136
  // };
134
137
 
135
138
  // Taxonomy validation — set fields to null to skip validation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.57.0",
3
+ "version": "0.59.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",
package/src/config.mjs CHANGED
@@ -63,6 +63,12 @@ const DEFAULTS = {
63
63
  // statuses file under the owning type folder; archive remains a separate
64
64
  // primitive untouched.
65
65
  filedStatuses: { held: 'held', shelved: 'held', paused: 'held' },
66
+ // Types whose archive nests under their own type dir (<typeDir>/<archiveDir>,
67
+ // e.g. docs/prompts/archived/) instead of the shared <root>/<archiveDir>.
68
+ // Prompts are session-local churn — keeping their archive out of the shared
69
+ // docs/archived/ stops them from burying plans/docs there. Set to [] to send
70
+ // every type to the shared archive.
71
+ archiveNestedTypes: ['prompt'],
66
72
  },
67
73
 
68
74
  taxonomy: {
@@ -476,6 +482,9 @@ export async function resolveConfig(cwd, explicitConfigPath) {
476
482
  // F15: filedStatuses keyed by status name, value = directory name. Empty
477
483
  // object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
478
484
  const filedStatuses = new Map(Object.entries(lifecycle.filedStatuses ?? {}));
485
+ // Types that archive into their own <typeDir>/<archiveDir> rather than the
486
+ // shared <root>/<archiveDir> (default: prompt).
487
+ const archiveNestedTypes = new Set(lifecycle.archiveNestedTypes ?? []);
479
488
 
480
489
  // Warn if rootStatuses keys don't match any configured root
481
490
  for (const rootKey of Object.keys(rootStatusesRaw)) {
@@ -507,7 +516,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
507
516
  rootValidStatuses,
508
517
  staleDaysByStatus,
509
518
 
510
- lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses },
519
+ lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses, archiveNestedTypes },
511
520
 
512
521
  validSurfaces,
513
522
  validModules,
package/src/guard.mjs CHANGED
@@ -25,11 +25,17 @@ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'),
25
25
 
26
26
  const SHELL_READERS = new Set(['cat', 'less', 'more', 'head', 'tail', 'bat', 'view', 'open']);
27
27
 
28
- // A path that ends in .md and sits under a `prompts/` directory is a saved
29
- // prompt regardless of which doc root it belongs to — robust across repos
30
- // without needing the resolved config.
28
+ // A path that ends in .md and sits under a `prompts/` directory is a
29
+ // session-local saved prompt regardless of which doc root it belongs to —
30
+ // robust across repos without needing the resolved config. Archived prompts
31
+ // (`…/prompts/archived/…`, the default nested archive for the prompt type) are
32
+ // committable history, NOT session-local, so they're explicitly excluded — the
33
+ // guard must not block committing or reading them.
31
34
  function isPromptPath(p) {
32
- return typeof p === 'string' && p.endsWith('.md') && /(^|\/)prompts\//.test(p);
35
+ if (typeof p !== 'string' || !p.endsWith('.md')) return false;
36
+ if (!/(^|\/)prompts\//.test(p)) return false;
37
+ if (/(^|\/)archived\//.test(p)) return false;
38
+ return true;
33
39
  }
34
40
 
35
41
  // Loose "is this a dotmd-managed doc" test: a .md file under one of the
package/src/lifecycle.mjs CHANGED
@@ -15,6 +15,29 @@ function findFileRoot(filePath, config) {
15
15
  return roots.find(r => filePath.startsWith(r + '/')) ?? config.docsRoot;
16
16
  }
17
17
 
18
+ // Resolve an archive target like `dotmd use` resolves a pickup target: an exact
19
+ // path wins (the resolveDocPath fast path), but a bare slug / basename falls
20
+ // back to a recursive basename match under the doc roots. Mirrors
21
+ // resolvePromptInput's basename pass so the closure verb is as forgiving about
22
+ // naming as `use`/`prompts archive`. A basename shared by two files errors with
23
+ // the candidate list rather than guessing which one to move.
24
+ function resolveArchiveTarget(input, config) {
25
+ const direct = resolveDocPath(input, config);
26
+ if (direct) return direct;
27
+ if (!input.endsWith('.md')) {
28
+ const withExt = resolveDocPath(input + '.md', config);
29
+ if (withExt) return withExt;
30
+ }
31
+
32
+ const slug = input.replace(/\.md$/, '');
33
+ const byBasename = collectDocFiles(config).filter(f => path.basename(f, '.md') === slug);
34
+ if (byBasename.length === 1) return byBasename[0];
35
+ if (byBasename.length > 1) {
36
+ die(`Multiple docs match "${input}" by basename:\n${byBasename.map(f => ' ' + toRepoPath(f, config.repoRoot)).join('\n')}`);
37
+ }
38
+ return null;
39
+ }
40
+
18
41
  function defaultTypeDir(docType, config) {
19
42
  if (docType === 'plan') return 'plans';
20
43
  if (docType === 'prompt') return 'prompts';
@@ -32,6 +55,17 @@ function findFilingRoot(filePath, fileRoot, docType, config) {
32
55
  return fileRoot;
33
56
  }
34
57
 
58
+ // Base directory a doc archives under. Types in lifecycle.archiveNestedTypes
59
+ // (default: prompt) archive into their own <typeDir>/ — yielding
60
+ // <typeDir>/<archiveDir> (e.g. docs/prompts/archived/) — so session-local
61
+ // prompt churn doesn't bury plans/docs in the shared <root>/<archiveDir>.
62
+ // Everything else archives under fileRoot. Used by both runStatus (set
63
+ // archived) and runArchive so the two paths stay in lockstep.
64
+ function archiveBaseFor(filePath, fileRoot, docType, config) {
65
+ const nest = config.lifecycle?.archiveNestedTypes?.has(docType) ?? false;
66
+ return nest ? findFilingRoot(filePath, fileRoot, docType, config) : fileRoot;
67
+ }
68
+
35
69
  // Best-effort index regen for any doc-set or doc-status mutation. The
36
70
  // generated block groups by status and embeds per-doc snapshots, so any
37
71
  // change that affects what would render leaves the index stale. Wrapped
@@ -172,8 +206,11 @@ export async function runStatus(argv, config, opts = {}) {
172
206
  }
173
207
 
174
208
  const today = nowIso();
175
- const archiveDir = path.join(fileRoot, config.archiveDir);
176
209
  const filingRoot = findFilingRoot(filePath, fileRoot, docType, config);
210
+ // Type-aware archive base (prompts nest under their type dir by default);
211
+ // unarchive reuses archiveBase so a prompt restores to its type dir.
212
+ const archiveBase = archiveBaseFor(filePath, fileRoot, docType, config);
213
+ const archiveDir = path.join(archiveBase, config.archiveDir);
177
214
  const relFromFilingRoot = path.relative(filingRoot, filePath);
178
215
  const relSegments = relFromFilingRoot.split(path.sep);
179
216
  const inArchive = isArchivedPath(toRepoPath(filePath, config.repoRoot), config);
@@ -202,7 +239,7 @@ export async function runStatus(argv, config, opts = {}) {
202
239
  finalPath = targetPath;
203
240
  }
204
241
  if (isUnarchiving) {
205
- const targetPath = path.join(fileRoot, path.basename(filePath));
242
+ const targetPath = path.join(archiveBase, path.basename(filePath));
206
243
  process.stdout.write(`${prefix} Would move: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
207
244
  finalPath = targetPath;
208
245
  }
@@ -235,7 +272,7 @@ export async function runStatus(argv, config, opts = {}) {
235
272
  }
236
273
 
237
274
  if (isUnarchiving) {
238
- const targetPath = path.join(fileRoot, path.basename(filePath));
275
+ const targetPath = path.join(archiveBase, path.basename(filePath));
239
276
  if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
240
277
  const result = gitMv(filePath, targetPath, config.repoRoot);
241
278
  if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
@@ -387,7 +424,7 @@ export function runArchive(argv, config, opts = {}) {
387
424
 
388
425
  if (!input) { die('Usage: dotmd archive <file>'); }
389
426
 
390
- const filePath = resolveDocPath(input, config);
427
+ const filePath = resolveArchiveTarget(input, config);
391
428
  if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); }
392
429
 
393
430
  const archiveFileRoot = findFileRoot(filePath, config);
@@ -436,7 +473,9 @@ export function runArchive(argv, config, opts = {}) {
436
473
  const closeoutAction = closeoutTemplate ? planCloseoutInjection(body) : null;
437
474
 
438
475
  const today = nowIso();
439
- const targetDir = path.join(archiveFileRoot, config.archiveDir);
476
+ // Type-aware: prompts archive under docs/prompts/archived/ by default (see
477
+ // archiveBaseFor); plans/docs keep the shared <root>/archived/.
478
+ const targetDir = path.join(archiveBaseFor(filePath, archiveFileRoot, asString(parsed.type), config), config.archiveDir);
440
479
  const targetPath = uniqueArchiveTarget(targetDir, path.basename(filePath));
441
480
  const oldRepoPath = toRepoPath(filePath, config.repoRoot);
442
481
  const newRepoPath = toRepoPath(targetPath, config.repoRoot);