dotmd-cli 0.77.3 → 0.79.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.
Files changed (55) hide show
  1. package/README.md +70 -60
  2. package/assets/opencode/plugin.js +45 -8
  3. package/bin/dotmd.mjs +274 -283
  4. package/package.json +2 -2
  5. package/scripts/postinstall.mjs +4 -4
  6. package/src/atomic-mutation.mjs +1 -1
  7. package/src/baton.mjs +86 -50
  8. package/src/check-collapse.mjs +5 -5
  9. package/src/claude-commands.mjs +6 -2
  10. package/src/commands.mjs +8 -8
  11. package/src/config.mjs +2 -2
  12. package/src/deps.mjs +1 -1
  13. package/src/doctor.mjs +17 -17
  14. package/src/fix-membership.mjs +1 -1
  15. package/src/frontmatter-fix.mjs +1 -1
  16. package/src/git.mjs +1 -1
  17. package/src/glossary.mjs +3 -3
  18. package/src/graph.mjs +1 -1
  19. package/src/guard.mjs +13 -10
  20. package/src/health.mjs +2 -2
  21. package/src/hints.mjs +7 -7
  22. package/src/host-integration.mjs +41 -22
  23. package/src/hub-membership.mjs +1 -1
  24. package/src/hud.mjs +14 -14
  25. package/src/index-file.mjs +2 -2
  26. package/src/init.mjs +24 -24
  27. package/src/install.mjs +4 -4
  28. package/src/journal.mjs +40 -7
  29. package/src/lifecycle.mjs +17 -17
  30. package/src/lint.mjs +1 -1
  31. package/src/migrate-prompts.mjs +1 -1
  32. package/src/migrate-template.mjs +2 -2
  33. package/src/migrate.mjs +1 -1
  34. package/src/misuse-read.mjs +4 -5
  35. package/src/modules.mjs +3 -3
  36. package/src/new.mjs +15 -12
  37. package/src/output-identity.mjs +7 -2
  38. package/src/pickup-card.mjs +2 -2
  39. package/src/pickup.mjs +2 -2
  40. package/src/prompts.mjs +18 -15
  41. package/src/query.mjs +9 -9
  42. package/src/rename.mjs +2 -2
  43. package/src/render.mjs +20 -20
  44. package/src/roadmap.mjs +5 -5
  45. package/src/runlist.mjs +11 -11
  46. package/src/ship.mjs +2 -2
  47. package/src/skill-drift.mjs +19 -6
  48. package/src/statuses.mjs +16 -16
  49. package/src/summary.mjs +1 -1
  50. package/src/surfaces.mjs +1 -1
  51. package/src/sync-status.mjs +4 -4
  52. package/src/update.mjs +11 -11
  53. package/src/use.mjs +6 -2
  54. package/src/validate.mjs +9 -9
  55. package/src/watch.mjs +1 -1
package/src/render.mjs CHANGED
@@ -284,7 +284,7 @@ function _renderContext(index, config, opts = {}) {
284
284
  const shown = stale.slice(0, cap).map(d => `${toSlug(d)} (${d.daysSinceUpdate}d)`).join(', ');
285
285
  const overflow = stale.length - cap;
286
286
  const tail = overflow > 0
287
- ? `${shown}, …and ${overflow} more (run \`dotmd stale\` for the full list)`
287
+ ? `${shown}, …and ${overflow} more (run \`runlist stale\` for the full list)`
288
288
  : shown;
289
289
  lines.push(`Stale: ${tail}`);
290
290
  } else {
@@ -297,7 +297,7 @@ function _renderContext(index, config, opts = {}) {
297
297
  const parts = [];
298
298
  if (withErrors.length) parts.push(`${withErrors.length} with errors`);
299
299
  if (withWarnings.length) parts.push(`${withWarnings.length} with warnings`);
300
- lines.push(`Non-compliant: ${parts.join(', ')} (run \`dotmd check\` for details)`);
300
+ lines.push(`Non-compliant: ${parts.join(', ')} (run \`runlist check\` for details)`);
301
301
  }
302
302
 
303
303
  const ctx = config.context;
@@ -371,10 +371,10 @@ export function renderBriefing(index, config) {
371
371
  lines.push(` > ${path.basename(p.path, '.md')} (${p.status}) ${next}`);
372
372
  }
373
373
  if (liveRoadmaps.length) {
374
- lines.push(` ${liveRoadmaps.length} roadmap${liveRoadmaps.length === 1 ? '' : 's'} ${dim('· dotmd roadmaps')}`);
374
+ lines.push(` ${liveRoadmaps.length} roadmap${liveRoadmaps.length === 1 ? '' : 's'} ${dim('· runlist roadmaps')}`);
375
375
  }
376
376
  if (liveRunlists) {
377
- lines.push(` ${liveRunlists} runlist${liveRunlists === 1 ? '' : 's'} ${dim('· dotmd runlists')}`);
377
+ lines.push(` ${liveRunlists} runlist${liveRunlists === 1 ? '' : 's'} ${dim('· runlist runlists')}`);
378
378
  }
379
379
  }
380
380
 
@@ -396,7 +396,7 @@ export function renderBriefing(index, config) {
396
396
  // with no clue what or where. `dotmd check` is the canonical detail view.
397
397
  const errorCount = index.errors.length;
398
398
  const errorPart = errorCount > 0
399
- ? `Errors: ${errorCount} ${dim('(run `dotmd check` to see)')}`
399
+ ? `Errors: ${errorCount} ${dim('(run `runlist check` to see)')}`
400
400
  : `Errors: ${errorCount}`;
401
401
  lines.push(`Stale: ${stale} | ${errorPart} | Warnings: ${index.warnings.length}`);
402
402
 
@@ -420,13 +420,13 @@ export function classifyIssueAction(issue) {
420
420
  // re-matching their prose. Drift is mechanically fixable; an unreadable cell
421
421
  // is not — nothing can guess which word in it was meant to be the status.
422
422
  if (issue?.meta?.kind === 'hub-status-drift') {
423
- return { action: 'dotmd sync-status', fixable: true, label: 'hub status drift' };
423
+ return { action: 'runlist sync-status', fixable: true, label: 'hub status drift' };
424
424
  }
425
425
  if (issue?.meta?.kind === 'hub-status-unreadable') {
426
426
  return { action: `edit ${file}: lead the status cell with the status word, or wrap it in <!--s-->…<!--/s-->`, fixable: false, label: 'hub status rows' };
427
427
  }
428
428
  if (issue?.meta?.kind === 'hub-membership-backref') {
429
- return { action: 'dotmd fix-membership', fixable: true, label: 'membership back-references' };
429
+ return { action: 'runlist fix-membership', fixable: true, label: 'membership back-references' };
430
430
  }
431
431
  if (issue?.meta?.kind === 'hub-membership-orphan') {
432
432
  return { action: `edit ${file}: add the child to the hub's meaningful list, or correct the child's parent_plan`, fixable: false, label: 'orphan membership claims' };
@@ -436,41 +436,41 @@ export function classifyIssueAction(issue) {
436
436
  }
437
437
 
438
438
  if (/Missing frontmatter `status`/.test(message)) {
439
- return { action: `dotmd bulk-tag ${file}`, fixable: false, label: 'missing status' };
439
+ return { action: `runlist bulk-tag ${file}`, fixable: false, label: 'missing status' };
440
440
  }
441
441
  if (/Missing frontmatter `updated`/.test(message) || /frontmatter `updated: .*` is behind git history/.test(message)) {
442
- return { action: 'dotmd touch --git', fixable: true, label: 'dates' };
442
+ return { action: 'runlist touch --git', fixable: true, label: 'dates' };
443
443
  }
444
- if ((/`(?:module|surface):` \(singular\) is deprecated/.test(message) && /Run `dotmd lint --fix`/.test(message))
444
+ if ((/`(?:module|surface):` \(singular\) is deprecated/.test(message) && /Run `runlist lint --fix`/.test(message))
445
445
  || /camelCase|nextStep|currentState|auditLevel/.test(message)) {
446
- return { action: 'dotmd lint --fix', fixable: true, label: 'frontmatter migrations' };
446
+ return { action: 'runlist lint --fix', fixable: true, label: 'frontmatter migrations' };
447
447
  }
448
448
  if (/Unknown surface/.test(message)) {
449
- return { action: 'dotmd surfaces', fixable: false, label: 'taxonomy' };
449
+ return { action: 'runlist surfaces', fixable: false, label: 'taxonomy' };
450
450
  }
451
451
  if (/Glossary config points at section|Glossary file configured/.test(message)) {
452
- return { action: 'edit dotmd.config.mjs glossary.path/glossary.section', fixable: false, label: 'glossary config' };
452
+ return { action: 'edit runlist.config.mjs glossary.path/glossary.section', fixable: false, label: 'glossary config' };
453
453
  }
454
454
  if (/under `.*\/` but `status: .*` is not an archive status/.test(message)) {
455
- return { action: message.match(/Run `([^`]+)`/)?.[1] ?? `dotmd set archived ${file}`, fixable: true, label: 'archive drift' };
455
+ return { action: message.match(/Run `([^`]+)`/)?.[1] ?? `runlist set archived ${file}`, fixable: true, label: 'archive drift' };
456
456
  }
457
457
  if (/`status: .*` but file is a direct child/.test(message)) {
458
- return { action: `dotmd archive ${file}`, fixable: true, label: 'archive drift' };
458
+ return { action: `runlist archive ${file}`, fixable: true, label: 'archive drift' };
459
459
  }
460
460
  if (/`current_state` is \d+ chars|`next_step` is \d+ chars/.test(message)) {
461
- return { action: 'dotmd doctor --frontmatter-fix', fixable: true, label: 'long frontmatter' };
461
+ return { action: 'runlist doctor --frontmatter-fix', fixable: true, label: 'long frontmatter' };
462
462
  }
463
463
  if (/`modules` is required/.test(message)) {
464
- return { action: `edit ${file} modules: or run dotmd lint --fix if scaffolded empty`, fixable: false, label: 'module metadata' };
464
+ return { action: `edit ${file} modules: or run runlist lint --fix if scaffolded empty`, fixable: false, label: 'module metadata' };
465
465
  }
466
466
  if (issue?.meta?.kind === 'body-link-resolution') {
467
467
  if (issue.meta.targetKind === 'document' && issue.meta.reason === 'missing') {
468
- return { action: 'dotmd fix-refs --dry-run', fixable: true, label: 'document links' };
468
+ return { action: 'runlist fix-refs --dry-run', fixable: true, label: 'document links' };
469
469
  }
470
470
  return { action: `edit ${file}: correct the linked file, asset, or directory`, fixable: false, label: 'file links' };
471
471
  }
472
472
  if (/entry `.*` does not resolve/.test(message)) {
473
- return { action: 'dotmd fix-refs --dry-run', fixable: true, label: 'references' };
473
+ return { action: 'runlist fix-refs --dry-run', fixable: true, label: 'references' };
474
474
  }
475
475
 
476
476
  return { action: `edit ${file}`, fixable: false, label: 'manual review' };
@@ -573,7 +573,7 @@ function _renderCheck(index, config, opts = {}) {
573
573
  }
574
574
  lines.push('');
575
575
  } else {
576
- 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.`));
576
+ lines.push(dim(`Run \`runlist check --verbose\` for per-doc detail. \`runlist doctor --apply\` auto-fixes supported issues (bare \`runlist doctor\` previews only); remaining issues need the suggested manual command.`));
577
577
  lines.push('');
578
578
  }
579
579
  }
package/src/roadmap.mjs CHANGED
@@ -61,7 +61,7 @@ function renderRoadmap(info, maxWidth) {
61
61
  return lines.join('\n') + '\n';
62
62
  }
63
63
 
64
- const NO_ROADMAPS = 'No roadmaps found. A roadmap is a plan with `execution_mode: roadmap` that composes runlists.\nScaffold one: dotmd new plan <name> --roadmap\n';
64
+ const NO_ROADMAPS = 'No roadmaps found. A roadmap is a plan with `execution_mode: roadmap` that composes runlists.\nScaffold one: runlist new plan <name> --roadmap\n';
65
65
 
66
66
  // `dotmd roadmaps` — the dashboard over every roadmap hub (mirrors `dotmd
67
67
  // runlists`): one row per roadmap with its recursive grand total + child count.
@@ -106,7 +106,7 @@ export function runRoadmap(index, argv, config) {
106
106
  const info = roadmaps.get(repoPath);
107
107
  if (!info) {
108
108
  die(`${repoPath} is not a roadmap hub (needs \`execution_mode: roadmap\`).\n` +
109
- `See \`dotmd roadmaps\` for roadmaps, or \`dotmd runlist ${hubArg}\` if it's a runlist.`);
109
+ `See \`runlist roadmaps\` for roadmaps, or \`runlist runlist ${hubArg}\` if it's a runlist.`);
110
110
  }
111
111
  if (json) { process.stdout.write(JSON.stringify(roadmapJson(info), null, 2) + '\n'); return; }
112
112
  process.stdout.write('\n' + renderRoadmap(info, process.stdout.columns || 100) + '\n');
@@ -139,10 +139,10 @@ function resolveRoadmapTarget(roadmaps, hubArg, config) {
139
139
  if (!info) die(`${hubArg} is not a roadmap hub (needs \`execution_mode: roadmap\`).`);
140
140
  return info;
141
141
  }
142
- if (roadmaps.size === 0) die('No roadmaps found. Scaffold one: dotmd new plan <name> --roadmap');
142
+ if (roadmaps.size === 0) die('No roadmaps found. Scaffold one: runlist new plan <name> --roadmap');
143
143
  if (roadmaps.size > 1) {
144
144
  const listed = [...roadmaps.values()].map(r => ` ${hubLabel(r.doc)}`).join('\n');
145
- die(`Multiple roadmaps — name one: dotmd roadmap next <hub>\n${listed}`);
145
+ die(`Multiple roadmaps — name one: runlist roadmap next <hub>\n${listed}`);
146
146
  }
147
147
  return [...roadmaps.values()][0];
148
148
  }
@@ -169,7 +169,7 @@ export async function runRoadmapNext(index, argv, config, opts = {}) {
169
169
  return ` ${hubLabel(c.doc)} (${c.doneCount}/${c.total} · ${state})`;
170
170
  }).join('\n');
171
171
  die(`No pickup-able plan across roadmap ${hubLabel(info.doc)} — every child runlist is done or parked:\n${lines}\n` +
172
- `Unstick one (e.g. \`dotmd set active <child>\`), or inspect a runlist: \`dotmd runlist <hub>\`.`);
172
+ `Unstick one (e.g. \`runlist set active <child>\`), or inspect a runlist: \`runlist runlist <hub>\`.`);
173
173
  }
174
174
 
175
175
  if (!json) {
package/src/runlist.mjs CHANGED
@@ -426,7 +426,7 @@ function renderRunlist(hubRepoPath, children, opts = {}) {
426
426
  const parked = children.filter(c => !c.missing && !archiveStatuses.has(c.status));
427
427
  lines.push(dim(parked.length === 0
428
428
  ? ' All children archived. Hub is ready for archive.'
429
- : ` No pickup-able child — ${parked.length} parked. Unstick one (e.g. \`dotmd set active <child>\`) to continue.`));
429
+ : ` No pickup-able child — ${parked.length} parked. Unstick one (e.g. \`runlist set active <child>\`) to continue.`));
430
430
  }
431
431
  return lines.join('\n') + '\n';
432
432
  }
@@ -497,7 +497,7 @@ async function runRunlistAdd(positional, config, { dryRun, json, testHooks }) {
497
497
  const hubInput = positional[0];
498
498
  const childTokens = positional.slice(1);
499
499
  if (!hubInput || childTokens.length === 0) {
500
- die('Usage: dotmd runlist add <hub-plan> <child...> (one or more child slugs or plan paths)');
500
+ die('Usage: runlist runlist add <hub-plan> <child...> (one or more child slugs or plan paths)');
501
501
  }
502
502
 
503
503
  let hubAbs = resolveHubInput(hubInput, config);
@@ -596,7 +596,7 @@ async function runRunlistAdd(positional, config, { dryRun, json, testHooks }) {
596
596
  if (!json) {
597
597
  process.stdout.write(dim(` runlist now has ${newRefs.length} ${newRefs.length === 1 ? 'child' : 'children'}.`) + '\n');
598
598
  if (!dryRun) {
599
- process.stdout.write(dim(` Show: dotmd runlist ${hubRepoPath} · pick up next: dotmd runlist next ${hubRepoPath}`) + '\n');
599
+ process.stdout.write(dim(` Show: runlist runlist ${hubRepoPath} · pick up next: runlist runlist next ${hubRepoPath}`) + '\n');
600
600
  }
601
601
  }
602
602
  }
@@ -729,7 +729,7 @@ function planClearChildParent(childAbs, hubAbs, config) {
729
729
  // Shared front half of remove/reorder: resolve the hub, read its `runlist:`,
730
730
  // die if it isn't a sprint hub with an array to mutate.
731
731
  function loadSprintHub(hubInput, verb, config) {
732
- if (!hubInput) die(`Usage: dotmd runlist ${verb} <hub-plan> <child...>`);
732
+ if (!hubInput) die(`Usage: runlist runlist ${verb} <hub-plan> <child...>`);
733
733
  let hubAbs = resolveHubInput(hubInput, config);
734
734
  if (!hubAbs) die(`Hub plan not found: ${hubInput}`);
735
735
  hubAbs = authorizeManagedSource(hubAbs, config, { kind: `Runlist ${verb} hub source` }).path;
@@ -749,7 +749,7 @@ function loadSprintHub(hubInput, verb, config) {
749
749
  async function runRunlistRemove(positional, config, { dryRun, json, clearParent, testHooks }) {
750
750
  const { hubAbs, hubRepoPath, hubDir, existingRefs, raw: hubRaw } = loadSprintHub(positional[0], 'remove', config);
751
751
  const childTokens = positional.slice(1);
752
- if (childTokens.length === 0) die('Usage: dotmd runlist remove <hub-plan> <child...>');
752
+ if (childTokens.length === 0) die('Usage: runlist runlist remove <hub-plan> <child...>');
753
753
 
754
754
  const removeRefs = [];
755
755
  for (const token of childTokens) {
@@ -814,7 +814,7 @@ function parseReorderArgs(argv) {
814
814
  async function runRunlistReorder(argv, config, { dryRun, json, testHooks }) {
815
815
  const { hubInput, children, before, after } = parseReorderArgs(argv);
816
816
  if (!hubInput || children.length === 0) {
817
- die('Usage: dotmd runlist reorder <hub> <child> --before|--after <other>\n or: dotmd runlist reorder <hub> <child1> <child2> ... (full new order)');
817
+ die('Usage: runlist runlist reorder <hub> <child> --before|--after <other>\n or: runlist runlist reorder <hub> <child1> <child2> ... (full new order)');
818
818
  }
819
819
  const { hubAbs, hubRepoPath, hubDir, existingRefs, raw: hubRaw } = loadSprintHub(hubInput, 'reorder', config);
820
820
 
@@ -876,8 +876,8 @@ export async function runRunlist(argv, config, opts = {}) {
876
876
 
877
877
  if (!hubInput) {
878
878
  die(sub === 'next'
879
- ? 'Usage: dotmd runlist next <hub-plan>'
880
- : 'Usage: dotmd runlist <hub-plan>');
879
+ ? 'Usage: runlist runlist next <hub-plan>'
880
+ : 'Usage: runlist runlist <hub-plan>');
881
881
  }
882
882
 
883
883
  const hubAbs = resolveHubInput(hubInput, config);
@@ -919,13 +919,13 @@ export async function runRunlist(argv, config, opts = {}) {
919
919
  `No pickup-able child in runlist ${hubRepoPath} — every remaining child is parked:\n` +
920
920
  `${listed}\n` +
921
921
  `Unstick one before continuing:\n` +
922
- ` dotmd set active <child> # if ready to resume\n` +
923
- ` dotmd use <child> # to inspect`,
922
+ ` runlist set active <child> # if ready to resume\n` +
923
+ ` runlist use <child> # to inspect`,
924
924
  );
925
925
  }
926
926
  const allArchived = children.some(c => !c.missing) && children.every(c => c.missing || archiveStatuses.has(c.status));
927
927
  if (allArchived) {
928
- die(`All children in runlist ${hubRepoPath} are archived. Hub is ready for \`dotmd archive ${hubRepoPath}\`.`);
928
+ die(`All children in runlist ${hubRepoPath} are archived. Hub is ready for \`runlist archive ${hubRepoPath}\`.`);
929
929
  }
930
930
  const missing = children.filter(c => c.missing).map(c => c.ref);
931
931
  die(`No pickup-able child in runlist ${hubRepoPath}. Unresolved refs: ${missing.join(', ')}`);
package/src/ship.mjs CHANGED
@@ -76,7 +76,7 @@ export async function runShip(argv, config, opts = {}) {
76
76
  const positional = argv.filter(a => !a.startsWith('-'));
77
77
  const bump = positional[0] ?? 'patch';
78
78
  if (!['patch', 'minor', 'major'].includes(bump)) {
79
- die(`Invalid bump: ${bump}. Usage: dotmd ship [patch|minor|major]`);
79
+ die(`Invalid bump: ${bump}. Usage: runlist ship [patch|minor|major]`);
80
80
  }
81
81
 
82
82
  const pkgPath = path.join(config.repoRoot, 'package.json');
@@ -140,7 +140,7 @@ export async function runShip(argv, config, opts = {}) {
140
140
  }
141
141
 
142
142
  const subject = `chore: release ${target}`;
143
- const body = `Auto-staged by \`dotmd ship\`:\n${allToStage.map(p => `- ${p}`).join('\n')}`;
143
+ const body = `Auto-staged by \`runlist ship\`:\n${allToStage.map(p => `- ${p}`).join('\n')}`;
144
144
  const commitMsg = `${subject}\n\n${body}`;
145
145
  const commit = spawnSync('git', ['commit', '-m', commitMsg], { cwd: config.repoRoot, encoding: 'utf8' });
146
146
  if (commit.status !== 0) die(`git commit failed: ${commit.stderr || commit.stdout}`);
@@ -15,9 +15,19 @@ import path from 'node:path';
15
15
  // surface and forgetting the other is exactly what this catches.
16
16
 
17
17
  export const CANONICAL_MARKERS = {
18
+ start: '<!-- runlist:canonical-workflow:start -->',
19
+ end: '<!-- runlist:canonical-workflow:end -->',
20
+ };
21
+
22
+ // The spelling used through dotmd-cli 0.78.0. Still read, so a copy of either
23
+ // surface that predates the rename keeps participating in the lockstep instead
24
+ // of silently dropping out of it. A block must open and close with the same
25
+ // spelling; the current spelling is tried first.
26
+ export const LEGACY_CANONICAL_MARKERS = {
18
27
  start: '<!-- dotmd:canonical-workflow:start -->',
19
28
  end: '<!-- dotmd:canonical-workflow:end -->',
20
29
  };
30
+ const MARKER_SETS = [CANONICAL_MARKERS, LEGACY_CANONICAL_MARKERS];
21
31
 
22
32
  const CLAUDE_MD = 'CLAUDE.md';
23
33
  const SKILL_MD = path.join('plugins', 'dotmd', 'skills', 'dotmd', 'SKILL.md');
@@ -28,12 +38,15 @@ const SKILL_MD = path.join('plugins', 'dotmd', 'skills', 'dotmd', 'SKILL.md');
28
38
  // its own CLAUDE.md (but never adopted the block) from ever tripping the guard.
29
39
  export function extractCanonicalBlock(text) {
30
40
  if (typeof text !== 'string') return null;
31
- const startIdx = text.indexOf(CANONICAL_MARKERS.start);
32
- if (startIdx === -1) return null;
33
- const afterStart = startIdx + CANONICAL_MARKERS.start.length;
34
- const endIdx = text.indexOf(CANONICAL_MARKERS.end, afterStart);
35
- if (endIdx === -1 || endIdx < afterStart) return null;
36
- return text.slice(afterStart, endIdx);
41
+ for (const markers of MARKER_SETS) {
42
+ const startIdx = text.indexOf(markers.start);
43
+ if (startIdx === -1) continue;
44
+ const afterStart = startIdx + markers.start.length;
45
+ const endIdx = text.indexOf(markers.end, afterStart);
46
+ if (endIdx === -1 || endIdx < afterStart) continue;
47
+ return text.slice(afterStart, endIdx);
48
+ }
49
+ return null;
37
50
  }
38
51
 
39
52
  // Whitespace-tolerant so a CRLF vs LF or a stray trailing space between the two
package/src/statuses.mjs CHANGED
@@ -125,8 +125,8 @@ function formatPropDisplay(v) {
125
125
  async function runAddStatus(args, config, opts) {
126
126
  const flags = parseFlags(args, { allowProps: true });
127
127
  const name = flags.positional[0];
128
- if (!name) die('Usage: dotmd statuses add <name> --type <type> [--like <existing>] [flags]');
129
- if (!flags.type) die('--type is required for `dotmd statuses add`.');
128
+ if (!name) die('Usage: runlist statuses add <name> --type <type> [--like <existing>] [flags]');
129
+ if (!flags.type) die('--type is required for `runlist statuses add`.');
130
130
 
131
131
  const validationErr = validateStatusName(name);
132
132
  if (validationErr) die(validationErr);
@@ -135,11 +135,11 @@ async function runAddStatus(args, config, opts) {
135
135
  const content = readFileSync(config.configPath, 'utf8');
136
136
  const parsed = parseStatusesBlock(content, flags.type);
137
137
  if (parsed.form === 'array') {
138
- die(`Type '${flags.type}' uses array-form statuses. Run \`dotmd statuses migrate ${flags.type}\` first to convert to rich form.`);
138
+ die(`Type '${flags.type}' uses array-form statuses. Run \`runlist statuses migrate ${flags.type}\` first to convert to rich form.`);
139
139
  }
140
140
 
141
141
  if (parsed.entries.some(e => e.name === name)) {
142
- die(`Status '${name}' already exists in type '${flags.type}'. Use \`dotmd statuses set\` to edit it.`);
142
+ die(`Status '${name}' already exists in type '${flags.type}'. Use \`runlist statuses set\` to edit it.`);
143
143
  }
144
144
 
145
145
  // Resolve --like base flags
@@ -152,7 +152,7 @@ async function runAddStatus(args, config, opts) {
152
152
  die(`--like target '${likeName}' is not defined in type '${flags.type}'.`);
153
153
  }
154
154
  if (likeEntry.multiLine) {
155
- die(`--like target '${likeName}' spans multiple lines in dotmd.config.mjs; this CLI only edits single-line entries.`);
155
+ die(`--like target '${likeName}' spans multiple lines in runlist.config.mjs; this CLI only edits single-line entries.`);
156
156
  }
157
157
  baseProps = parseEntryProps(likeEntry.raw);
158
158
  }
@@ -235,8 +235,8 @@ function labelOrigin(key, userProps) {
235
235
  async function runSetStatus(args, config, opts) {
236
236
  const flags = parseFlags(args, { allowProps: true });
237
237
  const name = flags.positional[0];
238
- if (!name) die('Usage: dotmd statuses set <name> --type <type> [flags]');
239
- if (!flags.type) die('--type is required for `dotmd statuses set`.');
238
+ if (!name) die('Usage: runlist statuses set <name> --type <type> [flags]');
239
+ if (!flags.type) die('--type is required for `runlist statuses set`.');
240
240
  if (Object.keys(flags.props).length === 0) {
241
241
  die('At least one flag is required (e.g. --quiet, --staleDays 30).');
242
242
  }
@@ -245,15 +245,15 @@ async function runSetStatus(args, config, opts) {
245
245
  const content = readFileSync(config.configPath, 'utf8');
246
246
  const parsed = parseStatusesBlock(content, flags.type);
247
247
  if (parsed.form === 'array') {
248
- die(`Type '${flags.type}' uses array-form statuses. Run \`dotmd statuses migrate ${flags.type}\` first.`);
248
+ die(`Type '${flags.type}' uses array-form statuses. Run \`runlist statuses migrate ${flags.type}\` first.`);
249
249
  }
250
250
 
251
251
  const existing = parsed.entries.find(e => e.name === name);
252
252
  if (!existing) {
253
- die(`Status '${name}' is not defined in type '${flags.type}'. Use \`dotmd statuses add\` to create it.`);
253
+ die(`Status '${name}' is not defined in type '${flags.type}'. Use \`runlist statuses add\` to create it.`);
254
254
  }
255
255
  if (existing.multiLine) {
256
- die(`Status '${name}' spans multiple lines in dotmd.config.mjs; edit it by hand.`);
256
+ die(`Status '${name}' spans multiple lines in runlist.config.mjs; edit it by hand.`);
257
257
  }
258
258
 
259
259
  const oldProps = parseEntryProps(existing.raw);
@@ -306,14 +306,14 @@ function printSetDiff(name, typeName, oldProps, newProps, userProps) {
306
306
  async function runRemoveStatus(args, config, opts) {
307
307
  const flags = parseFlags(args, { allowProps: false });
308
308
  const name = flags.positional[0];
309
- if (!name) die('Usage: dotmd statuses remove <name> --type <type>');
310
- if (!flags.type) die('--type is required for `dotmd statuses remove`.');
309
+ if (!name) die('Usage: runlist statuses remove <name> --type <type>');
310
+ if (!flags.type) die('--type is required for `runlist statuses remove`.');
311
311
 
312
312
  requireConfigPath(config);
313
313
  const content = readFileSync(config.configPath, 'utf8');
314
314
  const parsed = parseStatusesBlock(content, flags.type);
315
315
  if (parsed.form === 'array') {
316
- die(`Type '${flags.type}' uses array-form statuses. Run \`dotmd statuses migrate ${flags.type}\` first.`);
316
+ die(`Type '${flags.type}' uses array-form statuses. Run \`runlist statuses migrate ${flags.type}\` first.`);
317
317
  }
318
318
  if (!parsed.entries.find(e => e.name === name)) {
319
319
  die(`Status '${name}' is not defined in type '${flags.type}'.`);
@@ -324,7 +324,7 @@ async function runRemoveStatus(args, config, opts) {
324
324
  if (offenders.length > 0) {
325
325
  const list = offenders.slice(0, 10).map(p => ` - ${p}`).join('\n');
326
326
  const more = offenders.length > 10 ? `\n ... and ${offenders.length - 10} more` : '';
327
- die(`${offenders.length} doc(s) currently use status '${name}':\n${list}${more}\n\nMigrate them first: \`dotmd migrate status ${name} <other> [files...]\``);
327
+ die(`${offenders.length} doc(s) currently use status '${name}':\n${list}${more}\n\nMigrate them first: \`runlist migrate status ${name} <other> [files...]\``);
328
328
  }
329
329
 
330
330
  // Warn (don't refuse) if explicit lifecycle references the name.
@@ -412,7 +412,7 @@ function findMatchingBrace(content, openPos) {
412
412
  async function runMigrateType(args, config, opts) {
413
413
  const flags = parseFlags(args, { allowProps: false });
414
414
  const typeName = flags.positional[0];
415
- if (!typeName) die('Usage: dotmd statuses migrate <type>');
415
+ if (!typeName) die('Usage: runlist statuses migrate <type>');
416
416
 
417
417
  requireConfigPath(config);
418
418
  const content = readFileSync(config.configPath, 'utf8');
@@ -581,7 +581,7 @@ function lastNewlineIndexBefore(content, pos) {
581
581
 
582
582
  function requireConfigPath(config) {
583
583
  if (!config.configPath || !existsSync(config.configPath)) {
584
- die(`No dotmd.config.mjs found in ${process.cwd()} — run \`dotmd init\` first.`);
584
+ die(`No runlist.config.mjs found in ${process.cwd()} — run \`runlist init\` first.`);
585
585
  }
586
586
  }
587
587
 
package/src/summary.mjs CHANGED
@@ -22,7 +22,7 @@ export function runSummary(argv, config) {
22
22
  }
23
23
 
24
24
  const input = positional[0];
25
- if (!input) { die('Usage: dotmd summary <file> [--model <name>] [--json]'); }
25
+ if (!input) { die('Usage: runlist summary <file> [--model <name>] [--json]'); }
26
26
 
27
27
  const filePath = resolveDocArg(input, config);
28
28
 
package/src/surfaces.mjs CHANGED
@@ -20,7 +20,7 @@ export function runSurfaces(argv, config) {
20
20
 
21
21
  if (!surfaces || surfaces.length === 0) {
22
22
  process.stdout.write(dim('No surface taxonomy configured. Any surface value is accepted.\n'));
23
- process.stdout.write(dim('To restrict, set `taxonomy.surfaces` in dotmd.config.mjs.\n'));
23
+ process.stdout.write(dim('To restrict, set `taxonomy.surfaces` in runlist.config.mjs.\n'));
24
24
  return;
25
25
  }
26
26
 
@@ -163,8 +163,8 @@ export function checkHubStatusDrift(docs, config) {
163
163
  path: hub.path,
164
164
  level: row.marked ? 'error' : 'warning',
165
165
  message: row.marked
166
- ? `line ${row.line} rows \`${row.target}\` as \`${row.printed}\` inside a \`${MARKER_OPEN}…${MARKER_CLOSE}\` marker, but that doc's status is \`${row.actual}\`. The marker means dotmd owns that word — run \`dotmd sync-status\` to rewrite it, or change the doc's status.`
167
- : `line ${row.line} rows \`${row.target}\` as \`${row.printed}\`, but that doc's status is \`${row.actual}\`. Run \`dotmd sync-status\` to rewrite the row, or change the doc's status.`,
166
+ ? `line ${row.line} rows \`${row.target}\` as \`${row.printed}\` inside a \`${MARKER_OPEN}…${MARKER_CLOSE}\` marker, but that doc's status is \`${row.actual}\`. The marker means runlist owns that word — run \`runlist sync-status\` to rewrite it, or change the doc's status.`
167
+ : `line ${row.line} rows \`${row.target}\` as \`${row.printed}\`, but that doc's status is \`${row.actual}\`. Run \`runlist sync-status\` to rewrite the row, or change the doc's status.`,
168
168
  meta: { kind: DRIFT_KIND, target: row.target, printed: row.printed, actual: row.actual, line: row.line, marked: row.marked },
169
169
  };
170
170
  (row.marked ? errors : warnings).push(entry);
@@ -257,7 +257,7 @@ export function syncHubStatuses(config, { docs, dryRun = false, adopt = false, h
257
257
  if (result.unreadable > 0) {
258
258
  process.stdout.write(yellow(`${result.unreadable} row(s) sit under a status column with no readable status word`)
259
259
  + ' — lead the cell with the status, or wrap it in '
260
- + `${MARKER_OPEN}…${MARKER_CLOSE}. Run \`dotmd check\` to list them.\n`);
260
+ + `${MARKER_OPEN}…${MARKER_CLOSE}. Run \`runlist check\` to list them.\n`);
261
261
  }
262
262
  }
263
263
  return result;
@@ -281,7 +281,7 @@ function resolveHubArgs(args, docs) {
281
281
  const [match] = matches;
282
282
  if (!isHubDoc(match)) {
283
283
  die(`${match.path} is not a hub — it has no \`runlist:\` and no \`execution_mode: coordination|roadmap\`. `
284
- + 'Run `dotmd sync-status` with no argument to sweep every hub.');
284
+ + 'Run `runlist sync-status` with no argument to sweep every hub.');
285
285
  }
286
286
  paths.add(match.path);
287
287
  }
package/src/update.mjs CHANGED
@@ -89,7 +89,7 @@ export function readInstalledPlugin(opts = {}) {
89
89
  export function planMarketplaceRepair(plugin, { hasClaude, verb }) {
90
90
  const reason = `marketplace "${plugin.marketplace}" is not registered, so the installed plugin cannot load`;
91
91
  if (plugin.marketplace !== 'dotmd') {
92
- return [{ kind: 'skip', reason: `${reason} — re-add that marketplace (dotmd does not know its source), then rerun` }];
92
+ return [{ kind: 'skip', reason: `${reason} — re-add that marketplace (runlist does not know its source), then rerun` }];
93
93
  }
94
94
  if (!hasClaude) {
95
95
  return [{ kind: 'manual', reason, lines: [`/plugin marketplace add ${CLAUDE_MARKETPLACE}`, `/plugin ${verb} ${plugin.id}`] }];
@@ -112,7 +112,7 @@ export function planUpdate(opts, ctx) {
112
112
  }
113
113
  if (!opts.cliOnly) {
114
114
  if (!ctx.plugin) {
115
- steps.push({ kind: 'skip', reason: 'dotmd plugin not installed — skipping plugin update' });
115
+ steps.push({ kind: 'skip', reason: 'runlist plugin not installed — skipping plugin update' });
116
116
  } else if (ctx.plugin.marketplaceRegistered === false) {
117
117
  steps.push(...planMarketplaceRepair(ctx.plugin, { hasClaude: ctx.hasClaude, verb: 'update' }));
118
118
  } else if (!ctx.hasClaude) {
@@ -127,7 +127,7 @@ export function planUpdate(opts, ctx) {
127
127
  if (ctx.opencode?.exists && ctx.opencode.stale) {
128
128
  steps.push({ kind: 'opencode', path: ctx.opencode.path });
129
129
  } else if (ctx.opencode?.foreign) {
130
- steps.push({ kind: 'skip', reason: `${ctx.opencode.path} was not written by dotmd — leaving it alone` });
130
+ steps.push({ kind: 'skip', reason: `${ctx.opencode.path} was not written by runlist — leaving it alone` });
131
131
  }
132
132
  }
133
133
  return steps;
@@ -140,24 +140,24 @@ export function runUpdate(argv, _config, opts = {}) {
140
140
  const plugin = readInstalledPlugin();
141
141
 
142
142
  if (check) {
143
- process.stdout.write(`dotmd CLI: ${pkg.version}\n`);
143
+ process.stdout.write(`runlist CLI: ${pkg.version}\n`);
144
144
  if (plugin) {
145
145
  const cmp = compareVersions(plugin.version, pkg.version);
146
146
  const tag = cmp === 0 ? green('in sync')
147
147
  : cmp === null ? dim('(unknown)')
148
- : cmp < 0 ? yellow('behind — run `dotmd update`')
148
+ : cmp < 0 ? yellow('behind — run `runlist update`')
149
149
  : yellow('ahead — CLI is behind');
150
- process.stdout.write(`dotmd plugin: ${plugin.version ?? '?'} (${plugin.id}) ${tag}\n`);
150
+ process.stdout.write(`runlist plugin: ${plugin.version ?? '?'} (${plugin.id}) ${tag}\n`);
151
151
  if (plugin.marketplaceRegistered === false) {
152
- process.stdout.write(yellow(` marketplace "${plugin.marketplace}" is not registered — the plugin fails to load; run \`dotmd install claude\`\n`));
152
+ process.stdout.write(yellow(` marketplace "${plugin.marketplace}" is not registered — the plugin fails to load; run \`runlist install claude\`\n`));
153
153
  }
154
154
  } else {
155
- process.stdout.write(dim('dotmd plugin: not installed — `dotmd install claude`\n'));
155
+ process.stdout.write(dim('runlist plugin: not installed — `runlist install claude`\n'));
156
156
  }
157
157
  const oc = opencodeStatus({ version: pkg.version });
158
- if (!oc.exists) process.stdout.write(dim('dotmd opencode: not installed\n'));
159
- else if (oc.foreign) process.stdout.write(`dotmd opencode: ${yellow('unmanaged file — not written by dotmd')}\n`);
160
- else process.stdout.write(`dotmd opencode: ${oc.version} ${oc.stale ? yellow('behind — run `dotmd update`') : green('in sync')}\n`);
158
+ if (!oc.exists) process.stdout.write(dim('runlist opencode: not installed\n'));
159
+ else if (oc.foreign) process.stdout.write(`runlist opencode: ${yellow('unmanaged file — not written by runlist')}\n`);
160
+ else process.stdout.write(`runlist opencode: ${oc.version} ${oc.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
161
161
  return;
162
162
  }
163
163
 
package/src/use.mjs CHANGED
@@ -15,13 +15,16 @@ import { resolveDocArg } from './index.mjs';
15
15
  // With no argument, consumes the oldest pending prompt.
16
16
  export async function runUse(argv, config, opts = {}) {
17
17
  const positional = argv.find(a => !a.startsWith('-'));
18
+ // `--no-claim` reads and archives a prompt without starting the plan it links.
19
+ const noClaim = argv.includes('--no-claim');
20
+ const promptOpts = { ...opts, noClaim };
18
21
 
19
22
  if (!positional) {
20
23
  const queue = pendingPromptsOldestFirst(config);
21
24
  if (queue.length === 0) die('No pending prompts. Pass a file to use a plan or doc.');
22
25
  const head = queue[0];
23
26
  if (!head.abs) die(`Could not resolve path: ${head.doc.path}`);
24
- return consumePrompt(head.abs, config, opts);
27
+ return consumePrompt(head.abs, config, promptOpts);
25
28
  }
26
29
 
27
30
  // Exact path first, then prompt slugs (they keep precedence on a slug
@@ -39,8 +42,9 @@ export async function runUse(argv, config, opts = {}) {
39
42
  const type = asString(parsed.type);
40
43
 
41
44
  if (type === 'prompt') {
42
- return consumePrompt(filePath, config, opts);
45
+ return consumePrompt(filePath, config, promptOpts);
43
46
  }
47
+ if (noClaim) die('--no-claim applies to prompts only; a plan opened with `use` is always claimed.');
44
48
  if (type === 'plan') {
45
49
  // Mark in-session (pure frontmatter) and print the plan card.
46
50
  return startPlan([filePath, ...argv.filter(a => a !== positional)], config, opts);
package/src/validate.mjs CHANGED
@@ -143,7 +143,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
143
143
  doc.warnings.push({
144
144
  path: doc.path,
145
145
  level: 'warning',
146
- message: `Unknown surface \`${surface}\`; expected a known surface taxonomy value.${hint} Run \`dotmd surfaces\` to list all valid values.`,
146
+ message: `Unknown surface \`${surface}\`; expected a known surface taxonomy value.${hint} Run \`runlist surfaces\` to list all valid values.`,
147
147
  });
148
148
  }
149
149
  }
@@ -173,7 +173,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
173
173
  const autoFixable = singularValues.every(autoFixableString)
174
174
  && pluralValue.every(autoFixableString);
175
175
  const guidance = autoFixable
176
- ? `use \`${target}\`. Run \`dotmd lint --fix\` to migrate.`
176
+ ? `use \`${target}\`. Run \`runlist lint --fix\` to migrate.`
177
177
  : `use a \`${plural}:\` YAML list. Remove the deprecated \`${singular}:\` block manually and preserve all of its values.`;
178
178
  doc.warnings.push({
179
179
  path: doc.path,
@@ -228,7 +228,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
228
228
  doc.errors.push({
229
229
  path: doc.path,
230
230
  level: 'error',
231
- message: `\`status: ${doc.status}\` but file is a direct child of \`${parentDir}/\`, not \`${parentDir}/${config.archiveDir}/\`. Run \`dotmd archive ${doc.path}\` to relocate to \`${expected}\`, or change the status.`,
231
+ message: `\`status: ${doc.status}\` but file is a direct child of \`${parentDir}/\`, not \`${parentDir}/${config.archiveDir}/\`. Run \`runlist archive ${doc.path}\` to relocate to \`${expected}\`, or change the status.`,
232
232
  });
233
233
  }
234
234
  }
@@ -244,8 +244,8 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
244
244
  const parentSegments = path.dirname(doc.path).split('/');
245
245
  if (parentSegments.includes(config.archiveDir)) {
246
246
  const heal = doc.type === 'prompt'
247
- ? `dotmd prompts archive ${doc.path}`
248
- : `dotmd set archived ${doc.path}`;
247
+ ? `runlist prompts archive ${doc.path}`
248
+ : `runlist set archived ${doc.path}`;
249
249
  doc.errors.push({
250
250
  path: doc.path,
251
251
  level: 'error',
@@ -492,7 +492,7 @@ export function checkCoordinationHubExecutionMode(docs, config) {
492
492
  warnings.push({
493
493
  path: doc.path,
494
494
  level: 'warning',
495
- message: `reads as a coordination runlist (slug \`${base}\`) but is missing \`execution_mode: coordination\`. Add it so \`dotmd plans\` / \`dotmd runlists\` reliably treat it as a runlist, not an active plan.`,
495
+ message: `reads as a coordination runlist (slug \`${base}\`) but is missing \`execution_mode: coordination\`. Add it so \`runlist plans\` / \`runlist runlists\` reliably treat it as a runlist, not an active plan.`,
496
496
  });
497
497
  }
498
498
  return warnings;
@@ -547,7 +547,7 @@ export function checkRoadmapHubExecutionMode(docs, config) {
547
547
  warnings.push({
548
548
  path: doc.path,
549
549
  level: 'warning',
550
- message: `is a coordination hub whose ${hubChildren} of ${resolved} \`related_plans:\` children are themselves runlists — that's structurally a tier-3 roadmap. Set \`execution_mode: roadmap\` so \`dotmd roadmap\` rolls their progress up (recursive done/total) instead of rendering it flat among them.`,
550
+ message: `is a coordination hub whose ${hubChildren} of ${resolved} \`related_plans:\` children are themselves runlists — that's structurally a tier-3 roadmap. Set \`execution_mode: roadmap\` so \`runlist roadmap\` rolls their progress up (recursive done/total) instead of rendering it flat among them.`,
551
551
  });
552
552
  }
553
553
  }
@@ -622,7 +622,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
622
622
  doc.warnings.push({
623
623
  path: doc.path,
624
624
  level: 'warning',
625
- message: `\`next_step\` is ${nextStep.length} chars (cap: 800). One mechanical fix: \`dotmd doctor --frontmatter-fix\` (moves the overflow into the body) — do NOT hand-trim or re-run check in a loop. Going forward, write next_step as a 1-2 sentence pointer; detail goes in the body.`,
625
+ message: `\`next_step\` is ${nextStep.length} chars (cap: 800). One mechanical fix: \`runlist doctor --frontmatter-fix\` (moves the overflow into the body) — do NOT hand-trim or re-run check in a loop. Going forward, write next_step as a 1-2 sentence pointer; detail goes in the body.`,
626
626
  });
627
627
  }
628
628
 
@@ -635,7 +635,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
635
635
  doc.warnings.push({
636
636
  path: doc.path,
637
637
  level: 'warning',
638
- message: `\`current_state\` is ${currentState.length} chars (cap: 1500). One mechanical fix: \`dotmd doctor --frontmatter-fix\` (moves the overflow into the body) — do NOT hand-trim or re-run check in a loop. Going forward, write current_state as a 2-4 sentence summary; detail goes in the body.`,
638
+ message: `\`current_state\` is ${currentState.length} chars (cap: 1500). One mechanical fix: \`runlist doctor --frontmatter-fix\` (moves the overflow into the body) — do NOT hand-trim or re-run check in a loop. Going forward, write current_state as a 2-4 sentence summary; detail goes in the body.`,
639
639
  });
640
640
  }
641
641
 
package/src/watch.mjs CHANGED
@@ -22,7 +22,7 @@ export function runWatch(argv, config) {
22
22
 
23
23
  // Clear terminal
24
24
  process.stdout.write('\x1b[2J\x1b[H');
25
- process.stderr.write(dim(`[${new Date().toLocaleTimeString()}] dotmd ${subCommand.join(' ')}`) + '\n\n');
25
+ process.stderr.write(dim(`[${new Date().toLocaleTimeString()}] runlist ${subCommand.join(' ')}`) + '\n\n');
26
26
 
27
27
  spawnSync(process.execPath, [cliPath, ...subCommand], {
28
28
  stdio: 'inherit',