dotmd-cli 0.45.2 → 0.46.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/bin/dotmd.mjs CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { resolveConfig } from '../src/config.mjs';
7
7
  import { die, warn, levenshtein } from '../src/util.mjs';
8
8
  import { recordCliInvocation } from '../src/journal.mjs';
9
+ import { findRepeatFailureHint } from '../src/hints.mjs';
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -1556,7 +1557,15 @@ function _journalExit(err) {
1556
1557
  main()
1557
1558
  .then(() => { _journalExit(null); })
1558
1559
  .catch(err => {
1559
- process.stderr.write(`${err.message}\n`);
1560
+ let out = err.message;
1561
+ // F17c: append a repeat-failure tip when the journal shows this same shape
1562
+ // has already failed in this session within the lookup window. Lookup is
1563
+ // a no-op when the journal is disabled or DOTMD_NO_HINTS=1.
1564
+ try {
1565
+ const hint = findRepeatFailureHint(_invocationArgs, _resolvedConfig);
1566
+ if (hint) out = `${out}\n\nTip: ${hint}`;
1567
+ } catch { /* hint must never break error reporting */ }
1568
+ process.stderr.write(`${out}\n`);
1560
1569
  process.exitCode = 1;
1561
1570
  _journalExit(err);
1562
1571
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.45.2",
3
+ "version": "0.46.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",
@@ -10,7 +10,7 @@ import { bold, green, dim } from './color.mjs';
10
10
  // the warning on the next few-word touch-up.
11
11
  const FIELDS = [
12
12
  { name: 'current_state', cap: 1500, target: 1200, heading: '## Current State' },
13
- { name: 'next_step', cap: 300, target: 200, heading: '## Next Step' },
13
+ { name: 'next_step', cap: 800, target: 600, heading: '## Next Step' },
14
14
  ];
15
15
 
16
16
  export function runFrontmatterFix(config, opts = {}) {
package/src/hints.mjs ADDED
@@ -0,0 +1,133 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readJournalEntries, isJournalEnabled, journalFilePath } from './journal.mjs';
3
+ import { currentSessionId } from './lease.mjs';
4
+
5
+ // F17c: repeat-failure hints. When an agent runs the same broken invocation
6
+ // twice in the same session within HINT_WINDOW_MS, the second die() output is
7
+ // suffixed with a Tip: paragraph informed by the prior failure's recorded
8
+ // stderr. First failures stay terse — don't punish humans typing a command
9
+ // for the first time. The lookup is skipped cleanly when the journal is
10
+ // disabled or DOTMD_NO_HINTS=1, so this costs nothing for non-opt-in users.
11
+
12
+ const HINT_WINDOW_MS = 10 * 60 * 1000;
13
+ const OVERLAP_THRESHOLD = 0.75;
14
+
15
+ const TEMPLATES = [
16
+ {
17
+ match: /Too many arguments|Usage:/i,
18
+ hint: ({ count, argv }) =>
19
+ `${count}× the same shape on \`${argv[0]} ${argv[1] ?? ''}\` in this session. Run \`dotmd ${argv[0]} --help\` for the expected positional args.`,
20
+ },
21
+ {
22
+ match: /Already (consumed|archived)/i,
23
+ hint: ({ count, argv }) =>
24
+ `${count}× attempts to use a path that is already archived. Use \`dotmd prompts list\` to see what is actually pending, or \`dotmd next\` for the oldest live prompt.`,
25
+ },
26
+ {
27
+ match: /No pending prompts/i,
28
+ hint: ({ count }) =>
29
+ `${count}× \`dotmd next\` with no pending prompts in the queue. Either queue one with \`dotmd new prompt <slug> "..."\` or pass an explicit prompt file to \`dotmd use\`.`,
30
+ },
31
+ {
32
+ match: /Unknown command/i,
33
+ hint: ({ count }) =>
34
+ `${count}× the same unknown command. Run \`dotmd --help\` to list available commands; the dispatch already prints a did-you-mean for close misses.`,
35
+ },
36
+ {
37
+ match: /File not found|does not resolve/i,
38
+ hint: ({ count, argv }) =>
39
+ `${count}× pointing at a path that doesn't exist. Confirm the file with \`dotmd query\` or \`dotmd plans\` — paths resolve relative to repo root or doc roots, not the cwd.`,
40
+ },
41
+ {
42
+ match: /Lease conflict|in-session|held by/i,
43
+ hint: ({ count }) =>
44
+ `${count}× lease conflict in this session. Run \`dotmd plans --status in-session\` to see what's held; pass \`--takeover\` if the holder is stale, or close the other session first.`,
45
+ },
46
+ {
47
+ match: /Unknown status|Unknown surface/i,
48
+ hint: ({ count }) =>
49
+ `${count}× rejected by the taxonomy validator. Run \`dotmd statuses list\` or \`dotmd surfaces\` to print the valid values for this project.`,
50
+ },
51
+ ];
52
+
53
+ // Global value-consuming flags must be skipped together with the token that
54
+ // follows them — otherwise `--config /tmp/foo` injects the tempdir path as
55
+ // "positional" and dilutes Jaccard overlap below threshold. Keep this list
56
+ // aligned with the global flag-strip list in bin/dotmd.mjs (SCRUB_*).
57
+ const VALUE_FLAGS = new Set([
58
+ '--config', '--root', '--type', '--limit', '--sort', '--group',
59
+ '--status', '--owner', '--module', '--surface', '--domain',
60
+ '--audience', '--execution-mode', '--updated-since',
61
+ '--summarize-limit', '--model',
62
+ ]);
63
+
64
+ function nonFlagSet(argv) {
65
+ const out = new Set();
66
+ for (let i = 0; i < argv.length; i++) {
67
+ const a = argv[i];
68
+ if (typeof a !== 'string') continue;
69
+ if (a.startsWith('-')) {
70
+ if (VALUE_FLAGS.has(a)) i++;
71
+ continue;
72
+ }
73
+ out.add(a);
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function jaccard(a, b) {
79
+ if (a.size === 0 && b.size === 0) return 1;
80
+ const aArr = [...a];
81
+ const inter = aArr.filter(x => b.has(x)).length;
82
+ const union = new Set([...a, ...b]).size;
83
+ return union === 0 ? 0 : inter / union;
84
+ }
85
+
86
+ // Returns a hint string (no `Tip:` prefix) or null. The caller is responsible
87
+ // for formatting. Failures inside this function are swallowed — a malformed
88
+ // journal must never break the error-reporting path.
89
+ export function findRepeatFailureHint(failingArgv, config) {
90
+ try {
91
+ if (process.env.DOTMD_NO_HINTS === '1') return null;
92
+ if (!config) return null;
93
+ if (!isJournalEnabled(config)) return null;
94
+ if (!existsSync(journalFilePath(config))) return null;
95
+ if (!Array.isArray(failingArgv) || failingArgv.length === 0) return null;
96
+
97
+ const sid = currentSessionId();
98
+ const now = Date.now();
99
+ const cutoff = now - HINT_WINDOW_MS;
100
+ const failingShape = nonFlagSet(failingArgv);
101
+
102
+ const entries = readJournalEntries(config);
103
+ const matches = [];
104
+ for (const entry of entries) {
105
+ if (!entry || entry.sid !== sid) continue;
106
+ if (!Array.isArray(entry.argv) || entry.argv.length === 0) continue;
107
+ if (entry.argv[0] !== failingArgv[0]) continue;
108
+ if ((entry.exit ?? 0) === 0) continue;
109
+ const ts = new Date(entry.ts).getTime();
110
+ if (Number.isNaN(ts) || ts < cutoff) continue;
111
+ const priorShape = nonFlagSet(entry.argv);
112
+ if (jaccard(failingShape, priorShape) < OVERLAP_THRESHOLD) continue;
113
+ matches.push({ entry, ts });
114
+ }
115
+
116
+ if (matches.length === 0) return null;
117
+ matches.sort((a, b) => b.ts - a.ts);
118
+ const last = matches[0].entry;
119
+ const count = matches.length + 1;
120
+ const prevErr = last.err ?? '';
121
+ const ageMin = Math.max(1, Math.round((now - matches[0].ts) / 60000));
122
+
123
+ for (const tmpl of TEMPLATES) {
124
+ if (tmpl.match.test(prevErr)) {
125
+ return tmpl.hint({ count, argv: failingArgv, prev: last, ageMin });
126
+ }
127
+ }
128
+
129
+ return `${count}× the same failing shape on \`${failingArgv[0]}\` in this session (last attempt ${ageMin}m ago). Check the args — \`dotmd ${failingArgv[0]} --help\` shows what's expected.`;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
package/src/validate.mjs CHANGED
@@ -112,9 +112,18 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
112
112
  }
113
113
 
114
114
  if (config.validSurfaces && !config.lifecycle.skipWarningsFor.has(doc.status)) {
115
+ const knownSurfaces = [...config.validSurfaces];
115
116
  for (const surface of doc.surfaces) {
116
117
  if (!config.validSurfaces.has(surface)) {
117
- doc.warnings.push({ path: doc.path, level: 'warning', message: `Unknown surface \`${surface}\`; expected a known surface taxonomy value.` });
118
+ const suggestions = suggestCandidates(surface, knownSurfaces, 3);
119
+ const hint = suggestions.length
120
+ ? ` Did you mean: ${suggestions.map(s => `\`${s}\``).join(' | ')}?`
121
+ : '';
122
+ doc.warnings.push({
123
+ path: doc.path,
124
+ level: 'warning',
125
+ message: `Unknown surface \`${surface}\`; expected a known surface taxonomy value.${hint} Run \`dotmd surfaces\` to list all valid values.`,
126
+ });
118
127
  }
119
128
  }
120
129
  }
@@ -456,13 +465,17 @@ export function validatePlanShape(doc, body, frontmatter, config) {
456
465
  if (config.lifecycle.terminalStatuses.has(doc.status) || config.lifecycle.archiveStatuses.has(doc.status)) return;
457
466
  if (config.lifecycle.skipWarningsFor.has(doc.status)) return;
458
467
 
459
- // 1. next_step length cap (300 chars)
468
+ // 1. next_step length cap (800 chars). Was 300; raised in parallel with
469
+ // current_state for the same reason: agents need to encode "what to do next"
470
+ // with enough specificity (which file, which decision, which branch) that
471
+ // 300 chars often forced truncation into the body where the briefing
472
+ // doesn't read it.
460
473
  const nextStep = typeof frontmatter.next_step === 'string' ? frontmatter.next_step : '';
461
- if (nextStep.length > 300) {
474
+ if (nextStep.length > 800) {
462
475
  doc.warnings.push({
463
476
  path: doc.path,
464
477
  level: 'warning',
465
- message: `\`next_step\` is ${nextStep.length} chars (cap: 300). Long prose belongs in the body — keep next_step as a 1-2 line pointer.`,
478
+ message: `\`next_step\` is ${nextStep.length} chars (cap: 800). Long prose belongs in the body — keep next_step as a 1-2 sentence pointer.`,
466
479
  });
467
480
  }
468
481