dotmd-cli 0.28.0 → 0.28.2

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
@@ -89,19 +89,19 @@ Every document can have a `type` field in its frontmatter. Types determine which
89
89
  | Type | Purpose | Valid Statuses |
90
90
  |------|---------|----------------|
91
91
  | `plan` | Execution plans | `in-session`, `active`, `planned`, `blocked`, `partial`, `paused`, `awaiting`, `queued-after`, `archived` |
92
- | `doc` | Design docs, specs, ADRs, RFCs | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
93
- | `research` | Investigations, audits, analysis | `active`, `reference`, `archived` |
92
+ | `doc` | Design docs, specs, ADRs, RFCs, reference material | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
93
+ | `prompt` | Saved prompts that seed future Claude sessions | `pending`, `claimed`, `archived` |
94
94
 
95
95
  Documents without a `type` field use the global `statuses.order` from config.
96
96
 
97
- Templates auto-set the type: `--template plan` sets `type: plan`, `--template adr` sets `type: doc`, `--template audit` sets `type: research`.
97
+ `dotmd new <type> <name>` sets the `type:` field automatically (`plan`, `doc`, or `prompt`).
98
98
 
99
99
  Filter by type with `--type`:
100
100
 
101
101
  ```bash
102
102
  dotmd query --type plan --status active # active plans
103
103
  dotmd list --type doc # all docs
104
- dotmd export --type research # export research only
104
+ dotmd export --type prompt # export only saved prompts
105
105
  ```
106
106
 
107
107
  Customize types and their statuses in config with the `types` key. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs).
@@ -150,7 +150,6 @@ dotmd hud Three-line actionable triage (silent when clean —
150
150
  dotmd pickup <file> Pick up a plan (in-session + print body or queued handoff)
151
151
  dotmd release [<file>] Release in-session lease (alias: unpickup)
152
152
  dotmd handoff <file> [...] Queue a resume-prompt sidecar + release
153
- dotmd finish <file> Finish a plan (done or active)
154
153
  dotmd status <file> <status> Transition document status
155
154
  dotmd archive <file> Archive (status + move + update refs)
156
155
  dotmd bulk archive <files> Archive multiple files at once
@@ -167,7 +166,8 @@ dotmd summary <file> AI summary of a document
167
166
  dotmd glossary <term> Look up domain terms + related docs
168
167
  dotmd watch [command] Re-run a command on file changes
169
168
  dotmd diff [file] Show changes since last updated date
170
- dotmd new <name> Create a new document from template
169
+ dotmd new <type> <name> Create a new doc (type: doc, plan, or prompt)
170
+ dotmd prompts [sub] List, claim, or archive saved prompts
171
171
  dotmd init Create starter config + docs directory
172
172
  dotmd completions <shell> Output shell completion script (bash, zsh)
173
173
  ```
@@ -197,32 +197,80 @@ dotmd query --status active --summarize --summarize-limit 3
197
197
 
198
198
  Flags: `--type`, `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`, `--summarize`, `--summarize-limit`, `--model`.
199
199
 
200
- ### Scaffold with Templates
200
+ ### Create Documents
201
+
202
+ The signature is `dotmd new <type> <name> [body]`. `<type>` is one of the built-in types (`doc`, `plan`, `prompt`) or a custom type from your config. If you omit `<type>`, it defaults to `doc`.
201
203
 
202
204
  ```bash
203
- dotmd new my-feature # default (status + title)
204
- dotmd new my-plan --template plan # plan with module, surface, refs
205
- dotmd new my-decision --template adr # ADR: Context, Decision, Consequences
206
- dotmd new my-proposal --template rfc # RFC: Summary, Motivation, Design
207
- dotmd new my-audit --template audit # Audit: Scope, Findings, Recommendations
208
- dotmd new my-design --template design # Design: Goals, Non-Goals, Design
209
- dotmd new my-feature --status planned --title "Title" # custom status and title
210
- dotmd new my-doc --root modules # create in a specific root
211
- dotmd new --list-templates # show all available templates
205
+ dotmd new plan auth-revamp # type: plan docs/plans/auth-revamp.md
206
+ dotmd new doc token-refresh-design # type: doc docs/token-refresh-design.md
207
+ dotmd new my-feature # implicit type: doc
208
+ dotmd new plan auth --status planned # initial status override
209
+ dotmd new doc my-doc --title "Custom Title" # title override
210
+ dotmd new doc my-doc --root modules # create in a specific root
211
+ dotmd new --list-types # show registered types
212
212
  ```
213
213
 
214
- Built-in templates: `default`, `plan`, `adr`, `rfc`, `audit`, `design`. Add custom templates in your config:
214
+ Each built-in type has a template baked in:
215
+
216
+ | Type | Default destination | Shape |
217
+ |------|---------------------|-------|
218
+ | `plan` | `docs/plans/<slug>.md` | Problem → Phases → Closeout, with phase status markers and Version History |
219
+ | `doc` | `docs/<slug>.md` | Overview → Version History → Related (build-up shape lite) |
220
+ | `prompt` | `docs/prompts/<slug>.md` | Body is required (see [Saved Prompts](#saved-prompts)) |
221
+
222
+ Add custom types via `templates` in your config:
215
223
 
216
224
  ```js
217
225
  export const templates = {
218
226
  spike: {
219
227
  description: 'Timeboxed investigation',
220
- frontmatter: (status, today) => `status: ${status}\nupdated: ${today}\ntimebox: 2d`,
228
+ defaultStatus: 'active',
229
+ targetRoot: 'spikes', // in flat-array root configs, lands in the matching root
230
+ dir: 'spikes', // in single-root configs, creates docs/spikes/<slug>.md
231
+ frontmatter: (status, today) => `type: spike\nstatus: ${status}\nupdated: ${today}\ntimebox: 2d`,
221
232
  body: (title) => `\n# ${title}\n\n## Hypothesis\n\n\n\n## Findings\n\n\n`,
222
233
  },
223
234
  };
224
235
  ```
225
236
 
237
+ Then `dotmd new spike my-spike` creates a doc from your template.
238
+
239
+ **Routing your custom type to a directory.** Two knobs:
240
+
241
+ - `targetRoot: '<name>'` — name (basename or suffix) of a root entry. In configs with `root: ['docs/plans', 'docs/spikes', ...]` (flat-array layout), the new doc lands in the matching root.
242
+ - `dir: '<subdir>'` — subdirectory under `config.docsRoot`. Used as the fallback when `targetRoot` doesn't match anything (typical single-root layout).
243
+
244
+ Set both for portability. The `--root` CLI flag overrides both. **Overrides do not inherit builtin properties** — if you override `templates.prompt`, re-declare `targetRoot`, `dir`, `defaultStatus`, `requiresBody`, etc. that you want preserved.
245
+
246
+ ### Saved Prompts
247
+
248
+ Saved prompts are `.md` files with `type: prompt` that capture a request meant to seed a future Claude session — "look at the remaining lint warnings tomorrow," "resume the payments refactor," "draft the on-call runbook." The body is the prompt; the frontmatter tracks status.
249
+
250
+ ```bash
251
+ dotmd new prompt cleanup-tomorrow "look at remaining lint warnings"
252
+ dotmd new prompt resume-foo - <<'EOF'
253
+ multi-line
254
+ prompt body
255
+ EOF
256
+ dotmd new prompt from-file @/tmp/draft.md
257
+ ```
258
+
259
+ Manage them with the `prompts` command family:
260
+
261
+ ```bash
262
+ dotmd prompts # list pending prompts (default)
263
+ dotmd prompts list --all # all statuses
264
+ dotmd prompts next # show + claim the oldest pending prompt (prints body)
265
+ dotmd prompts use <file> # claim a specific prompt (prints body, flips to claimed)
266
+ dotmd prompts archive <file> # archive a prompt
267
+ dotmd prompts new <name> [body] # alias for `dotmd new prompt`
268
+ ```
269
+
270
+ `dotmd hud` surfaces pending prompts on session start (alongside held leases and queued handoffs), so a saved prompt acts as a self-addressed reminder: write it now, the next session sees it.
271
+
272
+ Statuses: `pending` (drafted, awaiting a session), `claimed` (consumed by a session), `archived`.
273
+
226
274
  ### Check & Fix
227
275
 
228
276
  ```bash
@@ -397,14 +445,18 @@ dotmd bulk archive docs/old-a.md docs/old-b.md # archive multiple
397
445
  dotmd bulk archive docs/old-*.md -n # preview
398
446
  ```
399
447
 
400
- ### Pickup & Finish
448
+ ### Pickup & Closeout
401
449
 
402
450
  ```bash
403
451
  dotmd pickup docs/plans/my-plan.md # set in-session + print body (or queued handoff)
404
- dotmd finish docs/plans/my-plan.md # set done + bump date
405
- dotmd finish docs/plans/my-plan.md active # back to active for more work
452
+ dotmd archive docs/plans/my-plan.md # fully shipped: archive + auto-release lease
453
+ dotmd release docs/plans/my-plan.md # need more work: release lease, flip to prior status
454
+ dotmd status docs/plans/my-plan.md partial # shipped + tail deferred (reference successors in body)
455
+ dotmd status docs/plans/my-plan.md awaiting # stuck on a human decision
406
456
  ```
407
457
 
458
+ `finish` is a legacy command that defaults to `status: done`, which is no longer in the default plan vocabulary as of 0.16. Use `archive` (fully shipped) or `release` + `status` (anything else). If you need it back, add `done` to `types.plan.statuses` in your config.
459
+
408
460
  ### Handoff (resume-prompts attached to plans)
409
461
 
410
462
  When you're stopping mid-work and the next session will need to pick up where
package/bin/dotmd.mjs CHANGED
@@ -571,12 +571,16 @@ Subcommands:
571
571
  list List pending prompts (default)
572
572
  next Consume the oldest pending prompt:
573
573
  print body to stdout, flip status to archived
574
- use <file> Consume a specific prompt (same as next, but
575
- targets <file> instead of picking oldest)
576
- archive <file> Archive a prompt without printing its body
574
+ use <file-or-slug> Consume a specific prompt (same as next, but
575
+ targets the named prompt instead of picking oldest)
576
+ archive <file-or-slug> Archive a prompt without printing its body
577
577
  new <slug> [body] Create a new prompt (alias for
578
578
  \`dotmd new prompt <slug> [body]\`)
579
579
 
580
+ \`<file-or-slug>\` accepts: an exact path (with or without .md), a bare
581
+ slug matching a prompt basename, or a unique substring of a prompt
582
+ path. Ambiguous substrings error with the candidate list.
583
+
580
584
  Default prompt statuses: pending, claimed, archived.
581
585
 
582
586
  Examples:
@@ -586,10 +590,11 @@ Examples:
586
590
  dotmd prompts --json # JSON output
587
591
 
588
592
  claude "$(dotmd prompts next)" # consume oldest pending + run claude
589
- claude "$(dotmd prompts use docs/prompts/foo.md)"
593
+ claude "$(dotmd prompts use resume-foo)" # by slug
594
+ claude "$(dotmd prompts use docs/prompts/foo.md)" # by path
590
595
 
591
596
  dotmd prompts next --dry-run # preview without consuming
592
- dotmd prompts archive docs/prompts/old.md
597
+ dotmd prompts archive old-thing
593
598
  dotmd prompts new my-prompt "Body text here"`,
594
599
 
595
600
  stale: `dotmd stale — list stale documents
@@ -14,7 +14,7 @@ export const archiveDir = 'archived';
14
14
  export const excludeDirs = ['evidence'];
15
15
 
16
16
  // Document types — each type has its own status vocabulary and context layout.
17
- // Defaults: plan, doc, research. Override to customize statuses per type.
17
+ // Defaults: plan, doc, prompt. Override to customize statuses per type, or add new types.
18
18
  //
19
19
  // Statuses can be defined as an array (names only) or as an object (rich form).
20
20
  // The object form co-locates all behavioral properties with each status,
@@ -66,6 +66,15 @@ export const excludeDirs = ['evidence'];
66
66
  // 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
67
67
  // },
68
68
  // },
69
+ // prompt: {
70
+ // // Saved prompts that seed future Claude sessions. `dotmd hud` surfaces
71
+ // // pending prompts on session start; `dotmd prompts next` claims the oldest.
72
+ // statuses: {
73
+ // 'pending': { context: 'expanded', staleDays: 30 },
74
+ // 'claimed': { context: 'counted', quiet: true },
75
+ // 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
76
+ // },
77
+ // },
69
78
  // };
70
79
 
71
80
  // ─── Array form (also supported) ────────────────────────────────────────────
@@ -166,6 +175,50 @@ export const presets = {
166
175
  mine: ['--owner', 'robert', '--status', 'active', '--all'],
167
176
  };
168
177
 
178
+ // ─── Templates ───────────────────────────────────────────────────────────────
179
+ // Define new types or override builtins. `dotmd new <type> <name>` looks here first.
180
+ //
181
+ // Properties:
182
+ // description: string — shown in `dotmd new --list-types`
183
+ // defaultStatus: string — initial status if `--status` not passed
184
+ // requiresBody: boolean — error if no body input (see `prompt` builtin)
185
+ // targetRoot: string — name (basename or suffix) of the root this type lives in.
186
+ // In flat-array `root` configs (e.g. ['docs/plans', 'docs/prompts']),
187
+ // the new doc lands in the matching root. Falls back to `config.docsRoot`
188
+ // if no root matches. The `--root` CLI flag still overrides.
189
+ // dir: string — subdirectory under `docsRoot` to use when `targetRoot` doesn't
190
+ // match anything. Builtin `plan` and `prompt` set both for portability:
191
+ // under `docsRoot='docs'`, `dir` puts files in `docs/plans/` and `docs/prompts/`;
192
+ // under flat-array roots, `targetRoot` routes directly to the type-specific root.
193
+ // frontmatter: (status, isoTime, ctx) => string
194
+ // body: (title, ctx) => string
195
+ //
196
+ // Custom type example — adds a `spike` type that lives in the `spikes` root (or
197
+ // under `docs/spikes/` in single-root layouts):
198
+ // export const templates = {
199
+ // spike: {
200
+ // description: 'Timeboxed investigation',
201
+ // defaultStatus: 'active',
202
+ // targetRoot: 'spikes', // for flat-array root configs
203
+ // dir: 'spikes', // for single-root configs
204
+ // frontmatter: (s, d) => `type: spike\nstatus: ${s}\ncreated: ${d}\ntimebox: 2d`,
205
+ // body: (t) => `\n# ${t}\n\n## Hypothesis\n\n\n\n## Findings\n\n`,
206
+ // },
207
+ //
208
+ // // Override a builtin (e.g. project-specific prompt frontmatter shape).
209
+ // // IMPORTANT: builtin properties are NOT inherited — re-declare `targetRoot`, `dir`,
210
+ // // `defaultStatus`, `requiresBody`, etc. that you want preserved.
211
+ // // prompt: {
212
+ // // description: 'Project resume prompt',
213
+ // // defaultStatus: 'pending',
214
+ // // requiresBody: true,
215
+ // // targetRoot: 'prompts',
216
+ // // dir: 'prompts',
217
+ // // frontmatter: (s, d, ctx) => `type: prompt\nstatus: ${s}\nproject_field: yes`,
218
+ // // body: (t, ctx) => `\n${ctx?.bodyInput ?? ''}\n`,
219
+ // // },
220
+ // };
221
+
169
222
  // ─── Notion ──────────────────────────────────────────────────────────────────
170
223
  // IMPORTANT: Use environment variables for tokens — never hardcode secrets in config files.
171
224
  // export const notion = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
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/init.mjs CHANGED
@@ -169,7 +169,9 @@ export function runInit(cwd, config) {
169
169
  }
170
170
  }
171
171
 
172
- process.stdout.write(`\nReady. Create your first doc:\n`);
173
- process.stdout.write(` dotmd new my-doc\n`);
174
- process.stdout.write(` dotmd list\n\n`);
172
+ process.stdout.write(`\nReady. A few starting points:\n`);
173
+ process.stdout.write(` dotmd new doc my-doc # scaffold a reference doc\n`);
174
+ process.stdout.write(` dotmd new plan my-plan # scaffold an execution plan\n`);
175
+ process.stdout.write(` dotmd list # see what you've got\n`);
176
+ process.stdout.write(` dotmd hud # session-start triage (ideal SessionStart hook)\n\n`);
175
177
  }
package/src/new.mjs CHANGED
@@ -44,6 +44,7 @@ const BUILTIN_TEMPLATES = {
44
44
  plan: {
45
45
  description: 'Execution plan — build-up shape (Problem → Phases → Closeout) with phase status markers and Version History',
46
46
  dir: 'plans',
47
+ targetRoot: 'plans',
47
48
  defaultStatus: 'active',
48
49
  frontmatter: (s, d) => [
49
50
  'type: plan',
@@ -124,6 +125,7 @@ Status markers (put in heading text):
124
125
  prompt: {
125
126
  description: 'Saved prompt to seed a future Claude session — body is required',
126
127
  dir: 'prompts',
128
+ targetRoot: 'prompts',
127
129
  defaultStatus: 'pending',
128
130
  requiresBody: true,
129
131
  frontmatter: (s, d, ctx) => [
@@ -244,8 +246,11 @@ export async function runNew(argv, config, opts = {}) {
244
246
  // Title
245
247
  const docTitle = title ?? namePart.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
246
248
 
247
- // Resolve target root
249
+ // Resolve target root. Precedence: CLI --root > template.targetRoot > config.docsRoot.
250
+ // When the chosen root is a first-class type-container (matched by --root or targetRoot),
251
+ // we skip the `template.dir` join — the root already points at the right directory.
248
252
  let targetRoot = config.docsRoot;
253
+ let routedToTypeRoot = false;
249
254
  if (rootName) {
250
255
  const roots = config.docsRoots || [config.docsRoot];
251
256
  const match = roots.find(r => r.endsWith(rootName) || path.basename(r) === rootName);
@@ -254,10 +259,19 @@ export async function runNew(argv, config, opts = {}) {
254
259
  die(`Unknown root: ${rootName}\nAvailable: ${available}`);
255
260
  }
256
261
  targetRoot = match;
262
+ routedToTypeRoot = true;
263
+ } else if (typeof template === 'object' && template.targetRoot) {
264
+ const roots = config.docsRoots || [config.docsRoot];
265
+ const match = roots.find(r => r.endsWith(template.targetRoot) || path.basename(r) === template.targetRoot);
266
+ if (match) {
267
+ targetRoot = match;
268
+ routedToTypeRoot = true;
269
+ }
257
270
  }
258
271
 
259
- // Template-declared subdirectory (e.g., prompt → 'prompts')
260
- if (typeof template === 'object' && template.dir && !nameDir) {
272
+ // Template-declared subdirectory (e.g., prompt → 'prompts') — only relevant when we
273
+ // didn't already land in a type-specific root via --root or targetRoot.
274
+ if (typeof template === 'object' && template.dir && !nameDir && !routedToTypeRoot) {
261
275
  nameDir = path.join(path.relative(config.repoRoot, targetRoot), template.dir);
262
276
  }
263
277
 
package/src/prompts.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
3
4
  import { asString, toRepoPath, die, resolveDocPath } from './util.mjs';
4
5
  import { buildIndex } from './index.mjs';
@@ -74,11 +75,46 @@ function runPromptsNext(argv, config, opts = {}) {
74
75
  consumePrompt(head.abs, config, opts);
75
76
  }
76
77
 
78
+ // Resolve user input to a prompt path. Tries (in order): exact path,
79
+ // path + '.md', exact basename match across type: prompt docs, substring
80
+ // match across type: prompt docs. Returns the absolute path or dies with a
81
+ // helpful message (no match / ambiguous match).
82
+ function resolvePromptInput(input, config) {
83
+ const direct = resolveDocPath(input, config);
84
+ if (direct) return direct;
85
+
86
+ if (!input.endsWith('.md')) {
87
+ const withExt = resolveDocPath(input + '.md', config);
88
+ if (withExt) return withExt;
89
+ }
90
+
91
+ const index = buildIndex(config);
92
+ const prompts = index.docs.filter(d => d.type === 'prompt');
93
+ if (prompts.length === 0) die(`No prompts in the index.`);
94
+
95
+ const slug = input.replace(/\.md$/, '');
96
+
97
+ const byBasename = prompts.filter(d => path.basename(d.path, '.md') === slug);
98
+ if (byBasename.length === 1) return path.resolve(config.repoRoot, byBasename[0].path);
99
+ if (byBasename.length > 1) {
100
+ die(`Multiple prompts match "${input}" by basename:\n${byBasename.map(d => ' ' + d.path).join('\n')}`);
101
+ }
102
+
103
+ const bySubstring = prompts.filter(d =>
104
+ d.path.includes(slug) || path.basename(d.path).includes(slug),
105
+ );
106
+ if (bySubstring.length === 1) return path.resolve(config.repoRoot, bySubstring[0].path);
107
+ if (bySubstring.length > 1) {
108
+ die(`Multiple prompts match "${input}":\n${bySubstring.map(d => ' ' + d.path).join('\n')}`);
109
+ }
110
+
111
+ die(`No prompt found matching: ${input}`);
112
+ }
113
+
77
114
  function runPromptsUse(argv, config, opts = {}) {
78
115
  const input = argv.find(a => !a.startsWith('-'));
79
- if (!input) die('Usage: dotmd prompts use <file>');
80
- const filePath = resolveDocPath(input, config);
81
- if (!filePath) die(`File not found: ${input}`);
116
+ if (!input) die('Usage: dotmd prompts use <file-or-slug>');
117
+ const filePath = resolvePromptInput(input, config);
82
118
  consumePrompt(filePath, config, opts);
83
119
  }
84
120
 
@@ -113,9 +149,8 @@ function consumePrompt(filePath, config, opts) {
113
149
 
114
150
  function runPromptsArchive(argv, config, opts = {}) {
115
151
  const input = argv.find(a => !a.startsWith('-'));
116
- if (!input) die('Usage: dotmd prompts archive <file>');
117
- const filePath = resolveDocPath(input, config);
118
- if (!filePath) die(`File not found: ${input}`);
152
+ if (!input) die('Usage: dotmd prompts archive <file-or-slug>');
153
+ const filePath = resolvePromptInput(input, config);
119
154
 
120
155
  const raw = readFileSync(filePath, 'utf8');
121
156
  const { frontmatter } = extractFrontmatter(raw);