dotmd-cli 0.67.0 → 0.68.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.67.0",
3
+ "version": "0.68.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",
package/src/baton.mjs CHANGED
@@ -203,7 +203,9 @@ export async function runBaton(argv, config, opts = {}) {
203
203
  } else {
204
204
  const setArgs = [status, planPath];
205
205
  if (note) setArgs.push('--note', note);
206
- archiveResult = await runSet(setArgs, config, { dryRun });
206
+ // viaBaton: this release IS the handoff — don't let runSet nudge "leave a
207
+ // baton" on top of the baton we're in the middle of.
208
+ archiveResult = await runSet(setArgs, config, { dryRun, viaBaton: true });
207
209
  statusChanged = true;
208
210
  }
209
211
  }
package/src/lifecycle.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { extractFrontmatter, parseSimpleFrontmatter, normalizeEol } from './frontmatter.mjs';
4
- import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath } from './util.mjs';
4
+ import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath, currentSessionId } from './util.mjs';
5
+ import { readJournalEntries } from './journal.mjs';
5
6
  import { gitMv, getGitLastModifiedBatch } from './git.mjs';
6
7
  import { buildIndex, collectDocFiles, resolveDocArg } from './index.mjs';
7
8
  import { renderIndexFile, writeIndex } from './index-file.mjs';
@@ -598,6 +599,24 @@ export function runArchive(argv, config, opts = {}) {
598
599
  // `dotmd set in-session <path>` is refused — acquiring a lease is asymmetric
599
600
  // enough to deserve its own verb (`dotmd pickup`), and silently routing here
600
601
  // would skip the lease-acquisition path entirely.
602
+
603
+ // Did THIS session already hand off via `dotmd baton`? The journal records the
604
+ // top-level argv of every invocation; a successful `baton …` means a resume
605
+ // prompt was saved this session, so the closure nudge would be redundant. Best
606
+ // effort — a disabled journal degrades to "might nudge," which is harmless for
607
+ // an advisory line. (baton's own internal `runSet` is suppressed by `viaBaton`,
608
+ // since its journal entry isn't written until the process exits.)
609
+ function batonSavedThisSession(config) {
610
+ const sid = currentSessionId();
611
+ let entries = [];
612
+ try { entries = readJournalEntries(config); } catch { return false; }
613
+ for (let i = entries.length - 1; i >= 0; i--) {
614
+ const e = entries[i];
615
+ if (e?.sid === sid && (e.exit ?? 0) === 0 && Array.isArray(e.argv) && e.argv[0] === 'baton') return true;
616
+ }
617
+ return false;
618
+ }
619
+
601
620
  export async function runSet(argv, config, opts = {}) {
602
621
  const { dryRun } = opts;
603
622
  const noIndex = argv.includes('--no-index');
@@ -631,26 +650,56 @@ export async function runSet(argv, config, opts = {}) {
631
650
  return runArchive(archiveArgs, config, { dryRun, note, archiveStatus: newStatus });
632
651
  }
633
652
 
634
- // `partial` promises a successor tracking the deferred tail. When neither a
635
- // --note nor any doc reference exists to point at it, remind — advisory
636
- // only, computed before the transition (filing may move the file).
653
+ // Two advisory reminders, both computed from one pre-transition frontmatter
654
+ // read (a non-archive `set` never moves the file, so the path stays valid).
655
+ // Advisory only — never block the transition.
637
656
  let partialReminder = false;
638
- if (newStatus === 'partial' && !note) {
639
- try {
640
- const { frontmatter: fmRaw, body } = extractFrontmatter(readFileSync(filePath, 'utf8'));
641
- const related = parseSimpleFrontmatter(fmRaw).related_plans;
657
+ let batonNudge = false;
658
+ try {
659
+ const { frontmatter: fmRaw, body } = extractFrontmatter(readFileSync(filePath, 'utf8'));
660
+ const fm = parseSimpleFrontmatter(fmRaw);
661
+
662
+ // `partial` promises a successor tracking the deferred tail. When neither a
663
+ // --note nor any doc reference exists to point at it, remind.
664
+ if (newStatus === 'partial' && !note) {
665
+ const related = fm.related_plans;
642
666
  const hasRelated = Array.isArray(related) ? related.length > 0 : Boolean(asString(related)?.trim());
643
667
  partialReminder = !hasRelated && !/[\w./-]+\.md\b/.test(body);
644
- } catch { /* advisory only */ }
645
- }
668
+ }
669
+
670
+ // Baton-on-exit backstop (the one core-loop step with no other mechanical
671
+ // catch). Fire ONLY on the baton-less in-session release: a plan you were
672
+ // actively working (old status `in-session`) is being parked at a
673
+ // non-terminal stop status while a known next pickup (`next_step`) remains,
674
+ // and no baton was saved this session. `archived` never reaches here (routed
675
+ // to runArchive above), so this can't fire on a fully-done close. Gating on
676
+ // `in-session` keeps it off pure triage of never-started plans — where no
677
+ // session work is owed a handoff — which is how the nag-fatigue risk is
678
+ // managed. Suppressed for baton's own internal release via `opts.viaBaton`.
679
+ if (!opts.viaBaton) {
680
+ const docType = asString(fm.type) ?? 'plan';
681
+ const oldStatus = asString(fm.status);
682
+ const nextStep = asString(fm.next_step)?.trim();
683
+ batonNudge = docType === 'plan'
684
+ && oldStatus === 'in-session'
685
+ && newStatus !== 'in-session'
686
+ && Boolean(nextStep)
687
+ && !batonSavedThisSession(config);
688
+ }
689
+ } catch { /* advisory only */ }
646
690
 
647
691
  const statusArgs = [filePath, newStatus];
648
692
  if (noIndex) statusArgs.push('--no-index');
649
693
  if (showFiles) statusArgs.push('--show-files');
650
694
  await runStatus(statusArgs, config, { dryRun, suppressDeprecation: true, note });
651
695
 
652
- if (partialReminder && !dryRun) {
653
- warn('partial usually references the successor plan tracking the tail — add a link to the body, or rerun with --note "tail tracked in <plan>".');
696
+ if (!dryRun) {
697
+ if (partialReminder) {
698
+ warn('partial usually references the successor plan tracking the tail — add a link to the body, or rerun with --note "tail tracked in <plan>".');
699
+ }
700
+ if (batonNudge) {
701
+ warn(`wrapping up? leave a baton so the next session picks up cleanly — \`dotmd baton ${path.basename(filePath, '.md')} @draft\` saves a resume prompt (no copy-paste into chat).`);
702
+ }
654
703
  }
655
704
  }
656
705