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 +6 -0
- package/dotmd.config.example.mjs +1 -0
- package/package.json +1 -1
- package/src/claude-commands.mjs +1 -1
- package/src/config.mjs +5 -0
- package/src/hud.mjs +17 -69
- package/src/index-file.mjs +13 -3
- package/src/init.mjs +1 -0
- package/src/render.mjs +1 -1
- package/src/validate.mjs +8 -2
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`.
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -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
package/src/claude-commands.mjs
CHANGED
|
@@ -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
|
|
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).
|
|
227
|
-
//
|
|
228
|
-
|
|
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 {
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
}
|
package/src/index-file.mjs
CHANGED
|
@@ -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(
|
|
38
|
-
lines.push('|-----|-----------------|');
|
|
38
|
+
lines.push(...snapshotHeader(snapshotMode));
|
|
39
39
|
for (const doc of docs) {
|
|
40
|
-
const snapshot =
|
|
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 (
|
|
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);
|