dotmd-cli 0.78.0 → 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 (54) hide show
  1. package/README.md +66 -63
  2. package/assets/opencode/plugin.js +7 -6
  3. package/bin/dotmd.mjs +272 -285
  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 +7 -7
  8. package/src/check-collapse.mjs +5 -5
  9. package/src/claude-commands.mjs +6 -2
  10. package/src/commands.mjs +4 -4
  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 +8 -8
  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 +11 -11
  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 +12 -12
  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/validate.mjs +9 -9
  54. package/src/watch.mjs +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.78.0",
3
+ "version": "0.79.0",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "homepage": "https://github.com/reowens/dotmd#readme",
43
43
  "scripts": {
44
- "test": "node --test test/*.test.mjs",
44
+ "test": "node --import ./test/setup-env.mjs --test test/*.test.mjs",
45
45
  "postinstall": "node scripts/postinstall.mjs",
46
46
  "preversion": "node scripts/release-preflight.mjs && npm test",
47
47
  "version": "node scripts/prepare-version-commit.mjs",
@@ -36,17 +36,17 @@ try {
36
36
 
37
37
  if ((process.env.RUNLIST_AUTO_PLUGIN_UPDATE ?? process.env.DOTMD_AUTO_PLUGIN_UPDATE) === '1' && hasClaude) {
38
38
  spawnSync('claude', ['plugin', 'update', 'dotmd@dotmd'], { stdio: 'ignore', timeout: 60000 });
39
- process.stdout.write('dotmd: refreshed the Claude Code plugin — restart your session (or /reload-plugins) to apply.\n');
39
+ process.stdout.write('runlist: refreshed the Claude Code plugin — restart your session (or /reload-plugins) to apply.\n');
40
40
  } else {
41
41
  // The CLI just installed fresh, so only the plugin can be stale — point at
42
42
  // the targeted refresh rather than the full `dotmd update` (CLI + plugin).
43
43
  const nudge = hasClaude
44
- ? 'dotmd CLI installed. Using the Claude Code plugin? Run `dotmd update --plugin-only` to refresh it, then restart.'
45
- : 'dotmd CLI installed.';
44
+ ? 'runlist CLI installed. Using the Claude Code plugin? Run `runlist update --plugin-only` to refresh it, then restart.'
45
+ : 'runlist CLI installed.';
46
46
  process.stdout.write(`${nudge}\n`);
47
47
  }
48
48
  if (hasOpencode) {
49
- process.stdout.write('dotmd: OpenCode detected — run `dotmd install opencode` for per-session plan ownership and a session-start briefing.\n');
49
+ process.stdout.write('runlist: OpenCode detected — run `runlist install opencode` for per-session plan ownership and a session-start briefing.\n');
50
50
  }
51
51
  } catch {
52
52
  // Best effort only — never break the install.
@@ -203,7 +203,7 @@ function transactionRepairMessage(manifestPath, manifest, reason) {
203
203
  ...(manifest.gitIndex?.retainedPaths ?? []),
204
204
  ].filter(Boolean);
205
205
  return `${reason}\nTransaction recovery refused to guess. Manifest: ${manifestPath}\n` +
206
- `Start with \`dotmd doctor --transactions\` — it reports this transaction's state and resolves it when the canonical files agree on one generation.\n` +
206
+ `Start with \`runlist doctor --transactions\` — it reports this transaction's state and resolves it when the canonical files agree on one generation.\n` +
207
207
  `If it reports the generations as ambiguous, inspect the canonical files and recovery artifacts, then restore one complete generation and remove the manifest:\n` +
208
208
  `${artifacts.map(item => ` ${item}`).join('\n') || ' (no content artifacts)'}`;
209
209
  }
package/src/baton.mjs CHANGED
@@ -31,7 +31,7 @@ export function findOwnedPlan(config, index = null) {
31
31
  // next decision, and a prompt assembled from frontmatter reads like a handoff
32
32
  // while carrying nothing the plan doesn't already say.
33
33
  const BODY_USAGE = `Nothing saved: baton needs the resume you wrote, passed as @<file> or - (stdin).
34
- dotmd baton [<plan-or-slug>] @/tmp/draft.md`;
34
+ runlist baton [<plan-or-slug>] @/tmp/draft.md`;
35
35
 
36
36
  // A handoff that lands beside a pending one leaves two prompts for the same
37
37
  // work, and the next session picks whichever sorts first. Baton used to step to
@@ -60,7 +60,7 @@ function pendingHandoffs(promptPath, planPath, config) {
60
60
  function refusePendingHandoff(pending) {
61
61
  const lines = pending.map(p => ` ${p}`).join('\n');
62
62
  const slug = path.basename(pending[0], '.md');
63
- die(`Nothing saved: a handoff for this is already pending:\n${lines}\nUse it (\`dotmd use ${slug}\`) or archive it (\`dotmd prompts archive ${pending[0]}\`), then run baton again.`);
63
+ die(`Nothing saved: a handoff for this is already pending:\n${lines}\nUse it (\`runlist use ${slug}\`) or archive it (\`runlist prompts archive ${pending[0]}\`), then run baton again.`);
64
64
  }
65
65
 
66
66
  // Is this positional a filesystem reference (must resolve, typos die) or a
@@ -87,7 +87,7 @@ export async function runBaton(argv, config, opts = {}) {
87
87
  if (a === '--force') { force = true; continue; }
88
88
  if (a === '--json') continue;
89
89
  if (!a.startsWith('-') || a === '-' || a.startsWith('@')) { positionals.push(a); continue; }
90
- die(`Unknown flag for \`dotmd baton\`: ${a}`);
90
+ die(`Unknown flag for \`runlist baton\`: ${a}`);
91
91
  }
92
92
 
93
93
  let planArg = null;
@@ -138,10 +138,10 @@ export async function runBaton(argv, config, opts = {}) {
138
138
  if (owned.plan) {
139
139
  planPath = path.resolve(config.repoRoot, owned.plan.path);
140
140
  } else if (owned.owned?.length > 1) {
141
- die(`Multiple plans are owned by this session; pass one explicitly:\n${owned.owned.map(d => ' dotmd baton ' + d.path + ' @/tmp/draft.md').join('\n')}`);
141
+ die(`Multiple plans are owned by this session; pass one explicitly:\n${owned.owned.map(d => ' runlist baton ' + d.path + ' @/tmp/draft.md').join('\n')}`);
142
142
  } else {
143
143
  const diagnostics = owned.diagnostics?.length ? `\nIgnored ownership records:\n${owned.diagnostics.map(d => ` ${d}`).join('\n')}` : '';
144
- die(`No valid in-session plan is owned by this session, so baton needs a name for the resume prompt:\n dotmd baton <slug> @/tmp/draft.md # saves resume-<slug>, touches nothing else\nHanding off a specific plan? dotmd baton <plan-file> @/tmp/draft.md${diagnostics}`);
144
+ die(`No valid in-session plan is owned by this session, so baton needs a name for the resume prompt:\n runlist baton <slug> @/tmp/draft.md # saves resume-<slug>, touches nothing else\nHanding off a specific plan? runlist baton <plan-file> @/tmp/draft.md${diagnostics}`);
145
145
  }
146
146
  }
147
147
 
@@ -162,7 +162,7 @@ export async function runBaton(argv, config, opts = {}) {
162
162
  const raw = readFileSync(planPath, 'utf8');
163
163
  const { frontmatter: fmRaw } = extractFrontmatter(raw);
164
164
  if (!fmRaw) {
165
- die(`${repoPath} has no frontmatter block — baton can't flip its status.\nFix the doc first (\`dotmd bulk-tag ${repoPath} --type plan --status in-session\`), or save the prompt without a status flip: dotmd baton ${path.basename(planPath, '.md')} @/tmp/draft.md`);
165
+ die(`${repoPath} has no frontmatter block — baton can't flip its status.\nFix the doc first (\`runlist bulk-tag ${repoPath} --type plan --status in-session\`), or save the prompt without a status flip: runlist baton ${path.basename(planPath, '.md')} @/tmp/draft.md`);
166
166
  }
167
167
  const fm = parseSimpleFrontmatter(fmRaw);
168
168
  const docType = asString(fm.type);
@@ -176,7 +176,7 @@ export async function runBaton(argv, config, opts = {}) {
176
176
  die(`Invalid status \`${status}\` for type \`${docType ?? 'plan'}\`\nValid: ${[...validStatuses].join(', ')}`);
177
177
  }
178
178
  if (status === 'in-session') {
179
- die('`dotmd baton --status in-session` contradicts baton release semantics. Choose active/paused/awaiting/partial/blocked.');
179
+ die('`runlist baton --status in-session` contradicts baton release semantics. Choose active/paused/awaiting/partial/blocked.');
180
180
  }
181
181
  assertPlanMutationAuthorized(repoPath, config, { sessionId: authoritativeSessionId(), force });
182
182
  // Before the plan-completion step: a refusal must leave nothing changed.
@@ -14,19 +14,19 @@ const CATEGORIES = [
14
14
  key: 'updated-behind-git',
15
15
  match: /^frontmatter `updated:.*` is behind git history/,
16
16
  label: 'docs have `updated` behind git history',
17
- fix: 'dotmd touch --git',
17
+ fix: 'runlist touch --git',
18
18
  },
19
19
  {
20
20
  key: 'singular-module',
21
- match: /^`module:` \(singular\) is deprecated.*Run `dotmd lint --fix`/,
21
+ match: /^`module:` \(singular\) is deprecated.*Run `runlist lint --fix`/,
22
22
  label: 'docs use deprecated singular `module:`',
23
- fix: 'dotmd lint --fix',
23
+ fix: 'runlist lint --fix',
24
24
  },
25
25
  {
26
26
  key: 'singular-surface',
27
- match: /^`surface:` \(singular\) is deprecated.*Run `dotmd lint --fix`/,
27
+ match: /^`surface:` \(singular\) is deprecated.*Run `runlist lint --fix`/,
28
28
  label: 'docs use deprecated singular `surface:`',
29
- fix: 'dotmd lint --fix',
29
+ fix: 'runlist lint --fix',
30
30
  },
31
31
  ];
32
32
 
@@ -16,7 +16,10 @@ import path from 'node:path';
16
16
  // NEVER touched. Every dotmd-stamped file is fair game, including legacy ones
17
17
  // dotmd no longer generates (e.g. the old baton.md).
18
18
 
19
- const GENERATED_MARKER = '<!-- dotmd-generated:';
19
+ // Nothing writes these files any more, so only the legacy banner can exist in
20
+ // the wild; the current spelling is accepted too so a banner-gated teardown
21
+ // never depends on which name stamped the file.
22
+ const GENERATED_MARKERS = ['<!-- runlist-generated:', '<!-- dotmd-generated:'];
20
23
 
21
24
  // The marker sits just below the YAML frontmatter Claude Code surfaces as the
22
25
  // command description. That description can be long (the retired plans.md baked
@@ -25,7 +28,8 @@ const GENERATED_MARKER = '<!-- dotmd-generated:';
25
28
  // are tiny command files, so reading them in full is cheap.
26
29
  function isGeneratedCommandFile(filePath) {
27
30
  try {
28
- return readFileSync(filePath, 'utf8').includes(GENERATED_MARKER);
31
+ const contents = readFileSync(filePath, 'utf8');
32
+ return GENERATED_MARKERS.some(marker => contents.includes(marker));
29
33
  } catch {
30
34
  return false;
31
35
  }
package/src/commands.mjs CHANGED
@@ -244,7 +244,7 @@ export function commandCompletionWords(name) {
244
244
  export function commandUsage(name) {
245
245
  const definition = commandDefinition(name);
246
246
  if (!definition) return null;
247
- return definition.forms.map(commandForm => `dotmd ${definition.name}${commandForm.syntax ? ` ${commandForm.syntax}` : ''}`).join('\n');
247
+ return definition.forms.map(commandForm => `runlist ${definition.name}${commandForm.syntax ? ` ${commandForm.syntax}` : ''}`).join('\n');
248
248
  }
249
249
 
250
250
  function optionMap(forms) {
@@ -267,12 +267,12 @@ function scanArgs(name, argv, options, passthrough = false, dashPositionalsAfter
267
267
  if (!option) {
268
268
  if (passthrough) continue;
269
269
  if (positional.length >= dashPositionalsAfter) { positional.push(arg); continue; }
270
- throw new Error(`Unknown flag for \`dotmd ${name}\`: ${arg}`);
270
+ throw new Error(`Unknown flag for \`runlist ${name}\`: ${arg}`);
271
271
  }
272
272
  if (option.arity === 1) {
273
273
  const next = argv[i + 1];
274
274
  if (next === undefined || options.has(next)) {
275
- throw new Error(`Missing value for \`${arg}\` in \`dotmd ${name}\`.`);
275
+ throw new Error(`Missing value for \`${arg}\` in \`runlist ${name}\`.`);
276
276
  }
277
277
  i += 1;
278
278
  } else if (option.arity === 'optional' && argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) {
@@ -317,7 +317,7 @@ export function validateCommandArgs(name, argv, { preset = false } = {}) {
317
317
  const preliminary = scanArgs(canonical, normalized, allOptions, allPassthrough, dashPositionalsAfter);
318
318
  const selected = definition.forms.find(commandForm => commandForm.subcommands.includes(preliminary[0]))
319
319
  ?? definition.forms.find(commandForm => commandForm.subcommands.length === 0);
320
- if (!selected) throw new Error(`Unknown subcommand for \`dotmd ${canonical}\`: ${preliminary[0] ?? '(missing)'}`);
320
+ if (!selected) throw new Error(`Unknown subcommand for \`runlist ${canonical}\`: ${preliminary[0] ?? '(missing)'}`);
321
321
  const selectedOptions = optionMap([selected]);
322
322
  const positional = scanArgs(canonical, normalized, selectedOptions, selected.passthrough, selected.dashPositionalsAfter);
323
323
  const args = selected.subcommands.includes(positional[0]) ? positional.slice(1) : positional;
package/src/config.mjs CHANGED
@@ -218,10 +218,10 @@ function normalizeRichStatuses(config, userConfig) {
218
218
  const skipStaleEffective = p.skipStale === true || quietImpliesSkipStale;
219
219
  const skipWarningsEffective = p.skipWarnings === true || quietImpliesSkipWarnings;
220
220
  if (skipStaleEffective && p.staleDays != null) {
221
- warn(`dotmd config: status "${typeName}.${name}" has skipStale: true and staleDays: ${p.staleDays} — staleDays is ignored. Drop one to silence this warning.`);
221
+ warn(`runlist config: status "${typeName}.${name}" has skipStale: true and staleDays: ${p.staleDays} — staleDays is ignored. Drop one to silence this warning.`);
222
222
  }
223
223
  if (skipWarningsEffective && p.requiresModule) {
224
- warn(`dotmd config: status "${typeName}.${name}" has skipWarnings: true and requiresModule: true — the module requirement can never fire. Drop one to silence this warning.`);
224
+ warn(`runlist config: status "${typeName}.${name}" has skipWarnings: true and requiresModule: true — the module requirement can never fire. Drop one to silence this warning.`);
225
225
  }
226
226
 
227
227
  if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
package/src/deps.mjs CHANGED
@@ -252,7 +252,7 @@ function renderFlatJson(graph, forwardMap, reverseMap, docByPath) {
252
252
  export function runUnblocks(argv, config) {
253
253
  const input = argv.find(a => !a.startsWith('-'));
254
254
  const json = argv.includes('--json');
255
- if (!input) die('Usage: dotmd unblocks <file>');
255
+ if (!input) die('Usage: runlist unblocks <file>');
256
256
 
257
257
  const filePath = resolveDocArg(input, config);
258
258
  const repoPath = toRepoPath(filePath, config.repoRoot);
package/src/doctor.mjs CHANGED
@@ -99,7 +99,7 @@ function runDoctorTransactions(argv, config, opts = {}) {
99
99
  }
100
100
  if (resolvable.length) {
101
101
  process.stdout.write(`\n${resolvable.length} resolvable — the canonical files already agree on one generation.\n`);
102
- process.stdout.write(dim('Run `dotmd doctor --transactions --apply` to clear them (no document content is touched).\n'));
102
+ process.stdout.write(dim('Run `runlist doctor --transactions --apply` to clear them (no document content is touched).\n'));
103
103
  }
104
104
  const stuck = report.filter(item => !item.resolvable && !clearedIds.has(item.id) && (item.status === 'failed-manual' || !item.readable));
105
105
  if (stuck.length) {
@@ -152,7 +152,7 @@ async function runDoctorClaims(argv, config, opts = {}) {
152
152
  // status into. Deciding by existence here rather than by catching
153
153
  // runSet's "File not found" keeps the bypass narrow and explicit.
154
154
  if (existsSync(path.resolve(config.repoRoot, claim.plan))) {
155
- await runSet(['active', claim.plan], config, { force: true, sessionId: operator, note: 'Claim released by `dotmd doctor --claims` — the owning session was gone.' });
155
+ await runSet(['active', claim.plan], config, { force: true, sessionId: operator, note: 'Claim released by `runlist doctor --claims` — the owning session was gone.' });
156
156
  } else {
157
157
  releaseVanishedPlanClaim(claim, config);
158
158
  claim.vanished = true;
@@ -201,13 +201,13 @@ async function runDoctorClaims(argv, config, opts = {}) {
201
201
  const pendingDead = dead.filter(claim => !releasedSet.has(claim.plan));
202
202
  if (pendingDead.length) {
203
203
  process.stdout.write(yellow(`\n${pendingDead.length} held by a session whose process is gone.\n`));
204
- process.stdout.write(dim('Run `dotmd doctor --claims --apply` to release them.\n'));
204
+ process.stdout.write(dim('Run `runlist doctor --claims --apply` to release them.\n'));
205
205
  }
206
206
  const unjudgeable = claims.filter(claim =>
207
207
  !claim.corrupt && claim.liveness !== 'dead' && !releasedSet.has(claim.plan));
208
208
  if (unjudgeable.length && olderThanMs === null) {
209
209
  process.stdout.write(dim(`\n${unjudgeable.length} cannot be judged from here — no owning process was recorded, or it is on another machine.\n`));
210
- process.stdout.write(dim('If you know those sessions are over: `dotmd doctor --claims --apply --older-than 24h`.\n'));
210
+ process.stdout.write(dim('If you know those sessions are over: `runlist doctor --claims --apply --older-than 24h`.\n'));
211
211
  }
212
212
  }
213
213
 
@@ -228,10 +228,10 @@ function runDoctorSession(argv) {
228
228
  for (const line of identity.advice) process.stdout.write(` → ${line}\n`);
229
229
 
230
230
  process.stdout.write('\n' + bold('Host integration\n'));
231
- if (oc.foreign) process.stdout.write(` ${yellow('!')} opencode: a dotmd.js dotmd did not write — ${oc.path}\n`);
231
+ if (oc.foreign) process.stdout.write(` ${yellow('!')} opencode: a dotmd.js runlist did not write — ${oc.path}\n`);
232
232
  else if (!oc.exists) process.stdout.write(` ${dim('·')} opencode: not installed — ${dim(oc.path)}\n`);
233
- else process.stdout.write(` ${oc.stale ? yellow('!') : green('✓')} opencode: ${oc.version}${oc.stale ? ` (CLI is ${dotmdVersion()} — run \`dotmd update\`)` : ''}\n`);
234
- process.stdout.write(dim(' Claude Code ships as a plugin — `dotmd install` reports both hosts.\n'));
233
+ else process.stdout.write(` ${oc.stale ? yellow('!') : green('✓')} opencode: ${oc.version}${oc.stale ? ` (CLI is ${dotmdVersion()} — run \`runlist update\`)` : ''}\n`);
234
+ process.stdout.write(dim(' Claude Code ships as a plugin — `runlist install` reports both hosts.\n'));
235
235
  }
236
236
 
237
237
  export function runDoctor(argv, config, opts = {}) {
@@ -273,7 +273,7 @@ export function runDoctor(argv, config, opts = {}) {
273
273
  const modeNote = dryRun
274
274
  ? dim('[preview — run with --apply to write]')
275
275
  : dim('[applying changes]');
276
- process.stdout.write(bold('dotmd doctor') + ' ' + modeNote + '\n\n');
276
+ process.stdout.write(bold('runlist doctor') + ' ' + modeNote + '\n\n');
277
277
  if (dryRun) {
278
278
  const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot', 'renderCheck']
279
279
  .filter(name => typeof config.hooks?.[name] === 'function');
@@ -335,7 +335,7 @@ export function runDoctor(argv, config, opts = {}) {
335
335
  process.stdout.write('[dry-run] No retired slash-command files to remove.\n');
336
336
  } else {
337
337
  for (const r of wouldRemove) {
338
- process.stdout.write(`[dry-run] Would remove retired .claude/commands/${r.name} (guidance now ships via the dotmd plugin).\n`);
338
+ process.stdout.write(`[dry-run] Would remove retired .claude/commands/${r.name} (guidance now ships via the runlist plugin).\n`);
339
339
  }
340
340
  }
341
341
  } else {
@@ -344,7 +344,7 @@ export function runDoctor(argv, config, opts = {}) {
344
344
  process.stdout.write('Nothing to clean up.\n');
345
345
  } else {
346
346
  for (const r of removed) {
347
- process.stdout.write(`${green('Removed')} retired .claude/commands/${r.name} (guidance now ships via the dotmd plugin)\n`);
347
+ process.stdout.write(`${green('Removed')} retired .claude/commands/${r.name} (guidance now ships via the runlist plugin)\n`);
348
348
  }
349
349
  }
350
350
  }
@@ -369,7 +369,7 @@ export function runDoctor(argv, config, opts = {}) {
369
369
  process.stdout.write('\n' + bold('Session identity') + '\n');
370
370
  process.stdout.write(`${yellow('!')} ${identity.summary}\n`);
371
371
  for (const line of identity.advice) process.stdout.write(dim(` → ${line}\n`));
372
- process.stdout.write(dim(' `dotmd doctor --session` for the full picture.\n'));
372
+ process.stdout.write(dim(' `runlist doctor --session` for the full picture.\n'));
373
373
  }
374
374
  }
375
375
 
@@ -387,7 +387,7 @@ function findDeprecatedCommandMentions(config) {
387
387
  for (const filePath of docs) {
388
388
  let raw = '';
389
389
  try { raw = readFileSync(filePath, 'utf8'); } catch { continue; }
390
- if (/\bdotmd status\b/.test(raw) || /\bdotmd (pickup|unpickup|release|finish)\b/.test(raw)) {
390
+ if (/\b(?:dotmd|runlist|rl) status\b/.test(raw) || /\b(?:dotmd|runlist|rl) (pickup|unpickup|release|finish)\b/.test(raw)) {
391
391
  matches.push(toRepoPath(filePath, config.repoRoot));
392
392
  }
393
393
  }
@@ -445,7 +445,7 @@ function runDoctorProject(config, { json = false } = {}) {
445
445
  return result;
446
446
  }
447
447
 
448
- process.stdout.write(bold('dotmd doctor --project') + '\n\n');
448
+ process.stdout.write(bold('runlist doctor --project') + '\n\n');
449
449
  process.stdout.write(`- running CLI version: ${result.cliVersion ?? 'unknown'}\n`);
450
450
  process.stdout.write(`- package dependency: ${result.packageDependency ?? '(none found)'}\n`);
451
451
  process.stdout.write(`- stale Claude commands: ${claudeCommandWarnings.length}\n`);
@@ -456,18 +456,18 @@ function runDoctorProject(config, { json = false } = {}) {
456
456
  process.stdout.write('- docs mentioning deprecated commands: 0\n');
457
457
  }
458
458
  if (docsWithoutFrontmatter.length) {
459
- process.stdout.write(yellow(`- docs without a frontmatter block: ${docsWithoutFrontmatter.length} — every status verb (\`set\`, \`archive\`, \`baton\`) dies on these. Fix: dotmd bulk-tag <file> --type <type> --status <status>`) + '\n');
459
+ process.stdout.write(yellow(`- docs without a frontmatter block: ${docsWithoutFrontmatter.length} — every status verb (\`set\`, \`archive\`, \`baton\`) dies on these. Fix: runlist bulk-tag <file> --type <type> --status <status>`) + '\n');
460
460
  for (const file of docsWithoutFrontmatter.slice(0, 10)) process.stdout.write(` - ${file}\n`);
461
461
  } else {
462
462
  process.stdout.write('- docs without a frontmatter block: 0\n');
463
463
  }
464
464
  if (planStatusGaps.length) {
465
- process.stdout.write(yellow(`- plan status vocab missing: ${planStatusGaps.join(', ')} — \`dotmd use\` and \`dotmd baton\` depend on these; add them to types.plan.statuses in dotmd.config.mjs`) + '\n');
465
+ process.stdout.write(yellow(`- plan status vocab missing: ${planStatusGaps.join(', ')} — \`runlist use\` and \`runlist baton\` depend on these; add them to types.plan.statuses in runlist.config.mjs`) + '\n');
466
466
  } else {
467
467
  process.stdout.write('- plan status vocab: ok\n');
468
468
  }
469
469
  if (skillDriftWarnings.length) {
470
- process.stdout.write(yellow(`- canonical workflow block: drifted — CLAUDE.md and the plugin SKILL.md teach different workflows. Reconcile the block between the \`dotmd:canonical-workflow\` markers in both files.`) + '\n');
470
+ process.stdout.write(yellow(`- canonical workflow block: drifted — CLAUDE.md and the plugin SKILL.md teach different workflows. Reconcile the block between the \`runlist:canonical-workflow\` markers in both files.`) + '\n');
471
471
  } else {
472
472
  process.stdout.write('- canonical workflow block: in sync\n');
473
473
  }
@@ -559,7 +559,7 @@ function runDoctorStatuses(config, { json = false } = {}) {
559
559
  return;
560
560
  }
561
561
 
562
- process.stdout.write(bold('dotmd doctor --statuses') + '\n\n');
562
+ process.stdout.write(bold('runlist doctor --statuses') + '\n\n');
563
563
 
564
564
  if (suggestions.length === 0) {
565
565
  process.stdout.write(`No overloaded status buckets detected (min bucket size: ${MIN_BUCKET_SIZE}).\n`);
@@ -21,7 +21,7 @@ function resolveHubArgs(args, docs) {
21
21
  const [match] = matches;
22
22
  if (!isHubDoc(match)) {
23
23
  die(`${match.path} is not a hub — it has no \`runlist:\` and no \`execution_mode: coordination|roadmap\`. `
24
- + 'Run `dotmd fix-membership` with no argument to sweep every hub.');
24
+ + 'Run `runlist fix-membership` with no argument to sweep every hub.');
25
25
  }
26
26
  paths.add(match.path);
27
27
  }
@@ -61,7 +61,7 @@ export function runFrontmatterFix(config, opts = {}) {
61
61
 
62
62
  const prefix = dryRun ? dim('[dry-run] ') : '';
63
63
  const banner = dryRun ? dim(' [preview — run without --dry-run to write]') : '';
64
- out.write(bold('dotmd doctor --frontmatter-fix') + banner + '\n\n');
64
+ out.write(bold('runlist doctor --frontmatter-fix') + banner + '\n\n');
65
65
 
66
66
  if (results.length === 0) {
67
67
  out.write(green('No over-cap fields found.') + '\n');
package/src/git.mjs CHANGED
@@ -36,7 +36,7 @@ function ensureGit() {
36
36
  if (gitChecked) return;
37
37
  const result = spawnSync('git', ['--version'], { encoding: 'utf8' });
38
38
  if (result.error) {
39
- throw new Error('git is not installed or not found in PATH. dotmd requires git for this operation.');
39
+ throw new Error('git is not installed or not found in PATH. runlist requires git for this operation.');
40
40
  }
41
41
  gitChecked = true;
42
42
  }
package/src/glossary.mjs CHANGED
@@ -183,9 +183,9 @@ export function runGlossary(argv, config) {
183
183
  const term = argv.find(a => !a.startsWith('-'));
184
184
 
185
185
  const result = loadGlossary(config);
186
- if (!result) die('No glossary configured. Add glossary: { path, section } to your dotmd config.');
186
+ if (!result) die('No glossary configured. Add glossary: { path, section } to your runlist config.');
187
187
  if (!result.found) {
188
- die(`Glossary section "## ${result.section}" not found in ${result.path}. Add the section, or update glossary.section in dotmd.config.mjs.`);
188
+ die(`Glossary section "## ${result.section}" not found in ${result.path}. Add the section, or update glossary.section in runlist.config.mjs.`);
189
189
  }
190
190
  if (result.entries.length === 0) {
191
191
  die(`Glossary section "## ${result.section}" found in ${result.path} but contains no recognizable entries (expected markdown table or schema→UI bullets).`);
@@ -212,7 +212,7 @@ export function runGlossary(argv, config) {
212
212
  return;
213
213
  }
214
214
 
215
- if (!term) die('Usage: dotmd glossary <term> | --list | --json');
215
+ if (!term) die('Usage: runlist glossary <term> | --list | --json');
216
216
 
217
217
  const matches = matchTerm(term, entries);
218
218
 
package/src/graph.mjs CHANGED
@@ -208,7 +208,7 @@ function _renderGraphText(graph, config) {
208
208
  export function renderGraphDot(graph, config) {
209
209
  const { nodes, edges } = graph;
210
210
  const lines = [];
211
- lines.push('digraph dotmd {');
211
+ lines.push('digraph runlist {');
212
212
  lines.push(' rankdir=LR;');
213
213
  lines.push(' node [shape=box, style="rounded,filled", fontname="Helvetica"];');
214
214
  lines.push('');
package/src/guard.mjs CHANGED
@@ -269,9 +269,9 @@ function evalBash(command, config, inspectGitPaths, baseCwd) {
269
269
  rule: 'commit-prompt',
270
270
  detail: `git ${git.subcommand} ${targets.join(' ')}`,
271
271
  reason:
272
- `Saved prompts (${targets.join(', ')}) are session-local dotmd artifacts, not source to commit. ` +
272
+ `Saved prompts (${targets.join(', ')}) are session-local runlist artifacts, not source to commit. ` +
273
273
  `Don't git add/commit them — commit your other changes without the prompt in the pathspec. ` +
274
- `The next session consumes a prompt with \`dotmd use <file>\` (or \`dotmd use\` for the oldest pending), which prints the body and archives it atomically.`,
274
+ `The next session consumes a prompt with \`runlist use <file>\` (or \`runlist use\` for the oldest pending), which prints the body and archives it atomically.`,
275
275
  };
276
276
  }
277
277
  }
@@ -283,9 +283,9 @@ function evalBash(command, config, inspectGitPaths, baseCwd) {
283
283
  rule: 'cat-prompt',
284
284
  detail: `${cmd0} ${promptTokens.join(' ')}`,
285
285
  reason:
286
- `${promptTokens.join(', ')} is a saved dotmd prompt. To start work from it, run \`dotmd use ${promptTokens[0]}\` — ` +
286
+ `${promptTokens.join(', ')} is a saved runlist prompt. To start work from it, run \`runlist use ${promptTokens[0]}\` — ` +
287
287
  `it commits archive/claim before at-most-once body output (prevents double-consumption). ` +
288
- `Just peeking or triaging (not consuming)? \`dotmd prompts show ${promptTokens[0]}\` reads it without archiving. Don't \`${cmd0}\` it directly.`,
288
+ `Just peeking or triaging (not consuming)? \`runlist prompts show ${promptTokens[0]}\` reads it without archiving. Don't \`${cmd0}\` it directly.`,
289
289
  };
290
290
  }
291
291
 
@@ -308,9 +308,9 @@ function evalRead(filePath, config) {
308
308
  rule: 'read-prompt',
309
309
  detail: filePath,
310
310
  reason:
311
- `${filePath} is a saved dotmd prompt. To start work from it, run \`dotmd use ${filePath}\` — it commits archive/claim before at-most-once body output so it can't be double-consumed. ` +
312
- `Just peeking or triaging (not consuming)? \`dotmd prompts show ${filePath}\` reads it without archiving. ` +
313
- `Surveying the whole queue? \`dotmd prompts show --all\` peeks every pending prompt in one call — don't Read them file by file.`,
311
+ `${filePath} is a saved runlist prompt. To start work from it, run \`runlist use ${filePath}\` — it commits archive/claim before at-most-once body output so it can't be double-consumed. ` +
312
+ `Just peeking or triaging (not consuming)? \`runlist prompts show ${filePath}\` reads it without archiving. ` +
313
+ `Surveying the whole queue? \`runlist prompts show --all\` peeks every pending prompt in one call — don't Read them file by file.`,
314
314
  };
315
315
  }
316
316
 
@@ -396,7 +396,7 @@ function emit(result) {
396
396
  hookSpecificOutput.permissionDecisionReason = result.reason;
397
397
  } else {
398
398
  // warn — allow the call but teach the agent the dotmd-native path.
399
- hookSpecificOutput.additionalContext = `[dotmd] ${result.reason}`;
399
+ hookSpecificOutput.additionalContext = `[runlist] ${result.reason}`;
400
400
  }
401
401
  process.stdout.write(JSON.stringify({ hookSpecificOutput }) + '\n');
402
402
  }
package/src/health.mjs CHANGED
@@ -123,7 +123,7 @@ export function runHealth(argv, config) {
123
123
 
124
124
  // Roadmaps (tier-3) — pinned above Runlists with the recursive grand total.
125
125
  if (roadmapHubs.length > 0) {
126
- process.stdout.write(`${bold('Roadmaps:')} ${roadmapHubs.length} ${dim('· dotmd roadmap')}\n`);
126
+ process.stdout.write(`${bold('Roadmaps:')} ${roadmapHubs.length} ${dim('· runlist roadmap')}\n`);
127
127
  for (const doc of roadmapHubs.slice(0, 8)) {
128
128
  const slug = hubLabel(doc).padEnd(28);
129
129
  const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : '?d';
@@ -142,7 +142,7 @@ export function runHealth(argv, config) {
142
142
  // surfaced as their own tally so they don't inflate the active count. Newest
143
143
  // first, mirroring `dotmd runlists`; capped with a "more" footer.
144
144
  if (runlistHubs.length > 0) {
145
- process.stdout.write(`${bold('Runlists:')} ${runlistHubs.length} ${dim('· dotmd runlists')}\n`);
145
+ process.stdout.write(`${bold('Runlists:')} ${runlistHubs.length} ${dim('· runlist runlists')}\n`);
146
146
  for (const doc of runlistHubs.slice(0, 8)) {
147
147
  const slug = hubLabel(doc).padEnd(28);
148
148
  const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : '?d';
package/src/hints.mjs CHANGED
@@ -17,32 +17,32 @@ const TEMPLATES = [
17
17
  {
18
18
  match: /Too many arguments|Usage:/i,
19
19
  hint: ({ count, argv }) =>
20
- `${count}× the same shape on \`${argv[0]} ${argv[1] ?? ''}\` in this session. Run \`dotmd ${argv[0]} --help\` for the expected positional args.`,
20
+ `${count}× the same shape on \`${argv[0]} ${argv[1] ?? ''}\` in this session. Run \`runlist ${argv[0]} --help\` for the expected positional args.`,
21
21
  },
22
22
  {
23
23
  match: /Already (consumed|archived)/i,
24
24
  hint: ({ count, argv }) =>
25
- `${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
+ `${count}× attempts to use a path that is already archived. Use \`runlist prompts list\` to see what is actually pending, or \`runlist next\` for the oldest live prompt.`,
26
26
  },
27
27
  {
28
28
  match: /No pending prompts/i,
29
29
  hint: ({ count }) =>
30
- `${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
+ `${count}× \`runlist next\` with no pending prompts in the queue. Either queue one with \`runlist new prompt <slug> "..."\` or pass an explicit prompt file to \`runlist use\`.`,
31
31
  },
32
32
  {
33
33
  match: /Unknown command/i,
34
34
  hint: ({ count }) =>
35
- `${count}× the same unknown command. Run \`dotmd --help\` to list available commands; the dispatch already prints a did-you-mean for close misses.`,
35
+ `${count}× the same unknown command. Run \`runlist --help\` to list available commands; the dispatch already prints a did-you-mean for close misses.`,
36
36
  },
37
37
  {
38
38
  match: /File not found|does not resolve/i,
39
39
  hint: ({ count, argv }) =>
40
- `${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
+ `${count}× pointing at a path that doesn't exist. Confirm the file with \`runlist query\` or \`runlist plans\` — paths resolve relative to repo root or doc roots, not the cwd.`,
41
41
  },
42
42
  {
43
43
  match: /Unknown status|Unknown surface/i,
44
44
  hint: ({ count }) =>
45
- `${count}× rejected by the taxonomy validator. Run \`dotmd statuses list\` or \`dotmd surfaces\` to print the valid values for this project.`,
45
+ `${count}× rejected by the taxonomy validator. Run \`runlist statuses list\` or \`runlist surfaces\` to print the valid values for this project.`,
46
46
  },
47
47
  ];
48
48
 
@@ -122,7 +122,7 @@ export function findRepeatFailureHint(failingArgv, config) {
122
122
  }
123
123
  }
124
124
 
125
- 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.`;
125
+ return `${count}× the same failing shape on \`${failingArgv[0]}\` in this session (last attempt ${ageMin}m ago). Check the args — \`runlist ${failingArgv[0]} --help\` shows what's expected.`;
126
126
  } catch {
127
127
  return null;
128
128
  }