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/guard.mjs CHANGED
@@ -223,8 +223,11 @@ function editStatusResult(target, config, detail) {
223
223
  rule: 'edit-status',
224
224
  detail,
225
225
  reason:
226
- `Looks like a hand-edit of the \`status:\` field in ${target}. Use \`dotmd set <status> ${target}\` instead — ` +
227
- `it validates the status against this doc's type, runs lifecycle hooks, fixes refs, and keeps the index in sync. Direct edits skip all of that.`,
226
+ // Names the canonical executable: a session that follows this message
227
+ // should land on the current name, not the legacy alias.
228
+ `Looks like a hand-edit of the \`status:\` field in ${target}. Use \`runlist set <status> ${target}\` instead — ` +
229
+ `it validates the status against this doc's type, runs lifecycle hooks, fixes refs, and keeps the index in sync. Direct edits skip all of that. ` +
230
+ `Creating a doc? \`runlist new <type> <name> --status <status>\` writes the status in the same step.`,
228
231
  };
229
232
  }
230
233
 
@@ -266,9 +269,9 @@ function evalBash(command, config, inspectGitPaths, baseCwd) {
266
269
  rule: 'commit-prompt',
267
270
  detail: `git ${git.subcommand} ${targets.join(' ')}`,
268
271
  reason:
269
- `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. ` +
270
273
  `Don't git add/commit them — commit your other changes without the prompt in the pathspec. ` +
271
- `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.`,
272
275
  };
273
276
  }
274
277
  }
@@ -280,9 +283,9 @@ function evalBash(command, config, inspectGitPaths, baseCwd) {
280
283
  rule: 'cat-prompt',
281
284
  detail: `${cmd0} ${promptTokens.join(' ')}`,
282
285
  reason:
283
- `${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]}\` — ` +
284
287
  `it commits archive/claim before at-most-once body output (prevents double-consumption). ` +
285
- `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.`,
286
289
  };
287
290
  }
288
291
 
@@ -305,9 +308,9 @@ function evalRead(filePath, config) {
305
308
  rule: 'read-prompt',
306
309
  detail: filePath,
307
310
  reason:
308
- `${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. ` +
309
- `Just peeking or triaging (not consuming)? \`dotmd prompts show ${filePath}\` reads it without archiving. ` +
310
- `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.`,
311
314
  };
312
315
  }
313
316
 
@@ -393,7 +396,7 @@ function emit(result) {
393
396
  hookSpecificOutput.permissionDecisionReason = result.reason;
394
397
  } else {
395
398
  // warn — allow the call but teach the agent the dotmd-native path.
396
- hookSpecificOutput.additionalContext = `[dotmd] ${result.reason}`;
399
+ hookSpecificOutput.additionalContext = `[runlist] ${result.reason}`;
397
400
  }
398
401
  process.stdout.write(JSON.stringify({ hookSpecificOutput }) + '\n');
399
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
  }
@@ -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
+ }