dotmd-cli 0.76.7 → 0.77.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.
@@ -28,25 +28,30 @@ import { execFile } from 'node:child_process';
28
28
  const PRIMER_TTL_MS = 60_000;
29
29
  const PRIMER_TIMEOUT_MS = 5_000;
30
30
 
31
- function dotmdExecutable() {
32
- return process.platform === 'win32' ? 'dotmd.cmd' : 'dotmd';
31
+ function cliExecutables() {
32
+ return process.platform === 'win32' ? ['runlist.cmd', 'dotmd.cmd'] : ['runlist', 'dotmd'];
33
33
  }
34
34
 
35
35
  function runHud(directory) {
36
36
  return new Promise(resolve => {
37
37
  let settled = false;
38
38
  const done = value => { if (!settled) { settled = true; resolve(value); } };
39
- try {
40
- execFile(dotmdExecutable(), ['hud'], {
41
- cwd: directory,
42
- timeout: PRIMER_TIMEOUT_MS,
43
- windowsHide: true,
44
- env: { ...process.env, NO_COLOR: '1' },
45
- }, (error, stdout) => done(error ? '' : (stdout ?? '').trim()));
46
- } catch {
47
- // `dotmd` not on PATH, spawn refused — no primer, no noise.
48
- done('');
49
- }
39
+ const candidates = cliExecutables();
40
+ const attempt = index => {
41
+ if (index >= candidates.length) { done(''); return; }
42
+ try {
43
+ execFile(candidates[index], ['hud'], {
44
+ cwd: directory,
45
+ timeout: PRIMER_TIMEOUT_MS,
46
+ windowsHide: true,
47
+ env: { ...process.env, NO_COLOR: '1' },
48
+ }, (error, stdout) => {
49
+ if (error?.code === 'ENOENT') attempt(index + 1);
50
+ else done(error ? '' : (stdout ?? '').trim());
51
+ });
52
+ } catch { attempt(index + 1); }
53
+ };
54
+ attempt(0);
50
55
  });
51
56
  }
52
57
 
@@ -76,10 +81,14 @@ export default async function dotmdOpencodePlugin({ directory }) {
76
81
  // available to a tool shell.
77
82
  'shell.env': async (input, output) => {
78
83
  try {
79
- if (input?.sessionID) output.env.DOTMD_SESSION_ID = `opencode:${input.sessionID}`;
84
+ if (input?.sessionID) {
85
+ output.env.RUNLIST_SESSION_ID = `opencode:${input.sessionID}`;
86
+ output.env.DOTMD_SESSION_ID = `opencode:${input.sessionID}`;
87
+ }
80
88
  // The OpenCode server process hosts the session and outlives every tool
81
89
  // shell, so it is the process whose liveness answers "is this claim's
82
90
  // owner still there?" — `dotmd doctor --claims` probes exactly this.
91
+ output.env.RUNLIST_SESSION_PID = String(process.pid);
83
92
  output.env.DOTMD_SESSION_PID = String(process.pid);
84
93
  } catch { /* never break a shell over this */ }
85
94
  },
package/bin/dotmd.mjs CHANGED
@@ -138,7 +138,7 @@ Rules:
138
138
 
139
139
  \`guard: { deny: false }\` in dotmd.config.mjs drops edit-status back to
140
140
  warn-only. Every catch is appended to the cross-repo misuse log. Disable the
141
- guard entirely with DOTMD_GUARD=0. Read the log with \`dotmd misuse\`; when one
141
+ guard entirely with RUNLIST_GUARD=0. Read the log with \`dotmd misuse\`; when one
142
142
  rule trips ≥3× in 7 days in a repo, \`dotmd hud\` opens the next session there
143
143
  with a one-line recap naming the habit to break.`,
144
144
 
@@ -278,7 +278,7 @@ Setup:
278
278
  watch [command] Re-run a command on file changes
279
279
  completions <shell> Shell completion script (bash, zsh)
280
280
  journal [--tail N|--errors|--by-command|--session id|--since iso|--json]
281
- View opt-in JSONL command journal (enable: DOTMD_JOURNAL=1 or journal: true)
281
+ View opt-in JSONL command journal (enable: RUNLIST_JOURNAL=1 or journal: true)
282
282
 
283
283
  Global Options:
284
284
  --config <path> Explicit config file path
@@ -400,10 +400,10 @@ invocation appends one JSONL line to .dotmd/journal.jsonl with argv, exit
400
400
  code, elapsed ms, session id, and (on error) a single-line err message.
401
401
 
402
402
  Enable:
403
- - env: DOTMD_JOURNAL=1
403
+ - env: RUNLIST_JOURNAL=1
404
404
  - config: \`export const journal = true;\` in dotmd.config.mjs
405
405
 
406
- The env var beats config (DOTMD_JOURNAL=0 forces off). The journal is
406
+ The env var beats config (RUNLIST_JOURNAL=0 forces off). The journal is
407
407
  default-off so non-agent users don't pay the size/PII cost.
408
408
 
409
409
  Reader options:
@@ -421,7 +421,7 @@ Storage:
421
421
  rotation or pruned after the retention window.
422
422
 
423
423
  Examples:
424
- DOTMD_JOURNAL=1 dotmd plans
424
+ RUNLIST_JOURNAL=1 dotmd plans
425
425
  dotmd journal --tail 5
426
426
  dotmd journal --errors
427
427
  dotmd journal --by-command
@@ -506,7 +506,7 @@ Ambiguous slugs error with the candidate list instead of guessing.
506
506
  When the path is omitted, exactly one plan must be owned by this session.
507
507
  Claude Code session IDs are recognized automatically, as is OpenCode (via
508
508
  OPENCODE_PID — per OpenCode process, not per session). Other hosts must set
509
- DOTMD_SESSION_ID; anonymous ownership mutations fail closed.
509
+ RUNLIST_SESSION_ID; anonymous ownership mutations fail closed.
510
510
  Pickup hooks use at-least-once delivery with a stable operationId; hook side
511
511
  effects must deduplicate that ID.
512
512
 
@@ -1442,6 +1442,20 @@ Pass file paths as positional args to scope to those files only; otherwise
1442
1442
  the whole docs tree is scanned.`,
1443
1443
  };
1444
1444
 
1445
+ // Help presents the new product name while the compatibility package and
1446
+ // plugin still use their old registry identities. Protect those identifiers
1447
+ // from the display-only command-name rewrite until the package cutover.
1448
+ function canonicalHelp(text) {
1449
+ return String(text)
1450
+ .replaceAll('dotmd-cli', '\u0000PACKAGE\u0000')
1451
+ .replaceAll('dotmd@dotmd', '\u0000PLUGIN\u0000')
1452
+ .replaceAll('reowens/dotmd', '\u0000REPOSITORY\u0000')
1453
+ .replace(/\bdotmd\b/g, 'runlist')
1454
+ .replaceAll('\u0000PACKAGE\u0000', 'dotmd-cli')
1455
+ .replaceAll('\u0000PLUGIN\u0000', 'dotmd@dotmd')
1456
+ .replaceAll('\u0000REPOSITORY\u0000', 'reowens/dotmd');
1457
+ }
1458
+
1445
1459
  const GLOBAL_VALUE_OPTIONS = new Set(['--config', '--root', '--type']);
1446
1460
  const GLOBAL_BOOLEAN_OPTIONS = new Set(['--dry-run', '-n', '--verbose']);
1447
1461
 
@@ -1512,7 +1526,7 @@ async function main() {
1512
1526
  // Tolerate accidentally pasting the command prefix twice, while leaving all
1513
1527
  // remaining arguments to the normal `use` grammar and path validation.
1514
1528
  if (command === 'use') {
1515
- while (restArgs[0] === 'dotmd' && restArgs[1] === 'use') restArgs = restArgs.slice(2);
1529
+ while ((restArgs[0] === 'runlist' || restArgs[0] === 'dotmd') && restArgs[1] === 'use') restArgs = restArgs.slice(2);
1516
1530
  }
1517
1531
 
1518
1532
  // Reconstruct the active global flags for proxy commands (e.g. `watch`) that
@@ -1559,12 +1573,12 @@ async function main() {
1559
1573
  const topic = restArgs[0];
1560
1574
  if (topic) {
1561
1575
  const key = `help:${topic}`;
1562
- if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
1563
- if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
1576
+ if (HELP[key]) { process.stdout.write(`${canonicalHelp(HELP[key])}\n`); return; }
1577
+ if (HELP[topic]) { process.stdout.write(`${canonicalHelp(HELP[topic])}\n`); return; }
1564
1578
  process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: dotmd <cmd> --help\n`);
1565
1579
  process.exit(1);
1566
1580
  }
1567
- process.stdout.write(`${HELP._main}\n`);
1581
+ process.stdout.write(`${canonicalHelp(HELP._main)}\n`);
1568
1582
  return;
1569
1583
  }
1570
1584
 
@@ -1588,7 +1602,7 @@ async function main() {
1588
1602
  // Per-command help
1589
1603
  if (args.includes('--help') || args.includes('-h')) {
1590
1604
  requireCommandPolicy(command, dispatchPolicy);
1591
- process.stdout.write(`${HELP[command] ?? commandUsage(command)}\n`);
1605
+ process.stdout.write(`${canonicalHelp(HELP[command] ?? commandUsage(command))}\n`);
1592
1606
  return;
1593
1607
  }
1594
1608
 
@@ -1623,11 +1637,21 @@ async function main() {
1623
1637
  if (notice) process.stderr.write(`${notice}\n`);
1624
1638
  }
1625
1639
 
1626
- const suppressSideEffects = effectiveDryRun || command === 'hud' || passiveMachineContext;
1640
+ const suppressSideEffects = effectiveDryRun || command === 'hud' || command === 'guard' || passiveMachineContext;
1627
1641
  Object.defineProperty(config, '_execution', {
1628
1642
  value: { dryRun, passive: command === 'hud' || passiveMachineContext, suppressSideEffects, gitStaleness },
1629
1643
  enumerable: false,
1630
1644
  });
1645
+ if (!suppressSideEffects) {
1646
+ const { migrateStateDirectory } = await import('../src/state-migration.mjs');
1647
+ const { LEGACY_STATE_DIR, STATE_DIR } = await import('../src/naming.mjs');
1648
+ const migration = migrateStateDirectory(config.repoRoot);
1649
+ if (migration.status === 'migrated') {
1650
+ process.stderr.write(`[runlist] migrated ${LEGACY_STATE_DIR}/ → ${STATE_DIR}/ (session state moved; repository files unchanged)\n`);
1651
+ } else if (migration.status === 'refused') {
1652
+ warn(migration.message);
1653
+ }
1654
+ }
1631
1655
  // Unknown names may still be user-defined query presets. Every built-in
1632
1656
  // dispatcher branch, including mutators above the shared index path, must be
1633
1657
  // present in the centralized command policy registry.
@@ -2240,7 +2264,7 @@ main()
2240
2264
  let out = err.message;
2241
2265
  // F17c: append a repeat-failure tip when the journal shows this same shape
2242
2266
  // has already failed in this session within the lookup window. Lookup is
2243
- // a no-op when the journal is disabled or DOTMD_NO_HINTS=1.
2267
+ // a no-op when the journal is disabled or RUNLIST_NO_HINTS=1.
2244
2268
  try {
2245
2269
  const hint = findRepeatFailureHint(sanitizeTelemetryArgv(_invocationArgs), _resolvedConfig);
2246
2270
  if (hint) out = `${out}\n\nTip: ${hint}`;
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Canonical executable. The implementation remains shared with the retired
4
+ // `dotmd` alias during the compatibility window so both commands behave
5
+ // identically and cannot drift.
6
+ await import('./dotmd.mjs');
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.76.7",
3
+ "version": "0.77.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",
7
7
  "bin": {
8
+ "runlist": "bin/runlist.mjs",
8
9
  "dotmd": "bin/dotmd.mjs"
9
10
  },
10
11
  "exports": {
@@ -15,7 +16,7 @@
15
16
  "src/",
16
17
  "assets/",
17
18
  "scripts/postinstall.mjs",
18
- "dotmd.config.example.mjs"
19
+ "runlist.config.example.mjs"
19
20
  ],
20
21
  "keywords": [
21
22
  "markdown",
@@ -1,4 +1,4 @@
1
- // dotmd.config.mjs — document management configuration
1
+ // runlist.config.mjs — document management configuration
2
2
  // All exports are optional. Omitted values use built-in defaults.
3
3
  // Place this file at the root of your project.
4
4
 
@@ -7,17 +7,17 @@
7
7
  // Directory containing your markdown docs (relative to this config file)
8
8
  export const root = 'docs';
9
9
 
10
- // Subdirectory for archived docs (used by `dotmd archive` and `dotmd status`)
10
+ // Subdirectory for archived docs (used by `runlist archive` and `runlist status`)
11
11
  export const archiveDir = 'archived';
12
12
 
13
13
  // Directories to skip when scanning
14
14
  export const excludeDirs = ['evidence'];
15
15
 
16
- // Floor under the scan surface. `dotmd check` fails when it scans fewer docs than
16
+ // Floor under the scan surface. `runlist check` fails when it scans fewer docs than
17
17
  // this, so a broken root or an over-eager exclude can't read as a clean estate —
18
18
  // zero errors and zero docs look identical otherwise. Off when unset. Set it well
19
19
  // below your real count (round down hard); raise it as the corpus grows.
20
- // Override for one run with `dotmd check --min-docs <n>`; skipped for path-scoped
20
+ // Override for one run with `runlist check --min-docs <n>`; skipped for path-scoped
21
21
  // checks, which are deliberate subsets.
22
22
  // export const minDocs = 500;
23
23
 
@@ -44,7 +44,7 @@ export const excludeDirs = ['evidence'];
44
44
  // `terminal` and `quiet` are orthogonal. Mark a status `terminal` only when it represents closure
45
45
  // (excluded from active-work scope). Use `quiet` for noise suppression without closure semantics.
46
46
  //
47
- // Contradiction check (since 0.36.2): dotmd `warn()`s at config-load when a status combines
47
+ // Contradiction check (since 0.36.2): runlist `warn()`s at config-load when a status combines
48
48
  // `skipStale: true` with a `staleDays` value (the number is silently ignored) or
49
49
  // `skipWarnings: true` with `requiresModule: true` (the module requirement can never fire).
50
50
  // The same check applies via the `quiet: true` sugar. Drop one of the conflicting fields to silence.
@@ -80,8 +80,8 @@ export const excludeDirs = ['evidence'];
80
80
  // },
81
81
  // },
82
82
  // prompt: {
83
- // // Saved prompts that seed future Claude sessions. `dotmd hud` surfaces
84
- // // pending prompts on session start; `dotmd prompts next` claims the oldest.
83
+ // // Saved prompts that seed future Claude sessions. `runlist hud` surfaces
84
+ // // pending prompts on session start; `runlist prompts next` claims the oldest.
85
85
  // statuses: {
86
86
  // 'pending': { context: 'expanded', staleDays: 30 },
87
87
  // 'held': { context: 'counted', quiet: true }, // saved but not next: hidden from hud/briefing, skipped by no-arg `use`
@@ -132,7 +132,7 @@ export const statuses = {
132
132
  // no `types` definition. With rich-form types, the runtime derives these from
133
133
  // per-status `terminal` / `archive` / `skipStale` / `skipWarnings` / `quiet`
134
134
  // flags. An explicit `lifecycle` export sitting alongside rich-form types will
135
- // SILENTLY OVERRIDE the per-status flags — `dotmd statuses` will warn you
135
+ // SILENTLY OVERRIDE the per-status flags — `runlist statuses` will warn you
136
136
  // before writing into a config in that state.
137
137
  //
138
138
  // export const lifecycle = {
@@ -155,13 +155,13 @@ export const taxonomy = {
155
155
  // Index file generation — remove this section to disable
156
156
  export const index = {
157
157
  path: 'docs/docs.md',
158
- startMarker: '<!-- GENERATED:dotmd:start -->',
159
- endMarker: '<!-- GENERATED:dotmd:end -->',
158
+ startMarker: '<!-- GENERATED:runlist:start -->',
159
+ endMarker: '<!-- GENERATED:runlist:end -->',
160
160
  snapshot: 'status', // default; use 'state' to include live current_state text
161
161
  archivedLimit: 8,
162
162
  };
163
163
 
164
- // Context briefing layout (`dotmd context`)
164
+ // Context briefing layout (`runlist context`)
165
165
  export const context = {
166
166
  expanded: ['active'],
167
167
  listed: ['ready', 'planned'],
@@ -170,7 +170,7 @@ export const context = {
170
170
  recentStatuses: ['active', 'ready', 'planned'],
171
171
  recentLimit: 10,
172
172
  truncateNextStep: 80,
173
- // Cap on slugs shown in the "Stale: …" tail before "…and N more (run `dotmd stale`)"
173
+ // Cap on slugs shown in the "Stale: …" tail before "…and N more (run `runlist stale`)"
174
174
  // takes over (since 0.36.2). Raise on wide terminals; lower for tighter briefings.
175
175
  staleTailLimit: 8,
176
176
  };
@@ -198,13 +198,13 @@ export const presets = {
198
198
  };
199
199
 
200
200
  // ─── Templates ───────────────────────────────────────────────────────────────
201
- // Define new types or override builtins. `dotmd new <type> <name>` looks here first.
201
+ // Define new types or override builtins. `runlist new <type> <name>` looks here first.
202
202
  //
203
203
  // Properties:
204
- // description: string — shown in `dotmd new --list-types`
204
+ // description: string — shown in `runlist new --list-types`
205
205
  // defaultStatus: string — initial status if `--status` not passed
206
206
  // acceptsBody: boolean — allow body input (inline / --body / @file / piped stdin).
207
- // REQUIRED if you want `cat draft.md | dotmd new <type> <slug>` (or @path,
207
+ // REQUIRED if you want `cat draft.md | runlist new <type> <slug>` (or @path,
208
208
  // --body, heredoc) to work. Your `body` fn must also interpolate the input,
209
209
  // e.g. `${ctx?.bodyInput?.trim() ?? ''}`. See the body-acceptance guard below.
210
210
  // requiresBody: boolean — error if no body input (implies acceptsBody; see `prompt` builtin)
@@ -221,13 +221,13 @@ export const presets = {
221
221
  //
222
222
  // Body-acceptance guard (the #1 custom-template gotcha):
223
223
  // When you override a builtin and supply your OWN `body` fn that NEVER references
224
- // `bodyInput`, dotmd assumes the fn would silently discard piped input — so it strips
224
+ // `bodyInput`, runlist assumes the fn would silently discard piped input — so it strips
225
225
  // the inherited `acceptsBody`/`requiresBody` and rejects body input with a fail-fast
226
226
  // error. Two ways to keep piped/@path/heredoc bodies working in a custom template:
227
227
  // 1. interpolate `${ctx?.bodyInput?.trim() ?? ''}` somewhere in your `body` fn, OR
228
228
  // 2. set `acceptsBody: true` explicitly (do BOTH if you want the input to actually land).
229
229
  // A `body: (t) =>` that ignores `ctx` is the classic trap — it scaffolds fine but
230
- // `dotmd new <type> <slug> < draft.md` errors until you wire in `bodyInput`.
230
+ // `runlist new <type> <slug> < draft.md` errors until you wire in `bodyInput`.
231
231
  //
232
232
  // Custom type example — adds a `spike` type that lives in the `spikes` root (or
233
233
  // under `docs/spikes/` in single-root layouts):
@@ -6,7 +6,7 @@
6
6
  // - Only on GLOBAL installs (`npm i -g`). A project devDep / CI / Docker
7
7
  // install must never touch a user's Claude Code state.
8
8
  // - Default: just print a one-line nudge. A CLI install silently mutating the
9
- // agent's plugin cache is surprising; opt in with DOTMD_AUTO_PLUGIN_UPDATE=1
9
+ // agent's plugin cache is surprising; opt in with RUNLIST_AUTO_PLUGIN_UPDATE=1
10
10
  // to actually run the refresh.
11
11
  // - NEVER fail the install: everything is swallowed and we always exit 0. A
12
12
  // nonzero postinstall would break `npm i -g dotmd-cli`.
@@ -34,7 +34,7 @@ try {
34
34
  } catch { return false; }
35
35
  })();
36
36
 
37
- if (process.env.DOTMD_AUTO_PLUGIN_UPDATE === '1' && hasClaude) {
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
39
  process.stdout.write('dotmd: refreshed the Claude Code plugin — restart your session (or /reload-plugins) to apply.\n');
40
40
  } else {
@@ -23,6 +23,7 @@ import os from 'node:os';
23
23
  import path from 'node:path';
24
24
  import { captureGitIndexGeneration, gitIndexProvablyUnpublished, gitIndexPublicationRaced, reclaimPreparedGitIndex, restoreGitIndexCas, sameGitIndexGeneration, stageMovePathsCas } from './git.mjs';
25
25
  import { authorizeManagedDestination, authorizeManagedSource, authorizeRepoGeneratedPath } from './managed-path.mjs';
26
+ import { ARTIFACT_PREFIX, LEGACY_ARTIFACT_PREFIX, LEGACY_STATE_DIR, STATE_DIR, isOwnedArtifact, stateDir } from './naming.mjs';
26
27
  import { commitRename } from './durable-rename.mjs';
27
28
 
28
29
  const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
@@ -85,7 +86,7 @@ export class MutationConflictError extends Error {
85
86
  constructor(message) {
86
87
  super(message);
87
88
  this.name = 'MutationConflictError';
88
- this.code = 'DOTMD_MUTATION_CONFLICT';
89
+ this.code = 'RUNLIST_MUTATION_CONFLICT';
89
90
  }
90
91
  }
91
92
 
@@ -93,7 +94,7 @@ export class MutationLockError extends Error {
93
94
  constructor(message) {
94
95
  super(message);
95
96
  this.name = 'MutationLockError';
96
- this.code = 'DOTMD_MUTATION_LOCK_TIMEOUT';
97
+ this.code = 'RUNLIST_MUTATION_LOCK_TIMEOUT';
97
98
  }
98
99
  }
99
100
 
@@ -117,7 +118,7 @@ function safeGeneratedPath(filePath, repoRoot, options, kind) {
117
118
  }
118
119
 
119
120
  function transactionRoot(repoRoot, options = {}) {
120
- return safeGeneratedPath(path.join(path.resolve(repoRoot), '.dotmd', 'transactions'), repoRoot, options, 'Transaction root');
121
+ return safeGeneratedPath(path.join(stateDir(repoRoot), 'transactions'), repoRoot, options, 'Transaction root');
121
122
  }
122
123
 
123
124
  function durableJson(filePath, value, options = {}) {
@@ -237,7 +238,7 @@ function validatePreparedGitIndex(prepared, label, directory) {
237
238
  }
238
239
  validateGitSnapshot(prepared.generation, `${label}.generation`);
239
240
  validateTransactionArtifact(prepared.path, directory, label);
240
- if (path.dirname(prepared.tempPath) !== path.dirname(prepared.generation.indexPath) || !path.basename(prepared.tempPath).startsWith('.dotmd-index-')) {
241
+ if (path.dirname(prepared.tempPath) !== path.dirname(prepared.generation.indexPath) || !isOwnedArtifact(path.basename(prepared.tempPath), 'index')) {
241
242
  throw new MutationConflictError(`Unsafe prepared Git index path in ${label}.`);
242
243
  }
243
244
  if ((prepared.generation.exists && (prepared.hash !== prepared.generation.hash || prepared.size !== prepared.generation.size))
@@ -289,7 +290,7 @@ function validateParticipantPath(participant, repoRoot, options, label) {
289
290
  throw new MutationConflictError(`Invalid transaction participant ${label}.`);
290
291
  }
291
292
  const txRoot = transactionRoot(repoRoot, options);
292
- const lockRoot = safeGeneratedPath(path.join(repoRoot, '.dotmd', 'locks'), repoRoot, options, 'Lock root');
293
+ const lockRoot = safeGeneratedPath(path.join(stateDir(repoRoot), 'locks'), repoRoot, options, 'Lock root');
293
294
  if (contained(txRoot, participant.path) || contained(lockRoot, participant.path)) {
294
295
  throw new MutationConflictError(`Transaction participant targets transaction/lock state: ${participant.path}`);
295
296
  }
@@ -299,7 +300,7 @@ function validateParticipantPath(participant, repoRoot, options, label) {
299
300
  } else {
300
301
  const authorized = safeGeneratedPath(participant.path, repoRoot, options, 'Transaction generated participant');
301
302
  if (options.config) {
302
- const ownershipRoot = path.join(path.resolve(repoRoot), '.dotmd', 'ownership');
303
+ const ownershipRoot = path.join(stateDir(repoRoot), 'ownership');
303
304
  if (!contained(ownershipRoot, authorized)) throw new MutationConflictError(`Generated transaction participant is outside session ownership state: ${participant.path}`);
304
305
  if (participant.label !== 'ownership') throw new MutationConflictError(`Session-generated participant must be classified as ownership: ${participant.path}`);
305
306
  }
@@ -349,8 +350,9 @@ function validateManifest(manifest, manifestPath, directory, repoRoot, options)
349
350
  if (!participant || !contained(createdDirectory.path, participant.path) || createdDirectory.path === participant.path) {
350
351
  throw new MutationConflictError(`Transaction-created directory is not bound to a destination participant: ${createdDirectory.path}`);
351
352
  }
352
- const expectedMarker = path.join(createdDirectory.path, `.dotmd-transaction-${manifest.id}`);
353
- if (createdDirectory.marker !== expectedMarker || createdDirectory.token !== manifest.directoryToken) {
353
+ const expectedMarkers = [ARTIFACT_PREFIX, LEGACY_ARTIFACT_PREFIX]
354
+ .map(prefix => path.join(createdDirectory.path, `${prefix}transaction-${manifest.id}`));
355
+ if (!expectedMarkers.includes(createdDirectory.marker) || createdDirectory.token !== manifest.directoryToken) {
354
356
  throw new MutationConflictError(`Transaction-created directory marker binding mismatch: ${createdDirectory.path}`);
355
357
  }
356
358
  if (!['intended', 'created', 'removing', 'removed'].includes(createdDirectory.markerState)) throw new MutationConflictError(`Invalid transaction directory marker state: ${createdDirectory.path}`);
@@ -358,7 +360,7 @@ function validateManifest(manifest, manifestPath, directory, repoRoot, options)
358
360
  throw new MutationConflictError(`Invalid transaction-created directory identity: ${createdDirectory.path}`);
359
361
  }
360
362
  if (createdDirectory.markerState !== 'intended' && createdDirectory.identity === null) throw new MutationConflictError(`Transaction-created directory state lacks an identity: ${createdDirectory.path}`);
361
- if (participant.policy === 'managed' && options.config) authorizeManagedDestination(path.join(createdDirectory.path, '.dotmd-directory-check.md'), options.config, { kind: 'Transaction-created directory' });
363
+ if (participant.policy === 'managed' && options.config) authorizeManagedDestination(path.join(createdDirectory.path, `${ARTIFACT_PREFIX}directory-check.md`), options.config, { kind: 'Transaction-created directory' });
362
364
  else safeGeneratedPath(createdDirectory.path, repoRoot, options, 'Transaction-created directory');
363
365
  }
364
366
  const createdPaths = new Set(manifest.createdDirectories.map(item => item.path));
@@ -752,7 +754,7 @@ function ensureTransactionDirectory(directory, transaction, options, participant
752
754
  const intent = {
753
755
  path: item,
754
756
  participantPath: path.resolve(participantPath),
755
- marker: path.join(item, `.dotmd-transaction-${transaction.manifest.id}`),
757
+ marker: path.join(item, `${ARTIFACT_PREFIX}transaction-${transaction.manifest.id}`),
756
758
  token: transaction.manifest.directoryToken,
757
759
  markerState: 'intended',
758
760
  identity: null,
@@ -850,9 +852,9 @@ function setMoveManifestPhase(transaction, phase, options, detail = null) {
850
852
  }
851
853
 
852
854
  function companionPhase(item, fallback) {
853
- return item.label === 'ownership' || item.path.includes(`${path.sep}.dotmd${path.sep}ownership${path.sep}`)
854
- ? 'ownership-publication'
855
- : fallback;
855
+ const ownershipPath = [STATE_DIR, LEGACY_STATE_DIR]
856
+ .some(dir => item.path.includes(`${path.sep}${dir}${path.sep}ownership${path.sep}`));
857
+ return item.label === 'ownership' || ownershipPath ? 'ownership-publication' : fallback;
856
858
  }
857
859
 
858
860
  function statIdentity(stat) {
@@ -919,7 +921,7 @@ function canonicalPath(filePath) {
919
921
 
920
922
  function lockPathFor(canonical, repoRoot) {
921
923
  const key = createHash('sha256').update(canonical).digest('hex');
922
- return path.join(path.resolve(repoRoot), '.dotmd', 'locks', `${key}.lock`);
924
+ return path.join(stateDir(repoRoot), 'locks', `${key}.lock`);
923
925
  }
924
926
 
925
927
  function ownerDescription(lockPath) {
@@ -978,7 +980,7 @@ export function withPathLocks(filePaths, options, callback) {
978
980
  const { repoRoot, timeoutMs = MUTATION_LOCK_TIMEOUT_MS, retryMs = 20 } = options;
979
981
  if (!repoRoot) throw new Error('withPathLocks requires repoRoot.');
980
982
  const canonicals = [...new Set(filePaths.map(canonicalPath))].sort();
981
- const lockRoot = safeGeneratedPath(path.join(path.resolve(repoRoot), '.dotmd', 'locks'), repoRoot, options, 'Lock root');
983
+ const lockRoot = safeGeneratedPath(path.join(stateDir(repoRoot), 'locks'), repoRoot, options, 'Lock root');
982
984
  ensureDirectoryDurable(lockRoot, options, 'lock-root-create');
983
985
  const acquired = [];
984
986
  const deadline = Date.now() + timeoutMs;
@@ -1059,7 +1061,7 @@ export function withPathLocks(filePaths, options, callback) {
1059
1061
 
1060
1062
  function tempPathFor(filePath) {
1061
1063
  const base = path.basename(filePath);
1062
- return path.join(path.dirname(filePath), `.${base}.dotmd-tmp-${process.pid}-${Date.now()}-${tempSequence++}`);
1064
+ return path.join(path.dirname(filePath), `.${base}${ARTIFACT_PREFIX}tmp-${process.pid}-${Date.now()}-${tempSequence++}`);
1063
1065
  }
1064
1066
 
1065
1067
  function fsyncDirectory(dirPath, options = {}, phase = 'directory') {
@@ -1110,7 +1112,7 @@ function committedGeneration(prepared, finalPath) {
1110
1112
  }
1111
1113
 
1112
1114
  function recoveryArtifact(filePath, content, mode, label) {
1113
- const artifact = path.join(path.dirname(filePath), `.${path.basename(filePath)}.dotmd-recovery-${label}-${randomUUID()}`);
1115
+ const artifact = path.join(path.dirname(filePath), `.${path.basename(filePath)}${ARTIFACT_PREFIX}recovery-${label}-${randomUUID()}`);
1114
1116
  const temp = writeCompleteTemp(artifact, content, mode);
1115
1117
  renameSync(temp.path, artifact);
1116
1118
  fsyncDirectory(path.dirname(artifact));
package/src/commands.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Canonical command grammar. Execution stays in bin/dotmd.mjs; this schema owns
1
+ // Canonical command grammar. Execution starts at bin/runlist.mjs; this schema owns
2
2
  // names, aliases, visibility, options, positional arity, help groups, and policy.
3
3
  const none = Object.freeze({ mutation: 'none', pathPolicy: 'read-only' });
4
4
  const mutates = (pathPolicy) => Object.freeze({ mutation: 'conditional', pathPolicy });
@@ -7,9 +7,9 @@ const COMMAND_WORDS = Object.freeze(Object.fromEntries(
7
7
  ));
8
8
 
9
9
  function bashCompletion() {
10
- return `# dotmd bash completion
11
- # Add to ~/.bashrc: eval "$(dotmd completions bash)"
12
- _dotmd() {
10
+ return `# runlist bash completion
11
+ # Add to ~/.bashrc: eval "$(runlist completions bash)"
12
+ _runlist() {
13
13
  local cur cmd expect_value
14
14
  cur="\${COMP_WORDS[COMP_CWORD]}"
15
15
  cmd=""
@@ -39,13 +39,13 @@ ${Object.entries(COMMAND_WORDS).map(([command, words]) =>
39
39
  *) COMPREPLY=( $(compgen -W "${GLOBAL_FLAGS.join(' ')}" -- "$cur") ) ;;
40
40
  esac
41
41
  }
42
- complete -F _dotmd dotmd`;
42
+ complete -F _runlist runlist dotmd`;
43
43
  }
44
44
 
45
45
  function zshCompletion() {
46
- return `# dotmd zsh completion
47
- # Add to ~/.zshrc: eval "$(dotmd completions zsh)"
48
- _dotmd() {
46
+ return `# runlist zsh completion
47
+ # Add to ~/.zshrc: eval "$(runlist completions zsh)"
48
+ _runlist() {
49
49
  local -a commands global_flags
50
50
  commands=(
51
51
  ${COMPLETION_COMMANDS.map(command => ` '${command}'`).join('\n')}
@@ -81,12 +81,12 @@ ${Object.entries(COMMAND_WORDS).map(([command, words]) =>
81
81
 
82
82
  _describe 'flag' global_flags
83
83
  }
84
- compdef _dotmd dotmd`;
84
+ compdef _runlist runlist dotmd`;
85
85
  }
86
86
 
87
87
  export function runCompletions(argv) {
88
88
  const shell = argv[0];
89
- if (!shell) die('Usage: dotmd completions <bash|zsh>');
89
+ if (!shell) die('Usage: runlist completions <bash|zsh>');
90
90
  if (shell === 'bash') process.stdout.write(bashCompletion() + '\n');
91
91
  else if (shell === 'zsh') process.stdout.write(zshCompletion() + '\n');
92
92
  else die(`Unsupported shell: ${shell}\nSupported: bash, zsh`);
@@ -1,5 +1,5 @@
1
1
  // Line-based, brace-aware editor for the `types.<typename>.statuses` block in
2
- // dotmd.config.mjs. Edits are scoped to single-line status entries; we refuse
2
+ // runlist config. Edits are scoped to single-line status entries; we refuse
3
3
  // (with an actionable error) on multi-line entries, array form, or anything
4
4
  // outside our supported shape. Atomic write contract:
5
5
  //
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
19
19
  import { pathToFileURL } from 'node:url';
20
+ import { ARTIFACT_PREFIX } from './naming.mjs';
20
21
  import { resolveConfig } from './config.mjs';
21
22
  import { commitRename } from './durable-rename.mjs';
22
23
 
@@ -230,7 +231,7 @@ function skipToNextProperty(content, start, end) {
230
231
  export function parseStatusesBlock(content, typeName) {
231
232
  const types = locateTypesBlock(content);
232
233
  if (!types) {
233
- throw new ConfigEditError('Your dotmd.config.mjs does not define a `types` block — there is nothing for `dotmd statuses` to edit. Add a `types: {...}` export to opt in to per-project status taxonomy. See dotmd.config.example.mjs for the rich-form template.');
234
+ throw new ConfigEditError('Your runlist config does not define a `types` block — there is nothing for `runlist statuses` to edit. Add a `types: {...}` export to opt in to per-project status taxonomy. See runlist.config.example.mjs for the rich-form template.');
234
235
  }
235
236
  const typeProp = findChildProperty(content, types.start, types.end, typeName);
236
237
  if (!typeProp) {
@@ -530,7 +531,7 @@ export function replaceEntry(content, parsed, name, newLine) {
530
531
  throw new ConfigEditError(`Status '${name}' is not defined for this type.`);
531
532
  }
532
533
  if (target.multiLine) {
533
- throw new ConfigEditError(`Status '${name}' spans multiple lines; this CLI only edits single-line entries. Edit dotmd.config.mjs by hand.`);
534
+ throw new ConfigEditError(`Status '${name}' spans multiple lines; this CLI only edits single-line entries. Edit the runlist config by hand.`);
534
535
  }
535
536
  return content.slice(0, target.lineStart) + newLine + content.slice(target.lineEnd);
536
537
  }
@@ -542,7 +543,7 @@ export function deleteEntry(content, parsed, name) {
542
543
  throw new ConfigEditError(`Status '${name}' is not defined for this type.`);
543
544
  }
544
545
  if (target.multiLine) {
545
- throw new ConfigEditError(`Status '${name}' spans multiple lines; delete it by hand in dotmd.config.mjs.`);
546
+ throw new ConfigEditError(`Status '${name}' spans multiple lines; delete it by hand in the runlist config.`);
546
547
  }
547
548
  return content.slice(0, target.lineStart) + content.slice(target.lineEnd);
548
549
  }
@@ -566,7 +567,7 @@ export function inferIndent(content, parsed) {
566
567
  export async function writeConfigAtomic(configPath, newContent, cwd) {
567
568
  // Node only imports .mjs/.js/.cjs, so the temp must keep a JS extension.
568
569
  // Sibling file in the same dir → atomic renameSync within one filesystem.
569
- const tmpPath = configPath.replace(/(\.[^.]+)$/, `.dotmd-edit-${process.pid}-${Date.now()}$1`);
570
+ const tmpPath = configPath.replace(/(\.[^.]+)$/, `${ARTIFACT_PREFIX}edit-${process.pid}-${Date.now()}$1`);
570
571
  writeFileSync(tmpPath, newContent, 'utf8');
571
572
 
572
573
  try {
package/src/config.mjs CHANGED
@@ -2,8 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { die, warn } from './util.mjs';
5
-
6
- const CONFIG_FILENAMES = ['dotmd.config.mjs', '.dotmd.config.mjs', 'dotmd.config.js'];
5
+ import { CONFIG_FILENAMES } from './naming.mjs';
7
6
 
8
7
  // Keys where user config replaces defaults entirely (not deep-merged).
9
8
  // These are flat maps or config sections where the user's version is authoritative —
@@ -116,7 +115,7 @@ const DEFAULTS = {
116
115
  glossary: null,
117
116
 
118
117
  // Opt-in JSONL command journal at .dotmd/journal.jsonl. Default off — agents
119
- // and users who want usage observability flip this on (or set DOTMD_JOURNAL=1).
118
+ // and users who want usage observability flip this on (or set RUNLIST_JOURNAL=1).
120
119
  journal: false,
121
120
 
122
121
  // PreToolUse guard behavior. `deny: false` drops the status-edit rules from
@@ -1,3 +1,5 @@
1
+ import { maskInlineCodeSpans } from './markdown-code-spans.mjs';
2
+
1
3
  export function extractFirstHeading(body) {
2
4
  return body.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? null;
3
5
  }
@@ -49,9 +51,7 @@ export function extractBodyLinks(body) {
49
51
  // every one of them was also never checked for breakage. Masking to same-length
50
52
  // filler keeps the link matchable while still neutralizing a link that is
51
53
  // itself inside code (`[fake](x.md)` stays unmatched), and preserves offsets.
52
- const stripped = body
53
- .replace(/^```[\s\S]*?^```/gm, '')
54
- .replace(/`[^`]+`/g, match => 'x'.repeat(match.length));
54
+ const stripped = maskInlineCodeSpans(body.replace(/^```[\s\S]*?^```/gm, ''));
55
55
  const links = [];
56
56
  // Supported inline destinations are a whitespace-free token (with Markdown
57
57
  // backslash escapes) or an angle-bracket destination, plus an optional