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/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # dotmd
1
+ # runlist
2
2
 
3
3
  CLI for managing Markdown documents with YAML frontmatter.
4
4
 
5
- dotmd indexes, queries, validates, graphs, exports, and lifecycle-manages plans,
5
+ runlist (formerly dotmd) indexes, queries, validates, graphs, exports, and lifecycle-manages plans,
6
6
  ADRs, RFCs, design docs, and other structured Markdown. It is built for
7
7
  AI-assisted development workflows where documents need to remain current and
8
8
  safe to mutate.
@@ -22,7 +22,9 @@ npx dotmd-cli init # try it without installing
22
22
 
23
23
  `runlist` is the canonical executable, `rl` is its short convenience alias, and
24
24
  `dotmd` remains supported during the compatibility window. All three invoke the
25
- same CLI; examples below retain `dotmd` while the public package identity does.
25
+ same CLI. The package is still published as `dotmd-cli`, and the Claude Code
26
+ plugin is still `dotmd@dotmd`; those names change in a later release. Legacy
27
+ `dotmd.config.*` files, `DOTMD_*` variables and `.dotmd/` state keep working.
26
28
 
27
29
  Maintainer release automation is POSIX-only because it uses Bash and POSIX
28
30
  command-line tools. The published Node.js CLI remains cross-platform.
@@ -33,19 +35,19 @@ The CLI alone gives an agent no orientation and no session identity. Install the
33
35
  integration for whichever host you run:
34
36
 
35
37
  ```bash
36
- dotmd install # what's installed for each host
37
- dotmd install claude # Claude Code plugin (marketplace + plugin)
38
- dotmd install opencode # OpenCode plugin (one auto-discovered file)
39
- dotmd doctor --session # what identity dotmd sees here, and from where
38
+ runlist install # what's installed for each host
39
+ runlist install claude # Claude Code plugin (marketplace + plugin)
40
+ runlist install opencode # OpenCode plugin (one auto-discovered file)
41
+ runlist doctor --session # what identity runlist sees here, and from where
40
42
  ```
41
43
 
42
- Both are one-time and global; `dotmd update` keeps them in step with the CLI.
44
+ Both are one-time and global; `runlist update` keeps them in step with the CLI.
43
45
  Codex needs no install for identity: it exports `CODEX_THREAD_ID` to every tool
44
- shell, and dotmd reads it as a per-session identity automatically.
46
+ shell, and runlist reads it as a per-session identity automatically.
45
47
 
46
48
  ### Claude Code Plugin
47
49
 
48
- `dotmd install claude` runs the two steps below for you. From inside a session:
50
+ `runlist install claude` runs the two steps below for you. From inside a session:
49
51
 
50
52
  ```text
51
53
  /plugin marketplace add reowens/dotmd
@@ -58,32 +60,33 @@ guard, the canonical workflow skill, and `/plans`, `/docs`, `/prompts`, and
58
60
 
59
61
  ### OpenCode Plugin
60
62
 
61
- `dotmd install opencode` writes one plugin file into OpenCode's global config
63
+ `runlist install opencode` writes one plugin file into OpenCode's global config
62
64
  directory, where OpenCode auto-discovers it — no `opencode.json` edit. It
63
65
  supplies the two things the CLI cannot get on its own:
64
66
 
65
67
  - **Per-session plan ownership.** OpenCode exports no session id to a tool
66
- shell. Without the plugin, dotmd falls back to `OPENCODE_PID`, which names the
68
+ shell. Without the plugin, runlist falls back to `OPENCODE_PID`, which names the
67
69
  OpenCode *process* — so every session in one OpenCode instance shares an
68
70
  identity and can release the others' in-session plans.
69
71
  - **A session-start briefing**, the equivalent of Claude Code's SessionStart
70
72
  hook. OpenCode's Claude Code compatibility covers skills and the system
71
- prompt, not hooks, so nothing else runs `dotmd hud`.
73
+ prompt, not hooks, so nothing else runs `runlist hud`.
72
74
 
73
- Restart OpenCode after installing. The file is version-stamped; a `dotmd.js`
74
- without that stamp is treated as hand-authored and is never overwritten.
75
+ Restart OpenCode after installing. The file is version-stamped (`runlist-generated:`, or
76
+ `dotmd-generated:` from older releases); a `dotmd.js` without either stamp is
77
+ treated as hand-authored and is never overwritten.
75
78
 
76
- The plugin requires a global CLI install because its hooks resolve `dotmd` from
79
+ The plugin requires a global CLI install because its hooks resolve `runlist` (or `dotmd`) from
77
80
  `PATH`. A project devDependency is useful for npm scripts but does not put the
78
81
  CLI on the hook's `PATH`.
79
82
 
80
83
  Keep the CLI and plugin aligned with:
81
84
 
82
85
  ```bash
83
- dotmd update
84
- dotmd update --check
85
- dotmd update --cli-only
86
- dotmd update --plugin-only
86
+ runlist update
87
+ runlist update --check
88
+ runlist update --cli-only
89
+ runlist update --plugin-only
87
90
  ```
88
91
 
89
92
  Restart Claude Code, or run `/reload-plugins`, after a plugin update.
@@ -91,29 +94,29 @@ Restart Claude Code, or run `/reload-plugins`, after a plugin update.
91
94
  ## Quick Start
92
95
 
93
96
  ```bash
94
- dotmd init # create config, docs/, and the generated index
95
- dotmd new plan auth-refresh # scaffold a typed document
96
- dotmd briefing # compact active-work orientation
97
- dotmd plans # live plan dashboard
98
- dotmd check # validate schema, references, and lifecycle shape
99
- dotmd doctor # preview repairs; add --apply to write
97
+ runlist init # create config, docs/, and the generated index
98
+ runlist new plan auth-refresh # scaffold a typed document
99
+ runlist briefing # compact active-work orientation
100
+ runlist plans # live plan dashboard
101
+ runlist check # validate schema, references, and lifecycle shape
102
+ runlist doctor # preview repairs; add --apply to write
100
103
  ```
101
104
 
102
- `dotmd briefing` is the compact orientation view. `dotmd context` is the fuller
103
- human/LLM briefing, while `dotmd agent-context` emits bounded structured JSON for
105
+ `runlist briefing` is the compact orientation view. `runlist context` is the fuller
106
+ human/LLM briefing, while `runlist agent-context` emits bounded structured JSON for
104
107
  agent integrations.
105
108
 
106
109
  ## Core Workflow
107
110
 
108
111
  ```bash
109
- dotmd briefing
110
- dotmd use docs/plans/auth-refresh.md
111
- dotmd set awaiting docs/plans/auth-refresh.md --note "Need API owner decision"
112
- dotmd set active docs/plans/auth-refresh.md --note "Decision received"
113
- dotmd archive docs/plans/auth-refresh.md --note "Shipped and verified"
112
+ runlist briefing
113
+ runlist use docs/plans/auth-refresh.md
114
+ runlist set awaiting docs/plans/auth-refresh.md --note "Need API owner decision"
115
+ runlist set active docs/plans/auth-refresh.md --note "Decision received"
116
+ runlist archive docs/plans/auth-refresh.md --note "Shipped and verified"
114
117
  ```
115
118
 
116
- Use `dotmd set <status> [<file>]` for lifecycle changes rather than editing a
119
+ Use `runlist set <status> [<file>]` for lifecycle changes rather than editing a
117
120
  `status:` line. It validates the status for the document type, updates history,
118
121
  runs lifecycle hooks, repairs references after moves, and synchronizes the
119
122
  index.
@@ -122,11 +125,18 @@ For unfinished session work, save the handoff and release the owned plan in one
122
125
  operation:
123
126
 
124
127
  ```bash
125
- dotmd baton @/tmp/resume.md
128
+ runlist baton @/tmp/resume.md
126
129
  ```
127
130
 
128
- Saved prompts are local session state. Consume them with `dotmd use`; inspect
129
- without consuming via `dotmd prompts show`.
131
+ Baton refuses when a handoff for the same work is already pending, so one piece
132
+ of work never has two resume prompts.
133
+
134
+ Saved prompts are local session state. Consume them with `runlist use`; inspect
135
+ without consuming via `runlist prompts show`. Consuming a baton prompt also claims
136
+ its plan; `runlist use --no-claim` reads and archives it without starting the plan.
137
+
138
+ New plans are created `planned`; `runlist use` starts one, and
139
+ `runlist new plan <name> --status <status>` sets a different starting status.
130
140
 
131
141
  ## Document Format
132
142
 
@@ -153,11 +163,11 @@ related_docs:
153
163
 
154
164
  `status` is the only universally required field. A `type` enables type-specific
155
165
  statuses, validation, templates, and briefing behavior. Explicit frontmatter
156
- wins, but dotmd can also derive titles, summaries, state, next steps, checklist
166
+ wins, but runlist can also derive titles, summaries, state, next steps, checklist
157
167
  progress, and Markdown links from the body.
158
168
 
159
169
  Use plural `modules:` and `surfaces:` arrays. The old singular keys remain
160
- readable for compatibility and can be migrated with `dotmd lint --fix`.
170
+ readable for compatibility and can be migrated with `runlist lint --fix`.
161
171
 
162
172
  ### Built-In Types
163
173
 
@@ -176,19 +186,19 @@ A sprint runlist is an ordered `runlist:` array on a hub plan. Scaffold a hub
176
186
  and children together:
177
187
 
178
188
  ```bash
179
- dotmd new plan auth-revamp --runlist extract,rewrite,cleanup
180
- dotmd runlist auth-revamp
181
- dotmd runlist next auth-revamp
189
+ runlist new plan auth-revamp --runlist extract,rewrite,cleanup
190
+ runlist runlist auth-revamp
191
+ runlist runlist next auth-revamp
182
192
  ```
183
193
 
184
194
  Mutate the structure through the CLI so the array, child `parent_plan` refs, and
185
195
  body order list remain synchronized:
186
196
 
187
197
  ```bash
188
- dotmd runlist add auth-revamp docs/plans/existing-plan.md
189
- dotmd runlist add auth-revamp follow-up
190
- dotmd runlist reorder auth-revamp follow-up --before cleanup
191
- dotmd runlist remove auth-revamp extract --clear-parent
198
+ runlist runlist add auth-revamp docs/plans/existing-plan.md
199
+ runlist runlist add auth-revamp follow-up
200
+ runlist runlist reorder auth-revamp follow-up --before cleanup
201
+ runlist runlist remove auth-revamp extract --clear-parent
192
202
  ```
193
203
 
194
204
  Archived children count as complete. Parked children (`blocked`, `partial`,
@@ -198,16 +208,16 @@ pickup but do not count as done.
198
208
  For a larger prose-first domain map, create a coordination runlist:
199
209
 
200
210
  ```bash
201
- dotmd new plan platform-work --coordination
202
- dotmd runlists
211
+ runlist new plan platform-work --coordination
212
+ runlist runlists
203
213
  ```
204
214
 
205
215
  For progress across several runlists, create a roadmap:
206
216
 
207
217
  ```bash
208
- dotmd new plan platform-roadmap --roadmap
209
- dotmd roadmap platform-roadmap
210
- dotmd roadmap platform-roadmap next
218
+ runlist new plan platform-roadmap --roadmap
219
+ runlist roadmap platform-roadmap
220
+ runlist roadmap platform-roadmap next
211
221
  ```
212
222
 
213
223
  Roadmaps roll up progress recursively and choose the first startable plan across
@@ -229,17 +239,17 @@ counts so dashboards do not double-count their children.
229
239
  The CLI is the source of truth for command syntax and options:
230
240
 
231
241
  ```bash
232
- dotmd --help
233
- dotmd help all
234
- dotmd help statuses
235
- dotmd <command> --help
242
+ runlist --help
243
+ runlist help all
244
+ runlist help statuses
245
+ runlist <command> --help
236
246
  ```
237
247
 
238
248
  Shell completion is generated from the same command registry:
239
249
 
240
250
  ```bash
241
- eval "$(dotmd completions bash)"
242
- eval "$(dotmd completions zsh)"
251
+ eval "$(runlist completions bash)"
252
+ eval "$(runlist completions zsh)"
243
253
  ```
244
254
 
245
255
  This README intentionally documents onboarding and concepts instead of
@@ -247,7 +257,7 @@ duplicating the complete command catalog.
247
257
 
248
258
  ## Configuration
249
259
 
250
- Run `dotmd init` to create `dotmd.config.mjs`. A minimal typed configuration:
260
+ Run `runlist init` to create `runlist.config.mjs` (a legacy `dotmd.config.mjs` is still read). A minimal typed configuration:
251
261
 
252
262
  ```js
253
263
  export const root = 'docs';
@@ -273,12 +283,12 @@ export const types = {
273
283
 
274
284
  Configuration supports multiple roots, custom types and templates, taxonomy,
275
285
  reference fields, presets, rendering, lifecycle hooks, validation hooks, and
276
- AI summarization hooks. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs)
286
+ AI summarization hooks. See [`runlist.config.example.mjs`](runlist.config.example.mjs)
277
287
  for the complete annotated reference.
278
288
 
279
289
  ## Hooks
280
290
 
281
- Functions exported from `dotmd.config.mjs` are detected as hooks. They can add
291
+ Functions exported from `runlist.config.mjs` are detected as hooks. They can add
282
292
  validation, customize rendering and summaries, or react to lifecycle events.
283
293
  Hooks receive the resolved config and command context; mutation hooks participate
284
294
  in the command's dry-run and failure contracts.
@@ -1,4 +1,4 @@
1
- // dotmd's OpenCode integration. Installed by `dotmd install opencode`, which
1
+ // runlist's OpenCode integration. Installed by `runlist install opencode`, which
2
2
  // copies this file (with a version banner prepended) to the OpenCode plugin
3
3
  // directory, where OpenCode auto-discovers it — it globs
4
4
  // `{plugin,plugins}/*.{ts,js}` under `.opencode/` and the global config dir, so
@@ -13,7 +13,7 @@
13
13
  //
14
14
  // 2. NO HOOK MAY THROW. OpenCode awaits hook callbacks inside the request it
15
15
  // is serving; a rejected promise fails the user's chat turn. Every hook
16
- // body is wrapped, and a failure degrades to "dotmd does nothing here"
16
+ // body is wrapped, and a failure degrades to "runlist does nothing here"
17
17
  // rather than to a broken session.
18
18
  //
19
19
  // Runs under Bun inside the OpenCode process. Node builtins only, no deps.
@@ -32,7 +32,9 @@ function cliExecutables() {
32
32
  return process.platform === 'win32' ? ['runlist.cmd', 'dotmd.cmd'] : ['runlist', 'dotmd'];
33
33
  }
34
34
 
35
- function runHud(directory) {
35
+ // Runs the CLI and resolves its trimmed stdout, or '' on any failure. `input`,
36
+ // when given, is written to the child's stdin.
37
+ function runCli(directory, args, input = null) {
36
38
  return new Promise(resolve => {
37
39
  let settled = false;
38
40
  const done = value => { if (!settled) { settled = true; resolve(value); } };
@@ -40,7 +42,7 @@ function runHud(directory) {
40
42
  const attempt = index => {
41
43
  if (index >= candidates.length) { done(''); return; }
42
44
  try {
43
- execFile(candidates[index], ['hud'], {
45
+ const child = execFile(candidates[index], args, {
44
46
  cwd: directory,
45
47
  timeout: PRIMER_TIMEOUT_MS,
46
48
  windowsHide: true,
@@ -49,12 +51,33 @@ function runHud(directory) {
49
51
  if (error?.code === 'ENOENT') attempt(index + 1);
50
52
  else done(error ? '' : (stdout ?? '').trim());
51
53
  });
54
+ child.stdin?.on('error', () => {});
55
+ if (input !== null) child.stdin?.end(input);
56
+ else child.stdin?.end();
52
57
  } catch { attempt(index + 1); }
53
58
  };
54
59
  attempt(0);
55
60
  });
56
61
  }
57
62
 
63
+ // OpenCode runs no Claude Code hooks, so `runlist guard` never sees its tool
64
+ // calls, and sessions opened pending prompts with the read tool, which prints a
65
+ // prompt without archiving it. The guard's answer for a prompt read is a
66
+ // warning, not a block, so it is applied after the call: the teaching text is
67
+ // appended to what the agent sees. Only calls that name a prompt file reach
68
+ // the CLI; everything else costs a regex.
69
+ const PROMPT_FILE = /(^|[\\/])prompts[\\/]\S*\.md\b/;
70
+
71
+ function guardPayload(tool, args) {
72
+ if (tool === 'read' && typeof args?.filePath === 'string' && PROMPT_FILE.test(args.filePath)) {
73
+ return { tool_name: 'Read', tool_input: { file_path: args.filePath } };
74
+ }
75
+ if (tool === 'bash' && typeof args?.command === 'string' && PROMPT_FILE.test(args.command)) {
76
+ return { tool_name: 'Bash', tool_input: { command: args.command } };
77
+ }
78
+ return null;
79
+ }
80
+
58
81
  export default async function dotmdOpencodePlugin({ directory }) {
59
82
  // Keyed by session so a subagent session primes independently, the way
60
83
  // SubagentStart does under Claude Code.
@@ -68,14 +91,14 @@ export default async function dotmdOpencodePlugin({ directory }) {
68
91
  // only the first request — and a never-refreshed one would keep announcing
69
92
  // a pending prompt this session already consumed.
70
93
  if (cached && Date.now() - cached.at < PRIMER_TTL_MS) return cached.text;
71
- const text = await runHud(directory);
94
+ const text = await runCli(directory, ['hud']);
72
95
  primers.set(key, { text, at: Date.now() });
73
96
  return text;
74
97
  }
75
98
 
76
99
  return {
77
100
  // Ownership identity. OpenCode sets no session-id variable of its own, and
78
- // `OPENCODE_PID` — what dotmd falls back to without this plugin — names the
101
+ // `OPENCODE_PID` — what runlist falls back to without this plugin — names the
79
102
  // OpenCode *process*, so every session in one TUI shares it and can release
80
103
  // the others' plans. This is the only place the real session id is
81
104
  // available to a tool shell.
@@ -83,18 +106,32 @@ export default async function dotmdOpencodePlugin({ directory }) {
83
106
  try {
84
107
  if (input?.sessionID) {
85
108
  output.env.RUNLIST_SESSION_ID = `opencode:${input.sessionID}`;
109
+ // Legacy name, kept for an older CLI (before 0.77.0) still on PATH.
86
110
  output.env.DOTMD_SESSION_ID = `opencode:${input.sessionID}`;
87
111
  }
88
112
  // The OpenCode server process hosts the session and outlives every tool
89
113
  // shell, so it is the process whose liveness answers "is this claim's
90
- // owner still there?" — `dotmd doctor --claims` probes exactly this.
114
+ // owner still there?" — `runlist doctor --claims` probes exactly this.
91
115
  output.env.RUNLIST_SESSION_PID = String(process.pid);
92
116
  output.env.DOTMD_SESSION_PID = String(process.pid);
93
117
  } catch { /* never break a shell over this */ }
94
118
  },
95
119
 
120
+ // The guard's warnings, appended after the call ran. A `deny` from the
121
+ // guard is ignored here: the call has already happened, so there is
122
+ // nothing left to refuse.
123
+ 'tool.execute.after': async (input, output) => {
124
+ try {
125
+ const payload = guardPayload(input?.tool, input?.args);
126
+ if (!payload || typeof output?.output !== 'string') return;
127
+ const raw = await runCli(directory, ['guard'], JSON.stringify(payload));
128
+ const note = raw ? JSON.parse(raw)?.hookSpecificOutput?.additionalContext : null;
129
+ if (typeof note === 'string' && note) output.output += `\n\n${note}`;
130
+ } catch { /* a guard failure never touches the tool result */ }
131
+ },
132
+
96
133
  // Session priming — the equivalent of the SessionStart hook that runs
97
- // `dotmd hud` under Claude Code. Silent outside a dotmd repo (hud prints
134
+ // `runlist hud` under Claude Code. Silent outside a runlist repo (hud prints
98
135
  // nothing and exits 0), so this is inert in unrelated projects.
99
136
  'experimental.chat.system.transform': async (input, output) => {
100
137
  try {