dotmd-cli 0.57.0 → 0.58.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 = {
@@ -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.58.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
@@ -32,6 +32,17 @@ function findFilingRoot(filePath, fileRoot, docType, config) {
32
32
  return fileRoot;
33
33
  }
34
34
 
35
+ // Base directory a doc archives under. Types in lifecycle.archiveNestedTypes
36
+ // (default: prompt) archive into their own <typeDir>/ — yielding
37
+ // <typeDir>/<archiveDir> (e.g. docs/prompts/archived/) — so session-local
38
+ // prompt churn doesn't bury plans/docs in the shared <root>/<archiveDir>.
39
+ // Everything else archives under fileRoot. Used by both runStatus (set
40
+ // archived) and runArchive so the two paths stay in lockstep.
41
+ function archiveBaseFor(filePath, fileRoot, docType, config) {
42
+ const nest = config.lifecycle?.archiveNestedTypes?.has(docType) ?? false;
43
+ return nest ? findFilingRoot(filePath, fileRoot, docType, config) : fileRoot;
44
+ }
45
+
35
46
  // Best-effort index regen for any doc-set or doc-status mutation. The
36
47
  // generated block groups by status and embeds per-doc snapshots, so any
37
48
  // change that affects what would render leaves the index stale. Wrapped
@@ -172,8 +183,11 @@ export async function runStatus(argv, config, opts = {}) {
172
183
  }
173
184
 
174
185
  const today = nowIso();
175
- const archiveDir = path.join(fileRoot, config.archiveDir);
176
186
  const filingRoot = findFilingRoot(filePath, fileRoot, docType, config);
187
+ // Type-aware archive base (prompts nest under their type dir by default);
188
+ // unarchive reuses archiveBase so a prompt restores to its type dir.
189
+ const archiveBase = archiveBaseFor(filePath, fileRoot, docType, config);
190
+ const archiveDir = path.join(archiveBase, config.archiveDir);
177
191
  const relFromFilingRoot = path.relative(filingRoot, filePath);
178
192
  const relSegments = relFromFilingRoot.split(path.sep);
179
193
  const inArchive = isArchivedPath(toRepoPath(filePath, config.repoRoot), config);
@@ -202,7 +216,7 @@ export async function runStatus(argv, config, opts = {}) {
202
216
  finalPath = targetPath;
203
217
  }
204
218
  if (isUnarchiving) {
205
- const targetPath = path.join(fileRoot, path.basename(filePath));
219
+ const targetPath = path.join(archiveBase, path.basename(filePath));
206
220
  process.stdout.write(`${prefix} Would move: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
207
221
  finalPath = targetPath;
208
222
  }
@@ -235,7 +249,7 @@ export async function runStatus(argv, config, opts = {}) {
235
249
  }
236
250
 
237
251
  if (isUnarchiving) {
238
- const targetPath = path.join(fileRoot, path.basename(filePath));
252
+ const targetPath = path.join(archiveBase, path.basename(filePath));
239
253
  if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
240
254
  const result = gitMv(filePath, targetPath, config.repoRoot);
241
255
  if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
@@ -436,7 +450,9 @@ export function runArchive(argv, config, opts = {}) {
436
450
  const closeoutAction = closeoutTemplate ? planCloseoutInjection(body) : null;
437
451
 
438
452
  const today = nowIso();
439
- const targetDir = path.join(archiveFileRoot, config.archiveDir);
453
+ // Type-aware: prompts archive under docs/prompts/archived/ by default (see
454
+ // archiveBaseFor); plans/docs keep the shared <root>/archived/.
455
+ const targetDir = path.join(archiveBaseFor(filePath, archiveFileRoot, asString(parsed.type), config), config.archiveDir);
440
456
  const targetPath = uniqueArchiveTarget(targetDir, path.basename(filePath));
441
457
  const oldRepoPath = toRepoPath(filePath, config.repoRoot);
442
458
  const newRepoPath = toRepoPath(targetPath, config.repoRoot);