dotmd-cli 0.49.5 → 0.50.1

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
@@ -879,9 +879,15 @@ export const index = {
879
879
  path: 'docs/docs.md',
880
880
  startMarker: '<!-- GENERATED:dotmd:start -->',
881
881
  endMarker: '<!-- GENERATED:dotmd:end -->',
882
+ snapshot: 'status', // default; use 'state' to include live current_state text
882
883
  };
883
884
  ```
884
885
 
886
+ Generated indexes default to status-only rows for live sections so README files
887
+ do not become stale mirrors of volatile `current_state` text. Set
888
+ `snapshot: 'state'` if you want the older `Status Snapshot` table for live
889
+ sections too. Archived highlights still include their historical snapshots.
890
+
885
891
  All exports are optional. Additional options: `context`, `display`, `presets`, `templates`, `excludeDirs`, `notion`. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs) for the full reference.
886
892
 
887
893
  Config discovery walks up from cwd looking for `dotmd.config.mjs` or `.dotmd.config.mjs`.
@@ -144,6 +144,7 @@ export const index = {
144
144
  path: 'docs/docs.md',
145
145
  startMarker: '<!-- GENERATED:dotmd:start -->',
146
146
  endMarker: '<!-- GENERATED:dotmd:end -->',
147
+ snapshot: 'status', // default; use 'state' to include live current_state text
147
148
  archivedLimit: 8,
148
149
  };
149
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.49.5",
3
+ "version": "0.50.1",
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",
@@ -133,7 +133,7 @@ function generateDocsCommand(config, version) {
133
133
 
134
134
  lines.push('Commands for working with docs:');
135
135
  lines.push('- `dotmd context` — LLM-oriented briefing across all types');
136
- lines.push('- `dotmd doctor` — auto-fix everything in one pass (refs, lint, dates, index)');
136
+ lines.push('- `dotmd doctor --apply` — auto-fix everything in one pass (refs, lint, dates, index; bare `dotmd doctor` previews only)');
137
137
  lines.push('- `dotmd query [filters]` — search by status, keyword, module, surface, type, staleness');
138
138
  lines.push('- `dotmd health` — plan pipeline, velocity, aging');
139
139
  lines.push('- `dotmd stats` — doc health dashboard (completeness, checklists, audit coverage)');
package/src/config.mjs CHANGED
@@ -316,6 +316,10 @@ function validateConfig(userConfig, config, validStatuses, indexPath) {
316
316
  warnings.push(`Config: index path does not exist: ${indexPath}`);
317
317
  }
318
318
 
319
+ if (config.index?.snapshot !== undefined && !['status', 'state'].includes(config.index.snapshot)) {
320
+ warnings.push("Config: index.snapshot must be 'status' or 'state'.");
321
+ }
322
+
319
323
  // Unknown top-level user config keys
320
324
  for (const key of Object.keys(userConfig)) {
321
325
  if (!VALID_CONFIG_KEYS.has(key)) {
@@ -512,6 +516,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
512
516
  indexPath,
513
517
  indexStartMarker: config.index?.startMarker ?? '<!-- GENERATED:dotmd:start -->',
514
518
  indexEndMarker: config.index?.endMarker ?? '<!-- GENERATED:dotmd:end -->',
519
+ indexSnapshot: config.index?.snapshot ?? 'status',
515
520
  archivedHighlightLimit: config.index?.archivedLimit ?? 8,
516
521
 
517
522
  context: config.context,
package/src/hud.mjs CHANGED
@@ -4,21 +4,11 @@ import { readLeases, findStaleLeases, currentSessionId, isLeaseStale, STALE_LEAS
4
4
  import { scrubStaleSilently } from './lease-scrub.mjs';
5
5
  import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
6
6
  import { asString, toRepoPath } from './util.mjs';
7
- import { dim, yellow } from './color.mjs';
7
+ import { dim } from './color.mjs';
8
8
  import { buildIndex } from './index.mjs';
9
9
  import { refreshStaleSlashCommands } from './claude-commands.mjs';
10
10
  import { readJournalEntries, journalFilePath } from './journal.mjs';
11
11
 
12
- const MAX_PREVIEW = 5;
13
-
14
- function slug(repoPath) { return path.basename(repoPath, '.md'); }
15
-
16
- function previewList(items, max = MAX_PREVIEW) {
17
- const slugs = items.slice(0, max).map(slug);
18
- const more = items.length > max ? `, +${items.length - max} more` : '';
19
- return slugs.join(', ') + more;
20
- }
21
-
22
12
  // Statuses that count as "actionable" for a prompt are derived from config:
23
13
  // types.prompt.context.expanded (the statuses the user wants prominently shown).
24
14
  // Falls back to ['pending'] when no prompt type is configured (defensive default
@@ -223,11 +213,12 @@ export function runHud(argv, config) {
223
213
  const hud = buildHud(config);
224
214
 
225
215
  // Self-heal stale slash-command files. Wrapped: a broken scaffolder must
226
- // never kill the SessionStart hook (would block every session). Skipped in
227
- // --json mode to keep the structured shape stable for programmatic callers.
228
- let refreshed = [];
216
+ // never kill the SessionStart hook (would block every session). Runs for its
217
+ // side effect only the refresh is no longer announced in stdout (see the
218
+ // primer-only contract below). Skipped in --json mode to keep the structured
219
+ // shape stable for programmatic callers.
229
220
  if (!json) {
230
- try { refreshed = refreshStaleSlashCommands(config); }
221
+ try { refreshStaleSlashCommands(config); }
231
222
  catch { /* swallow — see comment above */ }
232
223
  }
233
224
 
@@ -236,58 +227,15 @@ export function runHud(argv, config) {
236
227
  return;
237
228
  }
238
229
 
239
- const lines = [];
240
-
241
- // Always-on command primer. Replaces the prior plan-state / prompts /
242
- // stuck-leases / validation-errors lines those signals belong inside
243
- // their own commands (`plans`, `prompts`), not in the SessionStart hook.
244
- // hud's job is purely to remind the agent which verbs exist, since that's
245
- // the one thing the agent reaches for `--help` to recover. Keep it tight:
246
- // one line, the minimum verb set.
247
- lines.push(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] oldest pending prompt)'));
248
-
249
- const state = [];
250
- if (hud.owned?.length) state.push(`held: ${hud.owned.length} (${previewList(hud.owned)})`);
251
- if (hud.prompts?.length) state.push(`prompts: ${hud.prompts.length} (${previewList(hud.prompts)})`);
252
- if (hud.stale?.length) state.push(`stuck: ${hud.stale.length} (${previewList(hud.stale)})`);
253
- if (hud.errors > 0) state.push(`errors: ${hud.errors} (run dotmd check)`);
254
- if (state.length) lines.push(yellow(state.join(' · ')));
255
-
256
- if (refreshed.length > 0) {
257
- const from = refreshed[0].from;
258
- const to = refreshed[0].to;
259
- const names = refreshed.map(r => r.name).join(', ');
260
- lines.push(dim(`↻ slash commands refreshed (v${from} → v${to}): ${names}`));
261
- }
262
-
263
- // F17b: three journal-aware sections. Silent-when-clean: each block emits
264
- // only when it has entries.
265
- if (hud.previousSelf?.length) {
266
- lines.push(dim('— previous self —'));
267
- for (const e of hud.previousSelf) {
268
- const cmd = (e.argv ?? []).join(' ');
269
- const exitTag = e.exit === 0 ? '' : `, exit ${e.exit}`;
270
- lines.push(dim(` ${cmd} (${e.ago}${exitTag})`));
271
- }
272
- }
273
-
274
- if (hud.fleet?.length) {
275
- lines.push(dim('— fleet (last 24h) —'));
276
- for (const f of hud.fleet) {
277
- const heldTag = f.holding?.length
278
- ? ` · holding ${f.holding.map(p => path.basename(p, '.md')).join(', ')}`
279
- : '';
280
- const staleTag = f.stale ? yellow(' [stale]') : '';
281
- lines.push(dim(` session ${f.sid} · ${f.cmds} cmds · last ${f.lastAgo}${heldTag}`) + staleTag);
282
- }
283
- }
284
-
285
- if (hud.recentRejections?.length) {
286
- lines.push(dim('— recent rejections (last 1h) —'));
287
- for (const r of hud.recentRejections) {
288
- lines.push(dim(` ${r.count}× "${r.cls}" on \`${r.cmd}\``));
289
- }
290
- }
291
-
292
- process.stdout.write(lines.join('\n') + '\n');
230
+ // SessionStart contract: emit ONLY the command primer — the verb cheat-sheet
231
+ // that tells the agent which dotmd verbs exist. Everything else hud used to
232
+ // print (held/prompts/stuck/errors state, slash-command refresh notices, and
233
+ // the journal-aware previous-self / fleet / recent-rejections sections) is
234
+ // deliberately suppressed here: those signals nudged agents into phantom
235
+ // follow-up work e.g. "errors: 1 (run dotmd check)" prompting a check run
236
+ // for state that belongs inside its own command. Each of those signals lives
237
+ // in its proper command (`plans`, `prompts`, `check`) and stays available via
238
+ // `dotmd hud --json` for programmatic callers. The hook's job is purely to
239
+ // teach the verbs, never to report status.
240
+ process.stdout.write(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> (use [no-arg] → oldest pending prompt)') + '\n');
293
241
  }
@@ -21,6 +21,7 @@ export function renderIndexFile(index, config) {
21
21
  function renderGeneratedBlock(index, config) {
22
22
  const lines = [];
23
23
  const indexDir = config.indexPath ? path.dirname(path.relative(config.repoRoot, config.indexPath)).split(path.sep).join('/') : '';
24
+ const snapshotMode = config.indexSnapshot ?? 'status';
24
25
 
25
26
  for (const status of config.statusOrder) {
26
27
  const docs = index.docs.filter(doc => doc.status === status);
@@ -34,10 +35,9 @@ function renderGeneratedBlock(index, config) {
34
35
 
35
36
  lines.push(`## ${capitalize(status)}`);
36
37
  lines.push('');
37
- lines.push('| Doc | Status Snapshot |');
38
- lines.push('|-----|-----------------|');
38
+ lines.push(...snapshotHeader(snapshotMode));
39
39
  for (const doc of docs) {
40
- const snapshot = formatSnapshot(doc, config);
40
+ const snapshot = renderIndexSnapshot(doc, config, snapshotMode);
41
41
  const linkPath = indexDir ? path.relative(indexDir, doc.path).split(path.sep).join('/') : doc.path;
42
42
  lines.push(`| [${escapeTable(doc.title)}](${linkPath}) | ${escapeTable(snapshot)} |`);
43
43
  }
@@ -76,6 +76,16 @@ function renderArchivedSection(docs, config, status) {
76
76
  return lines;
77
77
  }
78
78
 
79
+ function snapshotHeader(snapshotMode) {
80
+ if (snapshotMode === 'state') return ['| Doc | Status Snapshot |', '|-----|-----------------|'];
81
+ return ['| Doc | Status |', '|-----|--------|'];
82
+ }
83
+
84
+ function renderIndexSnapshot(doc, config, snapshotMode) {
85
+ if (snapshotMode === 'state') return formatSnapshot(doc, config);
86
+ return capitalize(doc.status ?? 'unknown');
87
+ }
88
+
79
89
  export function writeIndex(content, config) {
80
90
  writeFileSync(config.indexPath, content, 'utf8');
81
91
  }
package/src/init.mjs CHANGED
@@ -189,6 +189,7 @@ function generateDetectedConfig(scan, rootPath) {
189
189
  lines.push(` path: '${rootPath}/docs.md',`);
190
190
  lines.push(` startMarker: '<!-- GENERATED:dotmd:start -->',`);
191
191
  lines.push(` endMarker: '<!-- GENERATED:dotmd:end -->',`);
192
+ lines.push(` snapshot: 'status',`);
192
193
  lines.push('};');
193
194
  lines.push('');
194
195
 
package/src/render.mjs CHANGED
@@ -493,7 +493,7 @@ function _renderCheck(index, opts = {}) {
493
493
  }
494
494
  lines.push('');
495
495
  } else {
496
- lines.push(dim(`Run \`dotmd check --verbose\` for per-doc detail. \`dotmd doctor\` auto-fixes supported issues; remaining issues need the suggested manual command.`));
496
+ lines.push(dim(`Run \`dotmd check --verbose\` for per-doc detail. \`dotmd doctor --apply\` auto-fixes supported issues (bare \`dotmd doctor\` previews only); remaining issues need the suggested manual command.`));
497
497
  lines.push('');
498
498
  }
499
499
  }
package/src/validate.mjs CHANGED
@@ -416,7 +416,10 @@ export function checkBidirectionalReferences(docs, config) {
416
416
  // rendering), so divergence almost always means the agent forgot the back-link.
417
417
  // Warning fires on the CHILD (that's the file that needs the edit). Skips
418
418
  // terminal/archive statuses on either side — runlists referencing closed work
419
- // are a normal history pattern.
419
+ // are a normal history pattern. A `>`-prefixed (one-way) runlist entry opts
420
+ // that child out of the back-pointer requirement — the same per-ref escape
421
+ // hatch the bidirectional reciprocity check honors (A4) — so a hub can order a
422
+ // child it doesn't own without nagging the child to add `parent_plan:`.
420
423
  export function checkRunlistBackPointers(docs, config) {
421
424
  const warnings = [];
422
425
  const skipStatuses = new Set([
@@ -428,10 +431,13 @@ export function checkRunlistBackPointers(docs, config) {
428
431
  for (const hub of docs) {
429
432
  if (skipStatuses.has(hub.status)) continue;
430
433
  const runlistRefs = hub.refFields?.runlist ?? [];
434
+ const runlistDirs = hub.refFieldDirections?.runlist ?? [];
431
435
  if (runlistRefs.length === 0) continue;
432
436
  const hubDir = path.dirname(path.join(config.repoRoot, hub.path));
433
437
 
434
- for (const ref of runlistRefs) {
438
+ for (let i = 0; i < runlistRefs.length; i++) {
439
+ const ref = runlistRefs[i];
440
+ if (runlistDirs[i] === 'one-way') continue; // `> child.md` → ordered but no back-pointer expected
435
441
  const resolved = resolveRefPath(ref, hubDir, config.repoRoot);
436
442
  if (!resolved) continue; // unresolved refs already get their own existence error
437
443
  const childPath = toRepoPath(resolved, config.repoRoot);