dotmd-cli 0.78.0 → 0.79.1

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 +11 -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 +4 -2
  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
@@ -25,7 +25,13 @@ import { hostSessionSource } from './util.mjs';
25
25
  import { readEnv, stateDir } from './naming.mjs';
26
26
  import { planMarketplaceRepair } from './update.mjs';
27
27
 
28
- export const GENERATED_MARKER = 'dotmd-generated:';
28
+ // New files carry the current banner. Every reader accepts both: files written
29
+ // by dotmd-cli 0.78.0 and earlier carry the legacy banner, and those must stay
30
+ // recognizable as generated forever, or an old install turns into a "foreign"
31
+ // file that install/update/remove refuse to touch.
32
+ export const GENERATED_MARKER = 'runlist-generated:';
33
+ export const LEGACY_GENERATED_MARKER = 'dotmd-generated:';
34
+ const GENERATED_MARKERS = [GENERATED_MARKER, LEGACY_GENERATED_MARKER];
29
35
  const PLUGIN_FILENAME = 'dotmd.js';
30
36
  const ASSET = path.resolve(fileURLToPath(import.meta.url), '..', '..', 'assets', 'opencode', 'plugin.js');
31
37
 
@@ -58,8 +64,8 @@ export function opencodePluginPath(opts = {}) {
58
64
  function banner(version) {
59
65
  return [
60
66
  `// ${GENERATED_MARKER}${version}`,
61
- '// Generated by `dotmd install opencode`. Refreshed by `dotmd update`.',
62
- '// Hand edits are overwritten — remove with `dotmd install opencode --remove`.',
67
+ '// Generated by `runlist install opencode`. Refreshed by `runlist update`.',
68
+ '// Hand edits are overwritten — remove with `runlist install opencode --remove`.',
63
69
  '',
64
70
  ].join('\n');
65
71
  }
@@ -68,31 +74,44 @@ export function renderOpencodePlugin(version) {
68
74
  return banner(version) + readFileSync(ASSET, 'utf8');
69
75
  }
70
76
 
71
- // The version a generated file was written by, or null when the file is absent
72
- // or carries no dotmd banner. A file without the banner is the user's own and
73
- // is never written or removed — same rule the retired slash-command scaffolding
74
- // followed.
75
- export function installedVersion(filePath) {
77
+ // The banner a generated file carries — { marker, version } — or null when the
78
+ // file is absent or carries neither banner. A file without a banner is the
79
+ // user's own and is never written or removed — same rule the retired
80
+ // slash-command scaffolding followed. The earliest banner in the file wins.
81
+ export function readGeneratedBanner(filePath) {
76
82
  let contents;
77
83
  try { contents = readFileSync(filePath, 'utf8'); } catch { return null; }
78
- const marker = contents.indexOf(GENERATED_MARKER);
79
- if (marker < 0) return null;
80
- const rest = contents.slice(marker + GENERATED_MARKER.length);
81
- return rest.split('\n', 1)[0].trim() || null;
84
+ let found = null;
85
+ for (const marker of GENERATED_MARKERS) {
86
+ const at = contents.indexOf(marker);
87
+ if (at >= 0 && (found === null || at < found.at)) found = { marker, at };
88
+ }
89
+ if (!found) return null;
90
+ const rest = contents.slice(found.at + found.marker.length);
91
+ return { marker: found.marker, version: rest.split('\n', 1)[0].trim() || null };
92
+ }
93
+
94
+ export function installedVersion(filePath) {
95
+ return readGeneratedBanner(filePath)?.version ?? null;
82
96
  }
83
97
 
84
98
  export function opencodeStatus(opts = {}) {
85
99
  const { version, ...rest } = opts;
86
100
  const filePath = opencodePluginPath(rest);
87
101
  const exists = existsSync(filePath);
88
- const installed = exists ? installedVersion(filePath) : null;
102
+ const generated = exists ? readGeneratedBanner(filePath) : null;
103
+ const installed = generated?.version ?? null;
104
+ const legacyBanner = generated?.marker === LEGACY_GENERATED_MARKER;
89
105
  return {
90
106
  path: filePath,
91
107
  exists,
92
108
  version: installed,
93
109
  // Present but unmarked: the user put a `dotmd.js` there themselves.
94
110
  foreign: exists && installed === null,
95
- stale: installed !== null && version !== undefined && installed !== version,
111
+ // A legacy banner is stale even at the same version, so the next install or
112
+ // update rewrites it in place with the current banner.
113
+ stale: installed !== null && ((version !== undefined && installed !== version) || legacyBanner),
114
+ legacyBanner,
96
115
  };
97
116
  }
98
117
 
@@ -110,7 +129,7 @@ export function installOpencodePlugin(opts = {}) {
110
129
  const { version, dryRun = false, force = false, ...rest } = opts;
111
130
  const status = opencodeStatus({ version, ...rest });
112
131
  if (status.foreign && !force) {
113
- return { ...status, action: 'refused', reason: 'a dotmd.js without a dotmd banner is already there — it was not written by dotmd' };
132
+ return { ...status, action: 'refused', reason: 'a dotmd.js without a generated banner is already there — it was not written by runlist' };
114
133
  }
115
134
  if (status.exists && !status.stale && !status.foreign) {
116
135
  return { ...status, action: 'current' };
@@ -153,7 +172,7 @@ export function describeSessionIdentity(opts = {}) {
153
172
  // still carries the old identity — say so rather than leave a warning
154
173
  // marker with no explanation.
155
174
  ? 'restart OpenCode — this session predates the installed integration'
156
- : 'dotmd install opencode — for a per-session identity');
175
+ : 'runlist install opencode — for a per-session identity');
157
176
  } else if (source.scope === 'terminal') {
158
177
  advice.push('every agent session in this terminal shares this id — set RUNLIST_SESSION_ID per session');
159
178
  }
@@ -202,11 +221,11 @@ export function degradedIdentityNotice(repoRoot, opts = {}) {
202
221
 
203
222
  const installed = opencodeStatus({ env, homedir, version }).exists;
204
223
  const lines = installed
205
- ? ['[dotmd] This OpenCode session started before the dotmd integration loaded, so its plan',
224
+ ? ['[runlist] This OpenCode session started before the runlist integration loaded, so its plan',
206
225
  ' ownership is still process-scoped. Restart OpenCode to pick it up.']
207
- : ['[dotmd] OpenCode detected, dotmd integration not installed — plan ownership is scoped to',
226
+ : ['[runlist] OpenCode detected, runlist integration not installed — plan ownership is scoped to',
208
227
  ' the OpenCode process, so another session in it can release plans this one claimed,',
209
- ' and no session-start briefing runs. Fix once: dotmd install opencode'];
228
+ ' and no session-start briefing runs. Fix once: runlist install opencode'];
210
229
 
211
230
  if (!record) return lines.join('\n');
212
231
  if (!repoRoot) return lines.join('\n');
@@ -251,7 +270,7 @@ export function claudeMarketplaceRefusalHint(marketplace = 'dotmd') {
251
270
  // the side effects stay in the caller.
252
271
  export function planClaudeInstall({ installed, hasClaude, remove = false } = {}) {
253
272
  if (remove) {
254
- if (!installed) return [{ kind: 'skip', reason: 'dotmd plugin is not installed' }];
273
+ if (!installed) return [{ kind: 'skip', reason: 'runlist plugin is not installed' }];
255
274
  return hasClaude
256
275
  ? [{ kind: 'run', cmd: ['claude', 'plugin', 'uninstall', installed.id] }]
257
276
  : [{ kind: 'manual', lines: [`/plugin uninstall ${installed.id}`] }];
@@ -263,7 +282,7 @@ export function planClaudeInstall({ installed, hasClaude, remove = false } = {})
263
282
  return planMarketplaceRepair(installed, { hasClaude, verb: 'update' })
264
283
  .map(step => (step.kind === 'marketplace' || step.kind === 'plugin' ? { ...step, kind: 'run', step: step.kind } : step));
265
284
  }
266
- if (installed) return [{ kind: 'skip', reason: `dotmd plugin already installed (${installed.version ?? 'unknown version'})` }];
285
+ if (installed) return [{ kind: 'skip', reason: `runlist plugin already installed (${installed.version ?? 'unknown version'})` }];
267
286
  // The marketplace has to be registered before the plugin resolves; adding one
268
287
  // already present is a no-op, so this stays safe to re-run.
269
288
  if (!hasClaude) {
@@ -280,7 +299,7 @@ export function removeOpencodePlugin(opts = {}) {
280
299
  const status = opencodeStatus(rest);
281
300
  if (!status.exists) return { ...status, action: 'absent' };
282
301
  if (status.foreign && !force) {
283
- return { ...status, action: 'refused', reason: 'not a dotmd-generated file' };
302
+ return { ...status, action: 'refused', reason: 'not a runlist-generated file' };
284
303
  }
285
304
  if (!dryRun) rmSync(status.path, { force: true });
286
305
  return { ...status, action: 'removed' };
@@ -273,7 +273,7 @@ export function checkHubMembershipDrift(docs, config) {
273
273
  warnings.push({
274
274
  path: child.path,
275
275
  level: 'warning',
276
- message: `is ranked in the body order of \`${hub.path}\` (the list \`dotmd runlist next\` walks) but has no \`parent_plan:\`. Add \`parent_plan: ${hub.path}\` so reverse-link tooling (pickup-card Related:, graph) stays consistent.`,
276
+ message: `is ranked in the body order of \`${hub.path}\` (the list \`runlist runlist next\` walks) but has no \`parent_plan:\`. Add \`parent_plan: ${hub.path}\` so reverse-link tooling (pickup-card Related:, graph) stays consistent.`,
277
277
  meta: { kind: BACKREF_KIND, hub: hub.path, source: 'body-order' },
278
278
  });
279
279
  }
package/src/hud.mjs CHANGED
@@ -33,8 +33,8 @@ export function detectVersionDrift(env = process.env) {
33
33
  const pluginVersion = JSON.parse(readFileSync(pj, 'utf8')).version;
34
34
  const cmp = compareVersions(pluginVersion, pkg.version);
35
35
  if (cmp === null || cmp === 0) return null;
36
- if (cmp < 0) return `dotmd plugin ${pluginVersion} is behind the CLI ${pkg.version} — run \`dotmd update\` then restart.`;
37
- return `dotmd CLI ${pkg.version} is behind the plugin ${pluginVersion} — run \`dotmd update\` (or npm i -g dotmd-cli).`;
36
+ if (cmp < 0) return `runlist plugin ${pluginVersion} is behind the CLI ${pkg.version} — run \`runlist update\` then restart.`;
37
+ return `runlist CLI ${pkg.version} is behind the plugin ${pluginVersion} — run \`runlist update\` (or npm i -g dotmd-cli).`;
38
38
  } catch {
39
39
  return null;
40
40
  }
@@ -155,9 +155,9 @@ const MISUSE_RECAP_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
155
155
  const MISUSE_RECAP_THRESHOLD = 3;
156
156
 
157
157
  const MISUSE_CORRECTIONS = {
158
- 'edit-status': 'never hand-edit `status:`; use `dotmd set <status> <file>`',
159
- 'cat-prompt': 'consume prompts with `dotmd use <file>`; peek without consuming via `dotmd prompts show <file>`',
160
- 'read-prompt': 'consume prompts with `dotmd use <file>`; peek without consuming via `dotmd prompts show <file>`',
158
+ 'edit-status': 'never hand-edit `status:`; use `runlist set <status> <file>`',
159
+ 'cat-prompt': 'consume prompts with `runlist use <file>`; peek without consuming via `runlist prompts show <file>`',
160
+ 'read-prompt': 'consume prompts with `runlist use <file>`; peek without consuming via `runlist prompts show <file>`',
161
161
  'commit-prompt': 'saved prompts are session-local; never git add/commit them',
162
162
  };
163
163
 
@@ -176,7 +176,7 @@ export function buildMisuseRecap(config, now = Date.now()) {
176
176
  const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
177
177
  if (!top || top[1] < MISUSE_RECAP_THRESHOLD) return null;
178
178
  const [rule, count] = top;
179
- const fix = MISUSE_CORRECTIONS[rule] ?? 'see `dotmd misuse`';
179
+ const fix = MISUSE_CORRECTIONS[rule] ?? 'see `runlist misuse`';
180
180
  return `sessions here tripped ${rule} ${count}× this week — ${fix}`;
181
181
  }
182
182
 
@@ -235,15 +235,15 @@ export function buildHud(config) {
235
235
  // lines: the verbs + the three wrong-moves the guard exists to stop, so the
236
236
  // subagent self-corrects before the guard ever has to fire.
237
237
  const SUBAGENT_PRIMER = [
238
- 'dotmd manages this repo\'s plans/docs/prompts (markdown + YAML frontmatter).',
238
+ 'runlist manages this repo\'s plans/docs/prompts (markdown + YAML frontmatter).',
239
239
  'Verbs: plans|briefing | query <filters> | use [<file>] | set <status> <file> | new <type> <slug> | archive <file>.',
240
- 'Do NOT: cat/read a docs/prompts/*.md (use `dotmd use <file>` — archive/claim commits before at-most-once output);',
241
- 'git add/commit a prompt (they are session-local, often gitignored); hand-edit a `status:` field (use `dotmd set`).',
240
+ 'Do NOT: cat/read a docs/prompts/*.md (use `runlist use <file>` — archive/claim commits before at-most-once output);',
241
+ 'git add/commit a prompt (they are session-local, often gitignored); hand-edit a `status:` field (use `runlist set`).',
242
242
  ].join('\n');
243
243
 
244
244
  export function buildPlanStatusPrimer(config, { maxChars = 220 } = {}) {
245
245
  const statuses = (resolveStatusMetadata(config).byType.plan ?? []).map(item => item.name);
246
- const fallback = 'run `dotmd statuses list --type plan`';
246
+ const fallback = 'run `runlist statuses list --type plan`';
247
247
  if (statuses.length === 0) return `Plan statuses unavailable; ${fallback}.`;
248
248
  const prefix = 'Plan statuses: ';
249
249
  const full = `${prefix}${statuses.join(', ')}`;
@@ -313,15 +313,15 @@ export function runHud(argv, config) {
313
313
  // Global in-session counts never provide a fallback.
314
314
  // The misuse recap stays for the same reason: a repeat-offense rule means
315
315
  // the primer alone isn't landing, so name the habit to break.
316
- process.stdout.write(dim('dotmd: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> baton [<slug>] <@<file>|-> (save a resume prompt; releases the in-session plan if any) (use [no-arg] → oldest pending prompt)') + '\n');
316
+ process.stdout.write(dim('runlist: plans|briefing set <status> [<file>] new <type> <slug> use [<file>] archive <file> baton [<slug>] <@<file>|-> (save a resume prompt; releases the in-session plan if any) (use [no-arg] → oldest pending prompt)') + '\n');
317
317
  process.stdout.write(dim(buildPlanStatusPrimer(config)) + '\n');
318
318
  if (hud.owned && hud.owned.via === 'ownership') {
319
- process.stdout.write(yellow(`[dotmd] in-session (yours): ${hud.owned.path} — continue it; hand off with \`dotmd baton @/tmp/draft.md\` before stopping.`) + '\n');
319
+ process.stdout.write(yellow(`[runlist] in-session (yours): ${hud.owned.path} — continue it; hand off with \`runlist baton @/tmp/draft.md\` before stopping.`) + '\n');
320
320
  }
321
321
  if (hud.prompts.length > 0) {
322
322
  const n = hud.prompts.length;
323
- process.stdout.write(yellow(`[dotmd] ${n} pending prompt${n === 1 ? '' : 's'} queued for this session — unless the user asks for something else, start by running \`dotmd use\` to consume the oldest (${hud.prompts[0]}) and act on it. Peek first: \`dotmd prompts show <file>\`; list: \`dotmd prompts\`.`) + '\n');
323
+ process.stdout.write(yellow(`[runlist] ${n} pending prompt${n === 1 ? '' : 's'} queued for this session — unless the user asks for something else, start by running \`runlist use\` to consume the oldest (${hud.prompts[0]}) and act on it. Peek first: \`runlist prompts show <file>\`; list: \`runlist prompts\`.`) + '\n');
324
324
  }
325
- if (hud.misuseRecap) process.stdout.write(yellow(`[dotmd] ${hud.misuseRecap}`) + '\n');
325
+ if (hud.misuseRecap) process.stdout.write(yellow(`[runlist] ${hud.misuseRecap}`) + '\n');
326
326
  if (drift) process.stdout.write(yellow(drift) + '\n');
327
327
  }
@@ -72,7 +72,7 @@ function renderArchivedSection(docs, config, status) {
72
72
  lines.push(`| [${escapeTable(doc.title)}](${linkPath}) | ${escapeTable(formatSnapshot(doc, config))} |`);
73
73
  }
74
74
  lines.push('');
75
- lines.push('- Use `dotmd list` or `dotmd json` for the full inventory.');
75
+ lines.push('- Use `runlist list` or `runlist json` for the full inventory.');
76
76
 
77
77
  return lines;
78
78
  }
@@ -141,7 +141,7 @@ export function checkIndex(docs, config, opts = {}) {
141
141
  const index = { docs };
142
142
  const expected = renderIndexFile(index, config);
143
143
  if (expected !== current) {
144
- errors.push({ path: config.indexPath, level: 'error', message: 'Generated index block is stale. Run `dotmd index`.' });
144
+ errors.push({ path: config.indexPath, level: 'error', message: 'Generated index block is stale. Run `runlist index`.' });
145
145
  }
146
146
 
147
147
  return { warnings, errors };
package/src/init.mjs CHANGED
@@ -87,40 +87,40 @@ status: planned
87
87
  created: ${today}
88
88
  updated: ${today}
89
89
  title: Example Plan
90
- summary: A scaffolded sample showing what a dotmd plan looks like — safe to delete.
91
- current_state: "Scaffolded by \`dotmd init\` as a shape reference. Nothing here is real work."
90
+ summary: A scaffolded sample showing what a runlist plan looks like — safe to delete.
91
+ current_state: "Scaffolded by \`runlist init\` as a shape reference. Nothing here is real work."
92
92
  next_step: "Delete this file, or edit it into your first real plan."
93
93
  ---
94
94
 
95
95
  # Example Plan
96
96
 
97
- > A scaffolded sample showing what a dotmd plan looks like — safe to delete.
97
+ > A scaffolded sample showing what a runlist plan looks like — safe to delete.
98
98
 
99
99
  ## Problem
100
100
 
101
- \`dotmd init\` leaves this file behind so \`docs/plans/\` survives a clone (git cannot
101
+ \`runlist init\` leaves this file behind so \`docs/plans/\` survives a clone (git cannot
102
102
  track an empty directory) and so the frontmatter above has something to point at.
103
103
 
104
104
  The fields that matter: \`status\` drives every listing, \`current_state\` and
105
- \`next_step\` are what \`dotmd briefing\` reads out, and \`updated\` drives staleness.
106
- Never hand-edit \`status:\` — \`dotmd set <status> <file>\` writes it, validates it
105
+ \`next_step\` are what \`runlist briefing\` reads out, and \`updated\` drives staleness.
106
+ Never hand-edit \`status:\` — \`runlist set <status> <file>\` writes it, validates it
107
107
  against the type, and runs the lifecycle hooks.
108
108
 
109
109
  ## Phases
110
110
 
111
- - [ ] Delete this file: \`dotmd archive docs/plans/${SAMPLE_PLAN_NAME}\`
112
- - [ ] Write a real one: \`dotmd new plan <name>\`
111
+ - [ ] Delete this file: \`runlist archive docs/plans/${SAMPLE_PLAN_NAME}\`
112
+ - [ ] Write a real one: \`runlist new plan <name>\`
113
113
 
114
114
  ## Version History
115
115
 
116
- - Scaffolded by \`dotmd init\`.
116
+ - Scaffolded by \`runlist init\`.
117
117
  `;
118
118
 
119
119
  const STARTER_INDEX = `# Docs
120
120
 
121
121
  <!-- GENERATED:dotmd:start -->
122
122
 
123
- _No docs yet. Run \`dotmd list\` after creating your first document._
123
+ _No docs yet. Run \`runlist list\` after creating your first document._
124
124
 
125
125
  <!-- GENERATED:dotmd:end -->
126
126
  `;
@@ -332,7 +332,7 @@ export async function runInit(cwd, config, opts = {}) {
332
332
  }
333
333
  if (existsSync(subPath)) {
334
334
  const detail = total > 0
335
- ? ` (${counts.withFrontmatter} dotmd-tracked, ${counts.withoutFrontmatter} plain .md)`
335
+ ? ` (${counts.withFrontmatter} runlist-tracked, ${counts.withoutFrontmatter} plain .md)`
336
336
  : '';
337
337
  process.stdout.write(` ${dryTag}${dim('exists')} docs/${sub}/${detail}\n`);
338
338
  } else {
@@ -373,7 +373,7 @@ export async function runInit(cwd, config, opts = {}) {
373
373
  for (const sub of subs) {
374
374
  process.stdout.write(` • move into docs/: mv ./${sub}/* docs/${sub}/ && rmdir ./${sub}\n`);
375
375
  }
376
- process.stdout.write(` • or use a flat layout — set in dotmd.config.mjs:\n`);
376
+ process.stdout.write(` • or use a flat layout — set in runlist.config.mjs:\n`);
377
377
  process.stdout.write(` export const root = [${subs.map(s => `'${s}'`).join(', ')}];\n`);
378
378
  }
379
379
 
@@ -425,7 +425,7 @@ export async function runInit(cwd, config, opts = {}) {
425
425
  });
426
426
  // Exit 0 → ignored. Exit 1 → not ignored. Exit 128 → not in repo / git error.
427
427
  if (probe.status === 0) {
428
- process.stdout.write(`\n ${yellow('notice')} docs/ is gitignored — files dotmd manages will NOT be tracked.\n`);
428
+ process.stdout.write(`\n ${yellow('notice')} docs/ is gitignored — files runlist manages will NOT be tracked.\n`);
429
429
  process.stdout.write(` Add an exception to .gitignore so docs/ is tracked:\n`);
430
430
  process.stdout.write(` !docs/\n`);
431
431
  process.stdout.write(` Or run: echo '!docs/' >> .gitignore\n`);
@@ -438,7 +438,7 @@ export async function runInit(cwd, config, opts = {}) {
438
438
  if (scan?.untaggedCount > 0) {
439
439
  const n = scan.untaggedCount;
440
440
  const noun = n === 1 ? 'file' : 'files';
441
- process.stdout.write(`\n ${yellow('hint')} ${n} untagged .md ${noun} found — run \`dotmd bulk-tag --dry-run\` to preview tagging.\n`);
441
+ process.stdout.write(`\n ${yellow('hint')} ${n} untagged .md ${noun} found — run \`runlist bulk-tag --dry-run\` to preview tagging.\n`);
442
442
  }
443
443
 
444
444
  // Claude Code integration. dotmd no longer scaffolds per-repo
@@ -457,25 +457,25 @@ export async function runInit(cwd, config, opts = {}) {
457
457
  const removed = removeGeneratedSlashCommands(cwd, { dryRun });
458
458
  for (const r of removed) {
459
459
  const verb = dryRun ? 'would remove' : 'removed';
460
- process.stdout.write(` ${dryTag}${yellow('clean')} .claude/commands/${r.name} (retired — ${verb}; guidance ships via the dotmd plugin)\n`);
460
+ process.stdout.write(` ${dryTag}${yellow('clean')} .claude/commands/${r.name} (retired — ${verb}; guidance ships via the runlist plugin)\n`);
461
461
  }
462
462
  }
463
463
 
464
464
  if (likelyClaudeUser) {
465
465
  const sessionStart = detectSessionStartHook(cwd);
466
466
  if (sessionStart.wired) {
467
- process.stdout.write(` ${dim('exists')} ${sessionStart.file} (SessionStart hook for \`dotmd hud\` already wired)\n`);
467
+ process.stdout.write(` ${dim('exists')} ${sessionStart.file} (SessionStart hook for \`runlist hud\` already wired)\n`);
468
468
  } else {
469
- process.stdout.write(`\n ${yellow('hint')} install the dotmd Claude Code plugin so its hooks + workflow skill\n`);
469
+ process.stdout.write(`\n ${yellow('hint')} install the runlist Claude Code plugin so its hooks + workflow skill\n`);
470
470
  process.stdout.write(` travel to every session and subagent automatically:\n\n`);
471
471
  process.stdout.write(` /plugin marketplace add reowens/dotmd\n`);
472
472
  process.stdout.write(` /plugin install dotmd@dotmd\n\n`);
473
- process.stdout.write(` The plugin's hooks call \`dotmd\` on your PATH, so install the CLI\n`);
473
+ process.stdout.write(` The plugin's hooks call \`runlist\` on your PATH, so install the CLI\n`);
474
474
  process.stdout.write(` globally too — ${green('npm i -g dotmd-cli')} (a project devDependency won't power them).\n\n`);
475
- process.stdout.write(` Or, without the plugin, wire \`dotmd hud\` at SessionStart by hand —\n`);
475
+ process.stdout.write(` Or, without the plugin, wire \`runlist hud\` at SessionStart by hand —\n`);
476
476
  process.stdout.write(` add to .claude/settings.json (merge into any existing hooks):\n\n`);
477
477
  process.stdout.write(` "hooks": { "SessionStart": [\n`);
478
- process.stdout.write(` { "hooks": [{ "type": "command", "command": "dotmd hud" }] }\n`);
478
+ process.stdout.write(` { "hooks": [{ "type": "command", "command": "runlist hud" }] }\n`);
479
479
  process.stdout.write(` ] }\n`);
480
480
  }
481
481
  }
@@ -505,8 +505,8 @@ export async function runInit(cwd, config, opts = {}) {
505
505
  }
506
506
 
507
507
  process.stdout.write(`\nReady. A few starting points:\n`);
508
- process.stdout.write(` dotmd new doc my-doc # scaffold a reference doc\n`);
509
- process.stdout.write(` dotmd new plan my-plan # scaffold an execution plan\n`);
510
- process.stdout.write(` dotmd list # see what you've got\n`);
511
- process.stdout.write(` dotmd hud # session-start triage\n\n`);
508
+ process.stdout.write(` runlist new doc my-doc # scaffold a reference doc\n`);
509
+ process.stdout.write(` runlist new plan my-plan # scaffold an execution plan\n`);
510
+ process.stdout.write(` runlist list # see what you've got\n`);
511
+ process.stdout.write(` runlist hud # session-start triage\n\n`);
512
512
  }
package/src/install.mjs CHANGED
@@ -43,7 +43,7 @@ function reportStatus(json) {
43
43
  return;
44
44
  }
45
45
  const { claude, opencode } = states;
46
- process.stdout.write(`${bold('dotmd host integrations')} ${dim(`CLI ${pkg.version}`)}\n\n`);
46
+ process.stdout.write(`${bold('runlist host integrations')} ${dim(`CLI ${pkg.version}`)}\n\n`);
47
47
 
48
48
  const claudeBroken = claude.installed && claude.marketplaceRegistered === false;
49
49
  const claudeState = !claude.installed ? yellow('not installed')
@@ -60,8 +60,8 @@ function reportStatus(json) {
60
60
  process.stdout.write(` ${dim(opencode.path)}\n`);
61
61
 
62
62
  const todo = [];
63
- if (!claude.installed || claudeBroken) todo.push('dotmd install claude');
64
- if (!opencode.exists || opencode.stale) todo.push('dotmd install opencode');
63
+ if (!claude.installed || claudeBroken) todo.push('runlist install claude');
64
+ if (!opencode.exists || opencode.stale) todo.push('runlist install opencode');
65
65
  if (todo.length) {
66
66
  process.stdout.write('\n');
67
67
  for (const cmd of todo) process.stdout.write(`Run ${bold(cmd)}\n`);
@@ -143,7 +143,7 @@ function installOpencode(argv, dryRun, json) {
143
143
  if (dryRun) return result;
144
144
  process.stdout.write('\nRestart OpenCode to apply. New sessions then get:\n');
145
145
  process.stdout.write(` ${dim('·')} a per-session ownership identity (one session can no longer release another's plan)\n`);
146
- process.stdout.write(` ${dim('·')} the ${bold('dotmd hud')} primer at session start, like Claude Code's SessionStart hook\n`);
146
+ process.stdout.write(` ${dim('·')} the ${bold('runlist hud')} primer at session start, like Claude Code's SessionStart hook\n`);
147
147
  process.stdout.write(dim('\nPlans already in-session were claimed under the old process-scoped identity;\n'));
148
148
  process.stdout.write(dim('close them before restarting, or reclaim with --force afterwards.\n'));
149
149
  return result;
package/src/journal.mjs CHANGED
@@ -10,11 +10,17 @@ const ROTATE_SIZE_BYTES = 5 * 1024 * 1024;
10
10
  const ROTATE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
11
11
  const BACKUP_RETENTION_MS = ROTATE_AGE_MS;
12
12
 
13
- const ERROR_LOG_FILE = 'dotmd-errors.log';
14
- const ERROR_LOG_BACKUP = 'dotmd-errors.log.1';
13
+ // New writes go to the runlist-* names. The dotmd-* names are what dotmd-cli
14
+ // 0.78.0 and earlier wrote, and what an older CLI still installed elsewhere on
15
+ // the machine keeps writing, so readers merge both and never rename or delete
16
+ // the legacy files (beyond the usual stale-schema and stale-backup pruning).
17
+ const ERROR_LOG_FILE = 'runlist-errors.log';
18
+ const ERROR_LOG_BACKUP = 'runlist-errors.log.1';
15
19
 
16
- const MISUSE_LOG_FILE = 'dotmd-misuse.log';
17
- const MISUSE_LOG_BACKUP = 'dotmd-misuse.log.1';
20
+ const MISUSE_LOG_FILE = 'runlist-misuse.log';
21
+ const MISUSE_LOG_BACKUP = 'runlist-misuse.log.1';
22
+ const LEGACY_MISUSE_LOG_FILE = 'dotmd-misuse.log';
23
+ const LEGACY_MISUSE_LOG_BACKUP = 'dotmd-misuse.log.1';
18
24
  const TELEMETRY_SCHEMA = 2;
19
25
  const REDACTED = '[redacted]';
20
26
  const SENSITIVE_VALUE_FLAGS = new Set([
@@ -358,9 +364,12 @@ export function recordGuardEvent(event) {
358
364
  }
359
365
  }
360
366
 
361
- export function readMisuseEntries() {
362
- const file = globalMisuseLogPath();
363
- purgeLegacyTelemetry(file, globalMisuseLogBackupPath());
367
+ // Every misuse log a reader should see: the current file, then the legacy one.
368
+ export function globalMisuseLogPaths() {
369
+ return [globalMisuseLogPath(), path.join(globalErrorLogDir(), LEGACY_MISUSE_LOG_FILE)];
370
+ }
371
+
372
+ function readLogLines(file) {
364
373
  if (!existsSync(file)) return [];
365
374
  let raw;
366
375
  try { raw = readFileSync(file, 'utf8'); } catch { return []; }
@@ -371,3 +380,27 @@ export function readMisuseEntries() {
371
380
  }
372
381
  return out;
373
382
  }
383
+
384
+ // Current and legacy entries, merged oldest-first by timestamp. The sort is
385
+ // stable, so entries with equal or unparseable timestamps keep file order.
386
+ export function readMisuseEntries() {
387
+ const dir = globalErrorLogDir();
388
+ const pairs = [
389
+ [globalMisuseLogPath(), globalMisuseLogBackupPath()],
390
+ [path.join(dir, LEGACY_MISUSE_LOG_FILE), path.join(dir, LEGACY_MISUSE_LOG_BACKUP)],
391
+ ];
392
+ const entries = [];
393
+ for (const [file, backup] of pairs) {
394
+ purgeLegacyTelemetry(file, backup);
395
+ pruneStaleBackup(backup);
396
+ entries.push(...readLogLines(file));
397
+ }
398
+ const time = (entry) => {
399
+ const t = Date.parse(entry?.ts);
400
+ return Number.isNaN(t) ? 0 : t;
401
+ };
402
+ return entries
403
+ .map((entry, index) => ({ entry, index, t: time(entry) }))
404
+ .sort((a, b) => a.t - b.t || a.index - b.index)
405
+ .map(({ entry }) => entry);
406
+ }
package/src/lifecycle.mjs CHANGED
@@ -35,7 +35,7 @@ import {
35
35
 
36
36
  export function renderLifecycleMutation(raw, updates, historyEntry, { createSection = false, bodyTransform = null } = {}) {
37
37
  raw = normalizeEol(raw);
38
- if (!raw.startsWith('---\n')) throw new Error('Document has no frontmatter block. Retrofit it with `dotmd bulk-tag` first.');
38
+ if (!raw.startsWith('---\n')) throw new Error('Document has no frontmatter block. Retrofit it with `runlist bulk-tag` first.');
39
39
  const endMarker = raw.indexOf('\n---\n', 4);
40
40
  if (endMarker === -1) throw new Error('Document has an unclosed frontmatter block.');
41
41
  let frontmatter = raw.slice(4, endMarker);
@@ -192,7 +192,7 @@ export function regenIndex(config, options = {}) {
192
192
  return true;
193
193
  } catch (err) {
194
194
  if (options.throwOnError) throw err;
195
- warn(`Could not regenerate index (run \`dotmd index\`): ${err.message}`);
195
+ warn(`Could not regenerate index (run \`runlist index\`): ${err.message}`);
196
196
  return false;
197
197
  }
198
198
  }
@@ -360,10 +360,10 @@ export async function runStatus(argv, config, opts = {}) {
360
360
  let newStatus = argv[1];
361
361
 
362
362
  if (!opts.suppressDeprecation) {
363
- process.stderr.write(dim('`dotmd status <file> <status>` is deprecated; prefer `dotmd set <status> [<file>]` (note: <status> first; <file> optional when a plan is in-session). Removed in a future major.\n'));
363
+ process.stderr.write(dim('`runlist status <file> <status>` is deprecated; prefer `runlist set <status> [<file>]` (note: <status> first; <file> optional when a plan is in-session). Removed in a future major.\n'));
364
364
  }
365
365
 
366
- if (!input) { die('Usage: dotmd status <file> <new-status>'); }
366
+ if (!input) { die('Usage: runlist status <file> <new-status>'); }
367
367
 
368
368
  let filePath = resolveDocArg(input, config);
369
369
  const sourceAuthorization = authorizeManagedSource(filePath, config, { kind: 'Status source' });
@@ -394,7 +394,7 @@ export async function runStatus(argv, config, opts = {}) {
394
394
  newStatus = await promptChoice('Which status?', effectiveOrder);
395
395
  if (!newStatus) die('No status selected.');
396
396
  } else {
397
- die('Usage: dotmd status <file> <new-status>');
397
+ die('Usage: runlist status <file> <new-status>');
398
398
  }
399
399
  }
400
400
 
@@ -526,7 +526,7 @@ export async function runStatus(argv, config, opts = {}) {
526
526
  // this so concurrent agents can do path-limited commits without pulling
527
527
  // each other's uncommitted index changes into the staging area.
528
528
  if (noIndex) {
529
- process.stderr.write(dim('(index not regenerated — run `dotmd index` to refresh)\n'));
529
+ process.stderr.write(dim('(index not regenerated — run `runlist index` to refresh)\n'));
530
530
  } else if (!opts.deferIndex) {
531
531
  regenIndex(config);
532
532
  }
@@ -568,7 +568,7 @@ export async function startPlan(argv, config, opts = {}) {
568
568
 
569
569
  // Interactive: pick from active/planned plans
570
570
  if (!input) {
571
- if (!isInteractive()) die('Usage: dotmd use <plan>');
571
+ if (!isInteractive()) die('Usage: runlist use <plan>');
572
572
  const index = buildIndex(config);
573
573
  const sessionId = authoritativeSessionId();
574
574
  const candidates = pickupCandidates(index, config, sessionId);
@@ -672,7 +672,7 @@ export async function startPlan(argv, config, opts = {}) {
672
672
  } else {
673
673
  process.stderr.write(`${green('▶ Started')}: ${repoPath} (${oldStatus ?? 'unset'} → in-session)\n\n`);
674
674
  if (fullBody) {
675
- const header = `[dotmd] in-session: ${repoPath} — close with: dotmd set <status> ${repoPath}\n---\n`;
675
+ const header = `[runlist] in-session: ${repoPath} — close with: runlist set <status> ${repoPath}\n---\n`;
676
676
  process.stdout.write(header);
677
677
  const content = (body ?? '').trim();
678
678
  if (content) process.stdout.write(content + '\n');
@@ -707,7 +707,7 @@ export function runArchive(argv, config, opts = {}) {
707
707
  }
708
708
  const input = argv[0];
709
709
 
710
- if (!input) { die('Usage: dotmd archive <file>'); }
710
+ if (!input) { die('Usage: runlist archive <file>'); }
711
711
 
712
712
  let filePath = resolveDocArg(input, config);
713
713
  const sourceAuthorization = authorizeManagedSource(filePath, config, { kind: 'Archive source' });
@@ -861,7 +861,7 @@ export function runArchive(argv, config, opts = {}) {
861
861
  if (selfRefsFixed) out.write('Updated references in archived file.\n');
862
862
  if (updatedRefCount > 0) out.write(`Updated references in ${updatedRefCount} file(s).\n`);
863
863
  if (config.indexPath && indexRegenerated) out.write('Index regenerated.\n');
864
- if (config.indexPath && noIndex) out.write(dim('(index not regenerated — run `dotmd index` to refresh)\n'));
864
+ if (config.indexPath && noIndex) out.write(dim('(index not regenerated — run `runlist index` to refresh)\n'));
865
865
 
866
866
  const touched = [oldRepoPath, newRepoPath, ...refTouchedPaths];
867
867
  if (config.indexPath && indexRegenerated) touched.push(config.indexPath);
@@ -931,7 +931,7 @@ export async function runSet(argv, config, opts = {}) {
931
931
  const newStatus = argv[0];
932
932
  let input = argv[1];
933
933
 
934
- if (!newStatus) die('Usage: dotmd set <status> [<path>]');
934
+ if (!newStatus) die('Usage: runlist set <status> [<path>]');
935
935
  let sessionId = null;
936
936
  if (!input) {
937
937
  sessionId = authoritativeSessionId();
@@ -1051,7 +1051,7 @@ export async function runSet(argv, config, opts = {}) {
1051
1051
  // `@<draft-file>` stays an obvious metavariable: the old `@draft` read as
1052
1052
  // a runnable path and got copied verbatim into failing batons. If it is
1053
1053
  // copied anyway, readBodyInput recognizes the <…> form and says so.
1054
- warn(`wrapping up? leave a baton so the next session picks up cleanly — write your resume notes to a file, then \`dotmd baton ${path.basename(filePath, '.md')} @<draft-file>\` saves a resume prompt (no copy-paste into chat).`);
1054
+ warn(`wrapping up? leave a baton so the next session picks up cleanly — write your resume notes to a file, then \`runlist baton ${path.basename(filePath, '.md')} @<draft-file>\` saves a resume prompt (no copy-paste into chat).`);
1055
1055
  }
1056
1056
  }
1057
1057
  return result;
@@ -1063,7 +1063,7 @@ export function runBulkArchive(argv, config, opts = {}) {
1063
1063
  const noIndex = argv.includes('--no-index') || opts.noIndex;
1064
1064
  const showFiles = argv.includes('--show-files') || opts.showFiles;
1065
1065
  const inputs = argv.filter(a => !a.startsWith('-'));
1066
- if (inputs.length === 0) die('Usage: dotmd bulk archive <file1> <file2> ... or <glob>');
1066
+ if (inputs.length === 0) die('Usage: runlist bulk archive <file1> <file2> ... or <glob>');
1067
1067
 
1068
1068
  const allFiles = collectDocFiles(config);
1069
1069
  const matched = [];
@@ -1124,7 +1124,7 @@ export function runBulkArchive(argv, config, opts = {}) {
1124
1124
  catch (err) { indexError = err.message; }
1125
1125
  if (config.indexPath && indexRegenerated) process.stdout.write('Index regenerated.\n');
1126
1126
  } else if (config.indexPath) {
1127
- process.stdout.write(dim('(index not regenerated — run `dotmd index` to refresh)\n'));
1127
+ process.stdout.write(dim('(index not regenerated — run `runlist index` to refresh)\n'));
1128
1128
  }
1129
1129
  if (showFiles) {
1130
1130
  const all = [...bulkTouched];
@@ -1248,8 +1248,8 @@ export function runTouch(argv, config, opts = {}) {
1248
1248
  return;
1249
1249
  }
1250
1250
 
1251
- if (inputs.length > 1) die('Multiple files require `dotmd touch --git <file...>`.');
1252
- if (!input) { die('Usage: dotmd touch <file>\n dotmd touch --git Bulk-sync dates from git history'); }
1251
+ if (inputs.length > 1) die('Multiple files require `runlist touch --git <file...>`.');
1252
+ if (!input) { die('Usage: runlist touch <file>\n runlist touch --git Bulk-sync dates from git history'); }
1253
1253
 
1254
1254
  let filePath = resolveDocArg(input, config);
1255
1255
  filePath = authorizeManagedSource(filePath, config, { kind: 'Touch source' }).path;
@@ -1358,7 +1358,7 @@ export function updateFrontmatter(filePath, updates) {
1358
1358
  // Name the remedy in the error: this is where every status verb lands when a
1359
1359
  // doc was created outside dotmd, and "no frontmatter block" alone left
1360
1360
  // sessions retrying other verbs instead of fixing the doc.
1361
- if (!raw.startsWith('---\n')) throw new Error(`${filePath} has no frontmatter block. Retrofit it first: dotmd bulk-tag ${filePath} --type <type> --status <status>`);
1361
+ if (!raw.startsWith('---\n')) throw new Error(`${filePath} has no frontmatter block. Retrofit it first: runlist bulk-tag ${filePath} --type <type> --status <status>`);
1362
1362
 
1363
1363
  const endMarker = raw.indexOf('\n---\n', 4);
1364
1364
  if (endMarker === -1) throw new Error(`${filePath} has unclosed frontmatter block.`);
package/src/lint.mjs CHANGED
@@ -218,7 +218,7 @@ export function runLint(argv, config, opts = {}) {
218
218
  }
219
219
  }
220
220
  }
221
- process.stdout.write(`\nRun ${bold('dotmd lint --fix')} to auto-fix.\n`);
221
+ process.stdout.write(`\nRun ${bold('runlist lint --fix')} to auto-fix.\n`);
222
222
  }
223
223
 
224
224
  if (nonFixable.length > 0) {
@@ -167,5 +167,5 @@ export function runMigratePrompts(argv, config, opts = {}) {
167
167
  process.stdout.write(dim(` [${c.kind}] ${c.detail}\n`));
168
168
  }
169
169
  }
170
- if (dryRun) process.stdout.write(`\nRun ${bold('dotmd doctor --migrate-prompts')} without --dry-run to apply.\n`);
170
+ if (dryRun) process.stdout.write(`\nRun ${bold('runlist doctor --migrate-prompts')} without --dry-run to apply.\n`);
171
171
  }