dotmd-cli 0.39.3 → 0.39.4

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
@@ -68,6 +68,7 @@ Create & Export:
68
68
  Setup:
69
69
  init Create starter config + docs directory
70
70
  statuses [list|add|set|remove|migrate] Manage per-project status taxonomy
71
+ help statuses Full status vocabulary + unstuck-actions + transitions
71
72
  watch [command] Re-run a command on file changes
72
73
  completions <shell> Shell completion script (bash, zsh)
73
74
  journal [--tail N|--errors|--by-command|--session id|--since iso|--json]
@@ -92,6 +93,90 @@ Options:
92
93
 
93
94
  Outputs the complete document index as JSON to stdout.`,
94
95
 
96
+ // Help topic accessed via \`dotmd help statuses\` (not a command — see dispatch
97
+ // below). Single-source-of-truth for the built-in status vocabulary across all
98
+ // three doc types. User-defined types/statuses live in config; introspect them
99
+ // with \`dotmd statuses list\`.
100
+ 'help:statuses': `dotmd help statuses — status vocabulary, unstuck-actions, and transitions
101
+
102
+ Every document has a \`type:\` field; each type has its own valid statuses.
103
+ Status validation is type-aware (type > root > global). To inspect or edit
104
+ the status taxonomy in a specific project, use \`dotmd statuses list\`.
105
+
106
+ ────────────────────────────────────────────────────────────────────
107
+ plan statuses (each maps to a distinct unstuck-action)
108
+
109
+ in-session A Claude session is working on it now.
110
+ Don't pick up unless you own it (auto-reattaches) or pass
111
+ --takeover. Stale lease cleanup: \`dotmd release --stale\`.
112
+
113
+ active Ready to be picked up.
114
+ \`dotmd pickup <file>\` → in-session.
115
+
116
+ planned Queued for future work, not yet ready to execute.
117
+ Transition to active when ready to start.
118
+
119
+ blocked External arrival wait — monitor.
120
+ Hardware, vendor delivery, third-party rollout. Quiet
121
+ (skipStale) — you can't speed it up by nagging.
122
+
123
+ partial Shipped + deferred tail — spawn successor plans.
124
+ Plan body should reference the successor plan(s). Quiet.
125
+
126
+ paused Started but stopped mid-work — re-evaluate to resume.
127
+ Short stale window (3 days) so resume-decisions don't decay.
128
+
129
+ awaiting Needs human input/decision — chase the answer.
130
+ NOT quiet — generates stale pressure so pings aren't forgotten.
131
+
132
+ queued-after Sequenced behind another plan — check predecessor.
133
+ Quiet. Can start once the predecessor ships.
134
+
135
+ archived No longer relevant; auto-moved to archive directory.
136
+
137
+ Canonical transitions:
138
+ active → in-session \`dotmd pickup <file>\`
139
+ in-session → active \`dotmd release <file>\`
140
+ in-session → partial \`dotmd status <file> partial\` (+ release)
141
+ in-session → awaiting \`dotmd status <file> awaiting\` (+ release)
142
+ any → archived \`dotmd archive <file>\`
143
+
144
+ ────────────────────────────────────────────────────────────────────
145
+ doc statuses
146
+
147
+ draft Work-in-progress reference doc.
148
+ active Living document, kept up-to-date.
149
+ review Awaiting peer review.
150
+ reference Stable canonical reference (excluded from stale checks).
151
+ deprecated Superseded but kept for history.
152
+ archived No longer relevant; moved to archive directory.
153
+
154
+ ────────────────────────────────────────────────────────────────────
155
+ prompt statuses
156
+
157
+ pending Ready for the next session to consume.
158
+ \`dotmd prompts use <file>\` prints body + archives atomically.
159
+ \`dotmd prompts next\` does the same for the oldest pending.
160
+
161
+ shelved Saved but hidden from \`hud\` / \`briefing\` / \`prompts next\`.
162
+ Still listed by \`dotmd prompts list\`.
163
+ \`dotmd prompts unshelve <file>\` → pending.
164
+
165
+ claimed Legacy intermediate state (atomic use → archived now).
166
+
167
+ archived Consumed prompt; body preserved in archive directory.
168
+
169
+ ────────────────────────────────────────────────────────────────────
170
+ Related commands:
171
+ dotmd statuses Inspect/manage per-project status taxonomy
172
+ dotmd status <f> <new> Transition a document's status
173
+ dotmd briefing See plans grouped by status
174
+ dotmd plans --status <s> Filter live plans by status
175
+ dotmd hud Two-line actionable triage (held / prompts / stuck)
176
+
177
+ Run \`dotmd statuses list --type plan\` to see the full set (including any
178
+ project-specific custom statuses) with their flags.`,
179
+
95
180
  completions: `dotmd completions <bash|zsh> — output shell completion script
96
181
 
97
182
  Add to your shell config:
@@ -241,6 +326,9 @@ Default plan statuses (each maps to a distinct unstuck-action):
241
326
  queued-after Sequenced behind another plan — check predecessor
242
327
  archived No longer relevant; auto-moved to archive directory
243
328
 
329
+ Run \`dotmd help statuses\` for the full vocabulary across all doc types
330
+ (plan, doc, prompt) plus canonical transitions and related commands.
331
+
244
332
  Use --dry-run (-n) to preview changes without writing anything.`,
245
333
 
246
334
  check: `dotmd check — validate frontmatter and references
@@ -630,8 +718,8 @@ sorted by status. Supports all query flags (--status, --module, --json,
630
718
  --sort, --group, etc.).
631
719
 
632
720
  Default plan statuses: in-session, active, planned, blocked, partial,
633
- paused, awaiting, queued-after, archived. Run \`dotmd status --help\` for
634
- the unstuck-action behind each one.
721
+ paused, awaiting, queued-after, archived. Run \`dotmd help statuses\` for
722
+ the unstuck-action behind each one and canonical transitions.
635
723
 
636
724
  Examples:
637
725
  dotmd plans # live plans (default)
@@ -671,6 +759,9 @@ Default prompt statuses: pending, shelved, claimed, archived.
671
759
 
672
760
  Examples:
673
761
  dotmd prompts # pending prompts (default)
762
+ dotmd prompts list --verbose # one row per prompt + target plan ref
763
+ # (from related_plans, parent_plan,
764
+ # or the first body .md link)
674
765
  dotmd prompts list --include-archived # all prompts including archived
675
766
  dotmd prompts list --status claimed # already-consumed prompts
676
767
  dotmd prompts --json # JSON output
@@ -825,6 +916,14 @@ async function main() {
825
916
  }
826
917
 
827
918
  if (command === 'help' || command === '--help' || command === '-h') {
919
+ const topic = args[1];
920
+ if (topic) {
921
+ const key = `help:${topic}`;
922
+ if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
923
+ if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
924
+ process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: statuses\nPer-command help: dotmd <cmd> --help\n`);
925
+ process.exit(1);
926
+ }
828
927
  process.stdout.write(`${HELP._main}\n`);
829
928
  return;
830
929
  }
@@ -927,7 +1026,7 @@ async function main() {
927
1026
  }
928
1027
  if (command === 'prompts') {
929
1028
  const { runPrompts } = await import('../src/prompts.mjs');
930
- await runPrompts(restArgs, config, { dryRun });
1029
+ await runPrompts(restArgs, config, { dryRun, verbose });
931
1030
  return;
932
1031
  }
933
1032
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.39.3",
3
+ "version": "0.39.4",
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/lifecycle.mjs CHANGED
@@ -246,7 +246,15 @@ export async function runPickup(argv, config, opts = {}) {
246
246
  }
247
247
 
248
248
  const pickupable = new Set(['active', 'planned', 'in-session']);
249
- if (oldStatus && !pickupable.has(oldStatus)) die(`Cannot pick up a plan with status '${oldStatus}'. Must be active or planned.\n ${repoPath}`);
249
+ if (oldStatus && !pickupable.has(oldStatus)) {
250
+ die(
251
+ `Cannot pick up a plan with status '${oldStatus}'. Must be active or planned.\n` +
252
+ ` ${repoPath}\n` +
253
+ `\n` +
254
+ `Recover with:\n` +
255
+ ` dotmd status ${repoPath} active && dotmd pickup ${repoPath}`,
256
+ );
257
+ }
250
258
 
251
259
  const today = nowIso();
252
260
  const leaseOldStatus = oldStatus === 'in-session' ? 'active' : (oldStatus ?? 'active');
@@ -572,6 +580,16 @@ export function runArchive(argv, config, opts = {}) {
572
580
  if (refCount > 0) {
573
581
  out.write(`${prefix} Would update references in ${refCount} file(s)\n`);
574
582
  }
583
+
584
+ // Preview lease release (only if a lease exists for this plan)
585
+ if (readLeases(config)[oldRepoPath]) {
586
+ out.write(`${prefix} Would release in-session lease: ${oldRepoPath}\n`);
587
+ }
588
+
589
+ // Preview onArchive hook fire
590
+ if (config.hooks?.onArchive) {
591
+ out.write(`${prefix} Would fire hook: onArchive\n`);
592
+ }
575
593
  return;
576
594
  }
577
595
 
package/src/prompts.mjs CHANGED
@@ -29,12 +29,17 @@ export async function runPrompts(argv, config, opts = {}) {
29
29
  }
30
30
  }
31
31
 
32
- function runPromptsList(argv, config) {
32
+ function runPromptsList(argv, config, opts = {}) {
33
33
  const index = buildIndex(config);
34
34
  const hasStatusFlag = argv.includes('--status');
35
35
  const includeArchived = argv.includes('--include-archived');
36
36
  const sub = argv[0];
37
37
 
38
+ if (opts.verbose && !argv.includes('--json')) {
39
+ renderPromptsVerbose(index, config, { hasStatusFlag, includeArchived });
40
+ return;
41
+ }
42
+
38
43
  let defaults;
39
44
  let extras = argv;
40
45
  if (sub === 'status') {
@@ -48,6 +53,67 @@ function runPromptsList(argv, config) {
48
53
  runQuery(index, [...defaults, ...extras], config, { preset: 'prompts' });
49
54
  }
50
55
 
56
+ // Resolve a prompt's "target plan" for `prompts list --verbose`. Order:
57
+ // 1. frontmatter `related_plans:` (first entry — assumed plan slug)
58
+ // 2. frontmatter `parent_plan:`
59
+ // 3. first body markdown link to a .md file
60
+ // Returns a repo-relative display path or null.
61
+ function findPromptTarget(promptDoc, config) {
62
+ const refs = promptDoc.refFields ?? {};
63
+ const fmTargets = [...(refs.related_plans ?? []), ...(refs.parent_plan ?? [])];
64
+ for (const t of fmTargets) {
65
+ if (typeof t === 'string' && t.trim()) return slugToPlanPath(t.trim(), config);
66
+ }
67
+
68
+ const links = promptDoc.bodyLinks ?? [];
69
+ const mdLink = links.find(l => /\.md(?:#|$)/.test(l.href ?? ''));
70
+ if (mdLink) return resolveBodyLink(mdLink.href, promptDoc.path);
71
+ return null;
72
+ }
73
+
74
+ // Plan slugs in frontmatter (e.g. `related_plans: [foo-bar]`) resolve to
75
+ // <docs-root>/plans/<slug>.md.
76
+ function slugToPlanPath(s, config) {
77
+ const cleaned = s.replace(/#.*$/, '').replace(/^\.\//, '');
78
+ if (cleaned.includes('/') || cleaned.endsWith('.md')) return cleaned;
79
+ return `${config.docsRootPrefix || 'docs/'}plans/${cleaned}.md`;
80
+ }
81
+
82
+ // Resolve a markdown body link relative to the prompt's location so e.g.
83
+ // `../plans/foo.md` from docs/prompts/x.md → docs/plans/foo.md.
84
+ function resolveBodyLink(link, promptRepoPath) {
85
+ const cleaned = link.replace(/#.*$/, '');
86
+ if (cleaned.startsWith('/')) return cleaned.replace(/^\/+/, '');
87
+ const promptDir = path.dirname(promptRepoPath);
88
+ return path.normalize(path.join(promptDir, cleaned));
89
+ }
90
+
91
+ function renderPromptsVerbose(index, config, { hasStatusFlag, includeArchived }) {
92
+ let prompts = index.docs.filter(d => d.type === 'prompt');
93
+ if (!hasStatusFlag && !includeArchived) {
94
+ prompts = prompts.filter(d => d.status !== 'archived');
95
+ }
96
+ if (prompts.length === 0) {
97
+ process.stdout.write('No prompts.\n');
98
+ return;
99
+ }
100
+
101
+ prompts.sort((a, b) => (b.updated ?? '').localeCompare(a.updated ?? ''));
102
+
103
+ const counts = {};
104
+ for (const p of prompts) counts[p.status ?? 'unknown'] = (counts[p.status ?? 'unknown'] ?? 0) + 1;
105
+ const summary = Object.entries(counts).map(([s, n]) => `${n} ${s}`).join(' · ');
106
+ process.stdout.write(`${prompts.length} prompt${prompts.length === 1 ? '' : 's'} · ${summary}\n\n`);
107
+
108
+ for (const p of prompts) {
109
+ const slug = path.basename(p.path, '.md');
110
+ const target = findPromptTarget(p, config);
111
+ const status = (p.status ?? 'unknown').toUpperCase();
112
+ const arrow = target ? ` ${dim('→')} ${target}` : ` ${dim('→ (no target plan)')}`;
113
+ process.stdout.write(` ${green(slug)} [${status}]\n${arrow}\n`);
114
+ }
115
+ }
116
+
51
117
  function pendingPromptsOldestFirst(config) {
52
118
  const index = buildIndex(config);
53
119
  const prompts = index.docs.filter(d => d.type === 'prompt' && d.status === 'pending');
@@ -139,7 +205,15 @@ function consumePrompt(filePath, config, opts) {
139
205
  }
140
206
 
141
207
  if (dryRun) {
142
- process.stderr.write(`${dim('[dry-run]')} Would emit body and archive: ${repoPath} (${status ?? 'unknown'} → archived)\n`);
208
+ const prefix = dim('[dry-run]');
209
+ process.stderr.write(`${prefix} Would emit body and archive: ${repoPath} (${status ?? 'unknown'} → archived)\n`);
210
+ const bytes = Buffer.byteLength(body, 'utf8');
211
+ const lines = body.split('\n').length;
212
+ process.stderr.write(`${prefix} body preview (${bytes}B, ${lines} lines):\n`);
213
+ process.stderr.write(`${dim('---8<---')}\n`);
214
+ process.stderr.write(body);
215
+ if (!body.endsWith('\n')) process.stderr.write('\n');
216
+ process.stderr.write(`${dim('--->8---')}\n`);
143
217
  runArchive([filePath], config, { dryRun: true, noIndex, out: process.stderr });
144
218
  return;
145
219
  }