dotmd-cli 0.39.0 → 0.39.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.39.0",
3
+ "version": "0.39.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",
@@ -20,7 +20,7 @@ const VERSION_REGEX = /<!-- dotmd-generated: ([\d.]+) -->/;
20
20
  const SLASH_DESCRIPTIONS = {
21
21
  plans: "dotmd-managed plan briefing for this repo. Use when the user asks what's on the plate, references a plan slug, queues work, or wants to pick up / release / archive a plan.",
22
22
  docs: "dotmd-managed docs briefing for this repo. Use when the user asks to list, scaffold, query, validate, archive, or rename non-plan docs (reference docs, ADRs, RFCs, design notes), or asks how the dotmd doc lifecycle works here.",
23
- baton: "Wrap the current session cleanly: update the in-flight plan, save ONE resume prompt via 'dotmd new prompt', release the lease. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
23
+ baton: "Save a resume prompt for the held plan and release the lease the minimum handoff. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
24
24
  };
25
25
 
26
26
  function frontmatterFor(name) {
@@ -67,13 +67,17 @@ function generatePlansCommand(config) {
67
67
 
68
68
  function generateBatonCommand() {
69
69
  const lines = [...frontmatterFor('baton'), VERSION_MARKER, ''];
70
- lines.push('You are wrapping this session. Hand the baton cleanly to the next one.');
70
+ lines.push('Wrap this session. Minimum required (two commands):');
71
71
  lines.push('');
72
- lines.push('1. **Update the in-flight plan.** Find it via `dotmd plans --status in-session`. Edit its `current_state:` / `next_step:` frontmatter so they reflect where things actually stand. If status should change (shipped → archive, stuck on a human decision awaiting, etc.), transition with `dotmd status <file> <status>`or `dotmd archive <file>` if work is done.');
72
+ lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` with a 10-20 line body via heredoc: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoffnever print it into chat for copy-paste.');
73
73
  lines.push('');
74
- lines.push('2. **Save ONE lean handoff prompt.** Run `dotmd new prompt resume-<plan-slug>` with a body of ~10-20 lines: point at the plan file, name the next concrete decision, flag any gotchas. Do NOT recap the plan body (the plan is for that). Do NOT print the handoff into chat for the user to copy-paste — the saved prompt is the handoff.');
74
+ lines.push('2. **Release the lease.** `dotmd release` or `dotmd archive <file>` if the work is fully shipped (archive auto-releases).');
75
75
  lines.push('');
76
- lines.push('3. **Release the lease.** `dotmd release` (skip if `dotmd archive` already closed out — archive auto-releases).');
76
+ lines.push('Optional, only when genuinely needed:');
77
+ lines.push('- Status really changed (paused / awaiting / partial / blocked): `dotmd status <file> <status>` BEFORE `dotmd release`.');
78
+ lines.push('- Plan dashboard text is misleading the user: edit `next_step:` in the plan frontmatter. Cosmetic — the next session reads the resume prompt, not the plan frontmatter.');
79
+ lines.push('');
80
+ lines.push('If you don\'t already know which plan you hold: `dotmd hud --json` and read `.owned`. Do NOT use `dotmd plans --status in-session` — that lists every session\'s holdings, not just yours.');
77
81
  lines.push('');
78
82
  lines.push('The next session\'s `dotmd hud` (SessionStart hook) surfaces the pending prompt automatically.');
79
83
  lines.push('');
package/src/new.mjs CHANGED
@@ -265,17 +265,37 @@ export async function runNew(argv, config, opts = {}) {
265
265
 
266
266
  // Fail-fast when the user passes body input to a template that doesn't
267
267
  // consume it — silently discarding heredoc content is the worst UX.
268
- // Templates opt in via `acceptsBody: true` or `requiresBody: true`. All
269
- // built-in templates (doc, plan, prompt) accept body; this guard fires
270
- // only for custom templates that opt out.
268
+ // Templates opt in via `acceptsBody: true` or `requiresBody: true`.
271
269
  if (bodyInput !== null && !template.acceptsBody && !template.requiresBody) {
272
- const accepting = Object.entries(BUILTIN_TEMPLATES)
273
- .filter(([, t]) => t.acceptsBody || t.requiresBody)
274
- .map(([n]) => n);
270
+ const configTemplates = config.raw?.templates ?? {};
271
+ // Compute the accepting list from the RESOLVED set (config merged over
272
+ // built-ins) so the hint doesn't contradict the rejection.
273
+ const resolvedNames = new Set([...Object.keys(BUILTIN_TEMPLATES), ...Object.keys(configTemplates)]);
274
+ const accepting = [...resolvedNames]
275
+ .filter(n => n !== typeName)
276
+ .filter(n => {
277
+ const t = resolveTemplate(n, config);
278
+ return t.acceptsBody || t.requiresBody;
279
+ });
275
280
  const hint = accepting.length > 0
276
281
  ? ` Templates that accept body input: ${accepting.join(', ')}.`
277
282
  : '';
278
- die(`\`${typeName}\` template does not accept body input, but body was passed via ${bodyInputSource}.${hint}\nEither drop the body, switch to a template that accepts it, or set \`acceptsBody: true\` on your custom \`${typeName}\` template in dotmd.config.mjs.`);
283
+
284
+ // Override-of-builtin diagnosis: the most common cause is a project
285
+ // dotmd.config.mjs that copy-pasted a stripped-down `plan` template
286
+ // and dropped the body-acceptance contract. Name that explicitly so
287
+ // an agent can self-fix without spelunking the config.
288
+ const builtin = BUILTIN_TEMPLATES[typeName];
289
+ const isOverride = Boolean(configTemplates[typeName] && builtin);
290
+ const builtinAccepts = Boolean(builtin && (builtin.acceptsBody || builtin.requiresBody));
291
+ let cause;
292
+ if (isOverride && builtinAccepts) {
293
+ const where = config.configPath ? toRepoPath(config.configPath, config.repoRoot) : 'dotmd.config.mjs';
294
+ cause = `Your config (${where}) overrides the built-in \`${typeName}\` template, and the override drops body acceptance.\nFix: in that override, add \`acceptsBody: true\` AND interpolate \`\${ctx?.bodyInput?.trim() ?? ''}\` into your \`body\` fn (e.g., inside \`## Problem\`). Or drop the override to use the built-in.`;
295
+ } else {
296
+ cause = `Either drop the body, switch to a template that accepts it, or set \`acceptsBody: true\` on your custom \`${typeName}\` template in dotmd.config.mjs.`;
297
+ }
298
+ die(`\`${typeName}\` template does not accept body input, but body was passed via ${bodyInputSource}.${hint}\n${cause}`);
279
299
  }
280
300
 
281
301
  // If name contains path separators, split into directory prefix and basename
@@ -379,10 +399,39 @@ export async function runNew(argv, config, opts = {}) {
379
399
  }
380
400
 
381
401
  function resolveTemplate(name, config) {
382
- // Config templates take priority
383
402
  const configTemplates = config.raw?.templates ?? {};
384
- if (configTemplates[name]) return configTemplates[name];
385
- if (BUILTIN_TEMPLATES[name]) return BUILTIN_TEMPLATES[name];
403
+ const override = configTemplates[name];
404
+ const builtin = BUILTIN_TEMPLATES[name];
405
+
406
+ if (override) {
407
+ if (!builtin) return { ...override, _overridesBuiltin: false };
408
+ // Partial-override DX: shallow-merge built-in under override so missing
409
+ // fields (description, dir, targetRoot, defaultStatus, frontmatter, body,
410
+ // acceptsBody, requiresBody) fall back to the built-in. Anything the
411
+ // override explicitly declares wins.
412
+ const merged = { ...builtin, ...override, _overridesBuiltin: true };
413
+
414
+ // Body-loss guard: if the override supplies its OWN body fn but doesn't
415
+ // explicitly opt in to body acceptance, the inherited built-in
416
+ // `acceptsBody`/`requiresBody` could let body input flow into a custom
417
+ // body fn that doesn't honor `ctx.bodyInput` — silently discarding the
418
+ // heredoc, the worst-UX bug fix #9 was added to prevent. Strip the
419
+ // inherited flags so the fail-fast guard fires. EXCEPT when the custom
420
+ // body fn references `bodyInput` itself, in which case it's clearly
421
+ // body-aware and inheriting acceptsBody is the agent-first move.
422
+ const overrodeBody = typeof override.body === 'function';
423
+ const declaredAcceptance = override.acceptsBody !== undefined || override.requiresBody !== undefined;
424
+ if (overrodeBody && !declaredAcceptance) {
425
+ const bodyAware = /bodyInput/.test(override.body.toString());
426
+ if (!bodyAware) {
427
+ merged.acceptsBody = undefined;
428
+ merged.requiresBody = undefined;
429
+ }
430
+ }
431
+ return merged;
432
+ }
433
+
434
+ if (builtin) return builtin;
386
435
 
387
436
  const available = [...new Set([...Object.keys(BUILTIN_TEMPLATES), ...Object.keys(configTemplates)])];
388
437
  die(`Unknown type: ${name}\nAvailable: ${available.join(', ')}`);