runlist 0.0.0 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1114 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +292 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/query.mjs +977 -0
  136. package/src/record-action.mjs +11 -0
  137. package/src/record-create.mjs +82 -0
  138. package/src/reference-planner.mjs +388 -0
  139. package/src/reference-values.mjs +11 -0
  140. package/src/rename.mjs +107 -0
  141. package/src/render.mjs +678 -0
  142. package/src/roadmap.mjs +184 -0
  143. package/src/runlist.mjs +944 -0
  144. package/src/section.mjs +225 -0
  145. package/src/semantic-search-client.mjs +58 -0
  146. package/src/ship.mjs +166 -0
  147. package/src/show.mjs +101 -0
  148. package/src/skill-drift.mjs +98 -0
  149. package/src/source-editor.mjs +491 -0
  150. package/src/stages.mjs +73 -0
  151. package/src/state-migration.mjs +123 -0
  152. package/src/stats.mjs +200 -0
  153. package/src/status-metadata.mjs +87 -0
  154. package/src/statuses.mjs +721 -0
  155. package/src/summary.mjs +74 -0
  156. package/src/surfaces.mjs +28 -0
  157. package/src/sync-status.mjs +316 -0
  158. package/src/template-store.mjs +62 -0
  159. package/src/update.mjs +338 -0
  160. package/src/use.mjs +58 -0
  161. package/src/util.mjs +337 -0
  162. package/src/validate.mjs +794 -0
  163. package/src/watch.mjs +56 -0
  164. package/src/xref.mjs +406 -0
  165. package/src/yardstick-command.mjs +178 -0
  166. package/src/yardstick.mjs +112 -0
@@ -0,0 +1,2714 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, statSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import path from 'node:path';
6
+ import { resolveConfig } from '../src/config.mjs';
7
+ import { readPlanStage } from '../src/stages.mjs';
8
+ import { scopeFilingCoverage } from '../src/filing.mjs';
9
+ import { die, warn, levenshtein, isArchivedPath, toRepoPath } from '../src/util.mjs';
10
+ import { recordCliInvocation, recordGlobalError, sanitizeTelemetryArgv } from '../src/journal.mjs';
11
+ import { findRepeatFailureHint } from '../src/hints.mjs';
12
+ import {
13
+ KNOWN_COMMANDS,
14
+ canonicalCommand,
15
+ commandOwnsOption,
16
+ commandPolicy,
17
+ commandUsage,
18
+ validateCommandArgs,
19
+ } from '../src/commands.mjs';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
24
+
25
+ const QUERY_VALUE_FLAGS = new Set([
26
+ '--type', '--status', '--keyword', '--owner', '--surface', '--module',
27
+ '--domain', '--audience', '--execution-mode', '--updated-since', '--limit',
28
+ '--sort', '--group', '--group-by', '--stage', '--summarize-limit', '--model',
29
+ ]);
30
+
31
+ function requireCommandPolicy(command, policy) {
32
+ if (policy) return policy;
33
+ const matches = KNOWN_COMMANDS
34
+ .map(cmd => ({ cmd, dist: levenshtein(command, cmd) }))
35
+ .sort((a, b) => a.dist - b.dist);
36
+ if (matches[0] && matches[0].dist <= 3) {
37
+ die(`Unknown command: ${command}\n\nDid you mean \`runlist ${matches[0].cmd}\`?`);
38
+ }
39
+ die(`Unknown command: ${command}\n\nRun \`runlist --help\` for available commands.`);
40
+ }
41
+
42
+ function resolveExistingPath(input, config) {
43
+ if (!input) return null;
44
+ const candidates = [];
45
+ if (path.isAbsolute(input)) {
46
+ candidates.push(input);
47
+ if (!input.endsWith('.md')) candidates.push(`${input}.md`);
48
+ } else {
49
+ candidates.push(path.resolve(config.repoRoot, input));
50
+ if (!input.endsWith('.md')) candidates.push(path.resolve(config.repoRoot, `${input}.md`));
51
+ for (const root of config.docsRoots || [config.docsRoot]) {
52
+ candidates.push(path.resolve(root, input));
53
+ if (!input.endsWith('.md')) candidates.push(path.resolve(root, `${input}.md`));
54
+ }
55
+ }
56
+ return candidates.find(candidate => existsSync(candidate)) ?? null;
57
+ }
58
+
59
+ function applyPathScopeToIndex(index, config, inputs) {
60
+ if (!inputs.length) return;
61
+
62
+ const selected = new Set();
63
+ for (const input of inputs) {
64
+ const resolved = resolveExistingPath(input, config);
65
+ if (!resolved) die(`Could not resolve check path: ${input}`);
66
+
67
+ const stat = statSync(resolved);
68
+ if (stat.isDirectory()) {
69
+ const dir = path.resolve(resolved);
70
+ const before = selected.size;
71
+ for (const doc of index.docs) {
72
+ const abs = path.resolve(config.repoRoot, doc.path);
73
+ if (abs === dir || abs.startsWith(dir + path.sep)) selected.add(doc.path);
74
+ }
75
+ if (selected.size === before) {
76
+ die(`No runlist documents found under check path: ${toRepoPath(dir, config.repoRoot)}`);
77
+ }
78
+ continue;
79
+ }
80
+
81
+ if (!stat.isFile() || !resolved.endsWith('.md')) die(`Check path is not a markdown file or directory: ${input}`);
82
+ const repoPath = toRepoPath(resolved, config.repoRoot);
83
+ if (!index.docs.some(d => d.path === repoPath)) {
84
+ die(`Check path is outside configured docs roots: ${repoPath}`);
85
+ }
86
+ selected.add(repoPath);
87
+ }
88
+
89
+ if (index.scanCoverage?.failures) {
90
+ const failures = index.scanCoverage.failures;
91
+ index.scanCoverage = {
92
+ complete: !failures.some(f => !f.path || selected.has(f.path)),
93
+ failures: failures.filter(f => !f.path || selected.has(f.path)),
94
+ excludedFailures: failures.filter(f => f.path && !selected.has(f.path)),
95
+ };
96
+ }
97
+ index.docs = index.docs.filter(d => selected.has(d.path));
98
+ index.errors = index.errors.filter(e => selected.has(e.path));
99
+ index.warnings = index.warnings.filter(w => selected.has(w.path));
100
+ index.countsByStatus = {};
101
+ index.countsByType = {};
102
+ for (const doc of index.docs) {
103
+ const status = doc.status ?? 'unknown';
104
+ index.countsByStatus[status] = (index.countsByStatus[status] ?? 0) + 1;
105
+ const type = doc.type || 'unknown';
106
+ if (!index.countsByType[type]) index.countsByType[type] = {};
107
+ index.countsByType[type][status] = (index.countsByType[type][status] ?? 0) + 1;
108
+ }
109
+ }
110
+
111
+ const HELP = {
112
+ _main: `runlist v${pkg.version} — frontmatter markdown document manager
113
+
114
+ Common commands:
115
+ plans Live plans (excludes archived)
116
+ prompts Prompt queue/admin (list, next, archive, new)
117
+ briefing Full briefing with plan counts + next steps
118
+ agent-context Compact bounded JSON context for agents
119
+ set <status> [file] Transition status (start work, finish, archive — all via target status)
120
+ new <type> <name> Create plan/doc/prompt (pipe stdin or @path for body)
121
+ use [<file-or-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
122
+ (no file: consume oldest pending prompt)
123
+ baton [<plan>|<slug>] @<draft-file>
124
+ Save a resume prompt (+ release the plan, if one is in-session)
125
+ archive <file> Close out a plan (status → archived, move, update refs)
126
+
127
+ More help:
128
+ runlist help all Full command list
129
+ runlist help statuses Status vocabulary + transitions
130
+ runlist <cmd> --help Per-command details
131
+
132
+ Global flags: --config <path> --root <name> --type <t,…> --dry-run/-n --verbose --version`,
133
+
134
+ guard: `runlist guard — PreToolUse hook handler (reads the tool-call JSON on stdin)
135
+
136
+ Wire it into Claude Code as a PreToolUse hook to intercept the wrong-moves
137
+ sessions keep making, and to log every one for audit:
138
+
139
+ {"matcher":"Bash|Read|Edit|Write","hooks":[{"type":"command","command":"runlist guard"}]}
140
+
141
+ Rules:
142
+ commit-prompt deny git add/commit of a (often gitignored) saved prompt
143
+ cat-prompt warn cat/less/head of a docs/prompts/*.md (use \`runlist use\`)
144
+ read-prompt warn Read tool on a saved prompt (use \`runlist use\`)
145
+ edit-status deny CHANGING a \`status:\` line — via Edit/Write or in-place
146
+ stream editors (sed -i, perl -pi, awk -i inplace).
147
+ Use \`runlist set <status> <file>\`. Edits that merely
148
+ carry an unchanged status: line as context don't fire.
149
+
150
+ \`guard: { deny: false }\` in runlist.config.mjs drops edit-status back to
151
+ warn-only. Every catch is appended to the cross-repo misuse log. Disable the
152
+ guard entirely with RUNLIST_GUARD=0. Read the log with \`runlist misuse\`; when one
153
+ rule trips ≥3× in 7 days in a repo, \`runlist hud\` opens the next session there
154
+ with a one-line recap naming the habit to break.`,
155
+
156
+ install: `runlist install [<host>] — install runlist's integration into an agent host
157
+
158
+ runlist install report what is installed for each known host
159
+ runlist install claude install the Claude Code plugin (marketplace + plugin)
160
+ runlist install codex install/refresh the Codex plugin (personal marketplace)
161
+ runlist install opencode install/refresh the OpenCode plugin (global config dir)
162
+ runlist install opencode --remove
163
+ runlist install opencode --path <dir> write to a specific plugin directory
164
+ runlist install opencode --force overwrite a runlist.js runlist did not write
165
+
166
+ The CLI on its own gives an agent no orientation and no session identity. Each
167
+ host gets that a different way:
168
+
169
+ claude Drives \`claude plugin marketplace add ${'reowens/runlist'}\` +
170
+ \`claude plugin install runlist@runlist\`. This is the FIRST install;
171
+ \`runlist update\` only refreshes a plugin already present (it skips
172
+ with "plugin not installed"), and the README's slash commands only
173
+ work from inside a session. Without the \`claude\` CLI on PATH the
174
+ two in-session commands are printed instead.
175
+ It also repairs a plugin Claude lists as "failed to load: Marketplace
176
+ runlist not found" — an install record whose marketplace registration
177
+ is gone — by re-adding the marketplace and updating the plugin. If
178
+ Claude refuses the marketplace, ~/.claude/settings.json declares it
179
+ under extraKnownMarketplaces with a source that no longer matches;
180
+ runlist names the field but never edits that file.
181
+
182
+ codex Copies the npm-packaged runlist-codex plugin into the personal
183
+ marketplace and invokes \`codex plugin add\`. It supplies a compact
184
+ skill, SessionStart orientation, and a PreToolUse guard. Codex
185
+ requires a separate hook trust review; start a new thread after
186
+ installing. CODEX_THREAD_ID remains the ownership identity.
187
+
188
+ opencode Writes one auto-discovered plugin file. OpenCode has no plugin
189
+ registry but globs \`{plugin,plugins}/*.{ts,js}\` under its global
190
+ config dir, so no opencode.json edit is needed. It supplies two
191
+ things the CLI cannot get on its own:
192
+ - Per-session ownership identity. OpenCode exports no session id
193
+ to a tool shell, so without it every session in one OpenCode
194
+ process shares one identity and can release the others'
195
+ in-session plans.
196
+ - The \`runlist hud\` primer at session start, the equivalent of
197
+ Claude Code's SessionStart hook. OpenCode's Claude Code
198
+ compatibility covers skills and the system prompt — not hooks —
199
+ so nothing else runs it.
200
+ The file is version-stamped and refreshed by \`runlist update\`. A
201
+ \`runlist.js\` without that stamp is treated as hand-authored and is
202
+ never overwritten or removed without --force.
203
+
204
+ Writing outside the repo is why this is an explicit verb: \`runlist doctor\`
205
+ reports a missing integration but never installs one.`,
206
+
207
+ update: `runlist update — update the runlist CLI and the Claude Code plugin together
208
+
209
+ runlist update npm i -g runlist + claude plugin update runlist@runlist
210
+ runlist update --check report CLI vs plugin versions, do nothing (network-free)
211
+ runlist update --cli-only only the npm CLI
212
+ runlist update --plugin-only only the plugin
213
+
214
+ The plugin and CLI ship in lockstep; a release bumps both. Updating the plugin
215
+ requires a session restart (or /reload-plugins) to apply. The plugin step needs
216
+ the \`claude\` CLI on PATH — otherwise it prints the \`/plugin update\` command to
217
+ run from a session instead. The OpenCode file is refreshed in the same run when
218
+ it is present and behind. The hosts are independent, so a failing step does not
219
+ stop the others: every step runs, the failures are listed together, and the
220
+ exit code is 1. A plugin whose marketplace registration is gone gets the
221
+ marketplace re-added before the update (see \`runlist install claude\`).`,
222
+
223
+ errors: `runlist errors — the newest failed runlist commands, newest first
224
+
225
+ Every runlist command that fails (an error, or a non-zero exit such as
226
+ \`check\` finding errors) appends one line to ~/.claude/logs/runlist-errors.log
227
+ (RUNLIST_ERROR_LOG_DIR moves it): when, the command with secrets redacted, and
228
+ the error's one-line message. It rolls over at 5 MB or a new version, retaining
229
+ up to eight backups for 30 days; this reads all retained files. Dry runs and the session-start hud are
230
+ never logged.
231
+
232
+ runlist errors last 20
233
+ runlist errors --limit 50 last N (--tail is the same)
234
+ runlist errors --repo <name> only failures in a matching repo
235
+ runlist errors --by-family group all retained failures by actionable family
236
+ runlist errors --json [{ at, command, message, repo, exit }]`,
237
+
238
+ misuse: `runlist misuse — read the cross-repo guard log (~/.claude/logs/runlist-misuse.log,
239
+ merged by time with the legacy dotmd-misuse.log that older CLIs wrote)
240
+
241
+ runlist misuse last 20 intercepted wrong-moves
242
+ runlist misuse --tail 50 last N
243
+ runlist misuse --by-rule counts per rule (deny/warn split)
244
+ runlist misuse --repo <name> filter by repo
245
+ runlist misuse --json machine-readable
246
+
247
+ Populated by the \`runlist guard\` PreToolUse hook — see \`runlist help guard\`.`,
248
+
249
+ // Full command list — opt-in via \`runlist help all\`. Kept exhaustive so the
250
+ // top-level \`--help\` can stay terse without losing discoverability. When you
251
+ // add a new command, add it here too.
252
+ 'help:all': `runlist v${pkg.version} — full command list
253
+
254
+ View & Query:
255
+ hud [--json] Command primer + pending-prompt triage — silent when clean
256
+ list [--verbose] [--json] List docs grouped by status (default command)
257
+ briefing [--json] Full briefing with plan status counts + next steps
258
+ context [--summarize] [--json] Full briefing (LLM-oriented; use --json --compact for bounded JSON)
259
+ agent-context [--json] Compact bounded JSON context for agents
260
+ focus [status] [--json] Detailed view for one status group
261
+ query [filters] [--json] Filtered search (--status, --keyword, --body, --stale, etc.)
262
+ grep <term> Keyword search incl. document bodies (query --keyword --body --all)
263
+ plans Live plans (excludes archived; --include-archived for all)
264
+ app [document] [--port 5173] Local Markdown document library with reviewed saves and draft recovery
265
+ use [<file-or-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
266
+ baton [<plan>|<slug>] @<draft> Save a resume prompt; releases the plan + prints the commit when one is in-session
267
+ prompts [list|show|archive|new] Prompt admin (list / peek / archive / save). Use \`runlist use\` to consume.
268
+ stale Stale docs (preset)
269
+ actionable Docs with next steps (preset)
270
+
271
+ Analyze:
272
+ stats [--json] Doc health dashboard
273
+ health [--json] Plan velocity, aging, and pipeline health
274
+ coverage [--json] Metadata and configured filing coverage
275
+ graph [--dot] [--json] Visualize document relationships
276
+ deps [file] [--json] Dependency tree or overview
277
+ modules [--sort cleanup] [--json] Module dashboard (plans grouped by module)
278
+ module <name> [--json] Plans for one module, grouped by status
279
+ surfaces [--json] List configured surface taxonomy
280
+ unblocks <file> [--json] Show what completes when this doc ships
281
+ diff [file] [--summarize] Show changes since last updated date
282
+ summary <file> [--json] AI summary of a document
283
+ model [start|stop|use|cap] The local model: server, which model, memory cap
284
+ glossary <term> [--list] [--json] Look up domain terms + related docs
285
+ record <file> <action> --note <reason> Act on a native flag or decision
286
+ decision <file> <id> --disposition <state> --note <reason> Record an outcome
287
+ decisions [doc|id] [--all|--check] Open and held decisions, read from the corpus
288
+ show <file...> [--json] One document's card: status, next step, related plans and docs, links
289
+ xref <file...> | --check [--flag] Cross-references: what names it, its decisions, its cited code
290
+
291
+ Validate & Fix:
292
+ doctor [--apply] Auto-fix everything: refs, membership, lint, long fields, dates, index (preview by default)
293
+ doctor --transactions Report/clear wedged mutation transactions (run this if mutations refuse repo-wide)
294
+ doctor --claims Report/release plan claims held by sessions that are gone ("busy in another session")
295
+ self-check Project/version skew diagnostic (alias: doctor --project)
296
+ lint [--fix] Check and auto-fix frontmatter issues
297
+ fix-refs [--dry-run] Auto-fix broken reference paths + body links
298
+ refs <old> <new> [--fix] Report (or rewrite) code-root citations of a moved document
299
+ refs repair [--fix] Same, for every archived document still cited at its old path
300
+ fix-membership [<hub>...] Add unambiguous missing child parent_plan back-references
301
+ sync-status [<hub>...] [--adopt] Rewrite hub table rows whose printed status drifted from the plan
302
+
303
+ Lifecycle:
304
+ use <file> Open a plan (mark in-session + print it) or consume a prompt
305
+ set <status> <file> Change a document's status (frontmatter write; archive also moves the file)
306
+ runlist <hub> [next|add|remove|reorder] Show, walk, or mutate an ordered group of plans (see \`runlist help runlist\`)
307
+ runlists List coordination-hub runlists (the Runlists dashboard)
308
+ roadmap [<hub>] [next] Tier-3: show a roadmap (runlists + rolled-up progress), or pick up its next action
309
+ roadmaps List roadmap hubs (the Roadmaps dashboard)
310
+ status <file> <status> Transition document status (deprecated; prefer \`set\`)
311
+ archive <file> Archive (status + move + update refs)
312
+ yardstick show [plan] Read the product goal beside a plan's delivery
313
+ yardstick mark|close|fold|clear Record an explicit owner assessment
314
+ bulk archive <f1> <f2> ... Archive multiple files at once
315
+ ship [patch|minor|major] Regen + commit + bump in one step (default: patch)
316
+ bulk-tag [files...] Tag pre-existing untagged .md files
317
+ touch <file> Bump updated date
318
+ touch --git [<file>...] Sync dates from substantive git history
319
+ rename <old> <new> Rename doc and update all references
320
+ migrate <field> <old> <new> [f...]Batch update a frontmatter field value (optional file filter)
321
+
322
+ Create & Export:
323
+ new <type> <name> [body] Create doc of given type (plan, doc, prompt)
324
+ index [--print] Generate/update docs.md index block
325
+ export [--format md|html|json] Export docs as markdown, HTML, or JSON
326
+
327
+ Setup:
328
+ init Create starter config + docs directory
329
+ install [claude|codex|opencode] Install the agent-host integration (no arg = status)
330
+ update [--check|--cli-only|--plugin-only] Update the CLI + Claude Code plugin (--check reports skew, no network)
331
+ statuses [list|add|set|remove|migrate] Manage per-project status taxonomy
332
+ help statuses Full status vocabulary + unstuck-actions + transitions
333
+ watch [command] Re-run a command on file changes
334
+ desktop [install|--app <path>] Install or open the optional native GUI
335
+ completions <shell> Shell completion script (bash, zsh)
336
+ journal [--tail N|--errors|--by-command|--session id|--since iso|--json]
337
+ View opt-in JSONL command journal (enable: RUNLIST_JOURNAL=1 or journal: true)
338
+ errors [--limit N] [--json] The newest failed runlist commands, from the cross-repo error log
339
+
340
+ Global Options:
341
+ --config <path> Explicit config file path
342
+ --root <name> Filter to a specific docs root
343
+ --type <t1,t2> Filter by document type (plan, doc, prompt)
344
+ --dry-run, -n Preview changes without writing anything
345
+ --verbose Show config details and doc count
346
+ --help, -h Show help (per-command: runlist <cmd> --help)
347
+ --version, -v Show version`,
348
+
349
+ list: `runlist list — list docs grouped by status
350
+
351
+ Options:
352
+ --verbose Show full details per doc
353
+ --json Output full index as JSON (same as runlist json)`,
354
+
355
+ json: `runlist json — full index as JSON
356
+
357
+ Outputs the complete document index as JSON to stdout.`,
358
+
359
+ // Help topic accessed via \`runlist help statuses\` (not a command — see dispatch
360
+ // below). Single-source-of-truth for the built-in status vocabulary across all
361
+ // three doc types. User-defined types/statuses live in config; introspect them
362
+ // with \`runlist statuses list\`.
363
+ 'help:statuses': `runlist help statuses — status vocabulary, unstuck-actions, and transitions
364
+
365
+ Every document has a \`type:\` field; each type has its own valid statuses.
366
+ Status validation is type-aware (type > root > global). To inspect or edit
367
+ the status taxonomy in a specific project, use \`runlist statuses list\`.
368
+
369
+ ────────────────────────────────────────────────────────────────────
370
+ plan statuses (each maps to a distinct unstuck-action)
371
+
372
+ in-session A Claude session is working on it now.
373
+ \`runlist use <file>\` marks it in-session and prints the plan.
374
+
375
+ active Ready to be worked on.
376
+ \`runlist use <file>\` → in-session.
377
+
378
+ planned Queued for future work, not yet ready to execute.
379
+ Transition to active when ready to start.
380
+
381
+ blocked External arrival wait — monitor.
382
+ Hardware, vendor delivery, third-party rollout. Quiet
383
+ (skipStale) — you can't speed it up by nagging.
384
+
385
+ partial Shipped + deferred tail — spawn successor plans.
386
+ Plan body should reference the successor plan(s). Quiet.
387
+
388
+ paused Started but stopped mid-work — re-evaluate to resume.
389
+ Short stale window (3 days) so resume-decisions don't decay.
390
+
391
+ awaiting Needs human input/decision — chase the answer.
392
+ NOT quiet — generates stale pressure so pings aren't forgotten.
393
+
394
+ queued-after Sequenced behind another plan — check predecessor.
395
+ Quiet. Can start once the predecessor ships.
396
+
397
+ archived No longer relevant; auto-moved to archive directory.
398
+
399
+ Canonical transitions:
400
+ active → in-session \`runlist use <file>\` (or \`runlist set in-session <file>\`)
401
+ in-session → active \`runlist set active <file>\`
402
+ in-session → partial \`runlist set partial <file>\`
403
+ in-session → awaiting \`runlist set awaiting <file>\`
404
+ any → archived \`runlist set archived <file>\` (or \`runlist archive\`)
405
+
406
+ ────────────────────────────────────────────────────────────────────
407
+ doc statuses
408
+
409
+ draft Work-in-progress reference doc.
410
+ active Living document, kept up-to-date.
411
+ review Awaiting peer review.
412
+ reference Stable canonical reference (excluded from stale checks).
413
+ deprecated Superseded but kept for history.
414
+ archived No longer relevant; moved to archive directory.
415
+
416
+ ────────────────────────────────────────────────────────────────────
417
+ prompt statuses
418
+
419
+ A prompt has two states and no third.
420
+
421
+ pending Ready for the next session to consume.
422
+ \`runlist prompts use <file>\` prints body + archives atomically.
423
+ \`runlist prompts next\` does the same for the oldest pending.
424
+
425
+ archived Consumed prompt; body preserved in the archive directory,
426
+ which is the only directory a prompt ever moves into.
427
+
428
+ \`held\`, \`shelved\` and \`claimed\` were removed 2026-08-30, along with
429
+ \`prompts hold\` / \`unhold\` / \`shelve\` / \`unshelve\` and the
430
+ prompts/held/ bucket. A prompt directs work, so parking one instead of
431
+ archiving it left work state outside the plan that owns it, fighting with
432
+ that plan's own status. If a prompt should not be consumed, lift its content
433
+ into the plan and archive the prompt.
434
+
435
+ ────────────────────────────────────────────────────────────────────
436
+ Related commands:
437
+ runlist statuses Inspect/manage per-project status taxonomy
438
+ runlist status <f> <new> Transition a document's status
439
+ runlist briefing See plans grouped by status
440
+ runlist plans --status <s> Filter live plans by status
441
+ runlist hud Command primer + pending-prompt triage
442
+
443
+ Run \`runlist statuses list --type plan\` to see the full set (including any
444
+ project-specific custom statuses) with their flags.`,
445
+
446
+ completions: `runlist completions <bash|zsh> — output shell completion script
447
+
448
+ Add to your shell config:
449
+ bash: eval "$(runlist completions bash)"
450
+ zsh: eval "$(runlist completions zsh)"`,
451
+
452
+ journal: `runlist journal — view opt-in command-usage journal
453
+
454
+ runlist's primary user is an agent (per docs/audit-example-project.md F17),
455
+ but the CLI gives no usage signal by default. Turn on the journal and every
456
+ invocation appends one JSONL line to .runlist/journal.jsonl with argv, exit
457
+ code, elapsed ms, session id, and (on error) a single-line err message.
458
+
459
+ Enable:
460
+ - env: RUNLIST_JOURNAL=1
461
+ - config: \`export const journal = true;\` in runlist.config.mjs
462
+
463
+ The env var beats config (RUNLIST_JOURNAL=0 forces off). The journal is
464
+ default-off so non-agent users don't pay the size/PII cost.
465
+
466
+ Reader options:
467
+ --tail N Last N entries (default: 20 when no other filter)
468
+ --errors Only non-zero exits
469
+ --session <id> Only entries from one session
470
+ --since <iso> Only entries with ts >= iso
471
+ --by-command Group by argv[0]: count, median ms, error rate
472
+ --help-topics Help topic counts, sessions, versions, retained denominator
473
+ --json Emit selected entries as a JSON array
474
+
475
+ Storage:
476
+ Rotates to .runlist/journal.jsonl.1 on runlist version change, at >5MB,
477
+ or when the oldest entry is >30 days.
478
+ Up to eight backups are retained for 30 days; readers include them.
479
+ Help calls record only topic and outcome, without authored arguments.
480
+ Passive context, HUD, and dry runs remain excluded from the journal.
481
+
482
+ Examples:
483
+ RUNLIST_JOURNAL=1 runlist plans
484
+ runlist journal --tail 5
485
+ runlist journal --errors
486
+ runlist journal --by-command
487
+ runlist journal --help-topics --json
488
+ runlist journal --since 2025-01-01 --json`,
489
+
490
+ query: `runlist query — filtered document search
491
+
492
+ Filters:
493
+ --type <t1,t2> Filter by type (plan, doc, prompt)
494
+ --status <s1,s2> Filter by status (comma-separated)
495
+ --keyword <term> Search title, summary, state, path
496
+ --body Extend --keyword into document bodies (lazy scan, shows matching-line excerpts)
497
+ --module <name> Filter by module
498
+ --surface <name> Filter by surface
499
+ --domain <name> Filter by domain
500
+ --owner <name> Filter by owner
501
+ --updated-since <date> Only docs updated after date
502
+ --stale Only stale docs
503
+ --has-next-step Only docs with a next step
504
+ --has-blockers Only docs with blockers
505
+ --checklist-open Only docs with open checklist items
506
+ --sort <field> Sort by: updated (default), title, status
507
+ --stage <word> Filter ships stage; @unset selects missing values
508
+ --group-by <field> Group by: stage, module, surface, owner (plans view)
509
+ --group <field> Alias for --group-by
510
+ --limit <n> Max results (default: 20)
511
+ --all Show all results (no limit)
512
+ --git Use git dates instead of frontmatter
513
+ --json Output as JSON
514
+ --summarize Add AI summaries to results
515
+ --summarize-limit <n> Max docs to summarize (default: 5)
516
+ --model <name> Model for AI summaries`,
517
+
518
+ grep: `runlist grep <term> — keyword search across frontmatter AND document bodies
519
+
520
+ Alias for \`runlist query --keyword <term> --body --all\`. Answers "which doc
521
+ discussed X?" with full doc cards (type, status, updated, path) plus 1-2
522
+ matching-line excerpts per body hit — instead of raw-grep's bare paths.
523
+
524
+ Bodies are read lazily: frontmatter filters run first, only surviving
525
+ candidates are opened. Archived docs are included but clearly labeled.
526
+
527
+ Composes with the usual query flags:
528
+ runlist grep skipStale everything mentioning skipStale
529
+ runlist grep retries --type plan only plans
530
+ runlist grep retries --status active only active docs
531
+ runlist grep retries --limit 5 cap results (default: unlimited)
532
+ runlist grep retries --json machine-readable (bodyMatches per doc)`,
533
+
534
+ ship: `runlist ship [patch|minor|major] — commit + bump in one step
535
+
536
+ Bundles the release steps into a single command:
537
+ 1. Auto-stage every dirty file matching the release allowlist
538
+ (src/, test/, bin/, docs/, plugins/, .claude-plugin/,
539
+ .claude/commands/, package*.json, runlist.config*.mjs, README.md,
540
+ CLAUDE.md, .gitignore). A real ship refuses while anything outside
541
+ the allowlist is dirty; dry-run reports those files without changing them.
542
+ 2. Commit with an auto-generated \`chore: release <version>\` message.
543
+ 3. Run \`npm version <bump>\` to bump package.json, tag, push, run
544
+ the publish workflow, and reinstall locally.
545
+
546
+ (Per-repo \`.claude/commands\` scaffolding is retired — the runlist plugin's
547
+ SKILL.md is canonical now — so ship no longer regenerates anything.)
548
+
549
+ Options:
550
+ --dry-run, -n Show what would happen without staging or bumping.
551
+
552
+ Defaults to patch. Pass \`minor\` or \`major\` to bump those instead.
553
+
554
+ Network failures after the version tag exists are resumed with
555
+ \`npm run release:resume\`. Never push tags or publish manually.`,
556
+
557
+ set: `runlist set <status> [<file-or-slug>] — change a document's status
558
+
559
+ Writes the new status into the file's frontmatter. In-session plans carry a
560
+ local, gitignored ownership record under .runlist/ so one session cannot release
561
+ another session's work.
562
+ - target is an archive status → archive the file (move + ref update)
563
+ - everything else → plain frontmatter status bump
564
+
565
+ <file-or-slug> resolves like \`runlist use\`/\`archive\`: exact path first, then
566
+ a unique bare slug / basename across the doc roots (\`set paused auth-revamp\`).
567
+ Ambiguous slugs error with the candidate list instead of guessing.
568
+ When the path is omitted, exactly one plan must be owned by this session.
569
+ Claude Code and Codex session IDs are recognized automatically (Codex exports
570
+ CODEX_THREAD_ID to every tool shell), as is OpenCode (via OPENCODE_PID — per
571
+ OpenCode process, not per session). Other hosts must set RUNLIST_SESSION_ID;
572
+ anonymous ownership mutations fail closed.
573
+ Pickup hooks use at-least-once delivery with a stable operationId; hook side
574
+ effects must deduplicate that ID.
575
+
576
+ Options:
577
+ --note "<text>" Append the reason to \`## Version History\` in the
578
+ same call (creates the section if missing). Saves
579
+ the status-change + worklog-edit round-trip.
580
+ --no-index Skip index regen (see \`runlist archive --help\`).
581
+ --show-files Append \`files: …\` footer.
582
+ --force Recover another session's plan (explicit path required).
583
+ --dry-run, -n Preview without writing.
584
+
585
+ Examples:
586
+ runlist set in-session docs/plans/x # mark a plan in-session
587
+ runlist set partial docs/plans/x --note "tail tracked in y.md"
588
+ runlist set archived docs/plans/x # archive a specific plan
589
+ runlist set active # release this session's sole owned plan
590
+
591
+ To open a plan (mark in-session AND print its body), use \`runlist use <file>\`.`,
592
+
593
+ status: `runlist status <file> <new-status> — transition document status
594
+
595
+ Moves the document to the new status. If transitioning to an archive
596
+ status, automatically moves the file to the archive directory and
597
+ regenerates the index (if configured).
598
+
599
+ Options:
600
+ --no-index Skip index regen (useful in concurrent-session repos
601
+ doing path-limited commits — see \`runlist archive --help\`).
602
+ --show-files Append \`files: …\` line to stderr (see \`runlist archive --help\`).
603
+
604
+ Default plan statuses (each maps to a distinct unstuck-action):
605
+ in-session A Claude session is working on it now
606
+ active Ready to be picked up
607
+ planned Queued for future work
608
+ blocked External arrival wait — monitor (hardware, vendor, rollout)
609
+ partial Shipped + deferred tail — spawn successor plans
610
+ paused Intentionally set aside — re-evaluate to resume
611
+ awaiting Needs human input/decision — chase the answer
612
+ queued-after Sequenced behind another plan — check predecessor
613
+ archived No longer relevant; auto-moved to archive directory
614
+
615
+ Run \`runlist help statuses\` for the full vocabulary across all doc types
616
+ (plan, doc, prompt) plus canonical transitions and related commands.
617
+
618
+ Use --dry-run (-n) to preview changes without writing anything.`,
619
+
620
+ check: `runlist check — validate frontmatter and references
621
+
622
+ By default the warning list is suppressed: you see counts plus a one-line
623
+ pointer to \`runlist doctor\` (auto-fix) or \`runlist check --verbose\`
624
+ (per-doc detail). Errors are always shown in full.
625
+ With \`filing\` enabled in config, reports missing/multiple/uncategorized plan
626
+ homes from status-bearing subject rows. These are warnings, never auto-filed.
627
+
628
+ Options:
629
+ --verbose Show every warning per-doc (with category collapse
630
+ applied — high-frequency auto-fixable categories
631
+ summarize to a one-line bulk-fix hint).
632
+ --no-collapse Like --verbose but disables category collapse too —
633
+ every warning prints raw.
634
+ --errors-only Show only errors, suppress warnings entirely
635
+ --fix Auto-fix broken refs, lint issues, and regenerate index
636
+ --json Output errors and warnings as JSON (always full detail)
637
+ --min-docs <n> Fail if fewer than <n> docs were scanned — a floor that
638
+ tells "nothing is wrong" apart from "nothing was looked
639
+ at". Overrides \`minDocs\` in config for this run;
640
+ skipped when checking specific paths.
641
+ --dry-run, -n Preview fixes without writing (with --fix)`,
642
+
643
+ archive: `runlist archive <file-or-slug> — archive a document
644
+
645
+ Sets status to 'archived', moves to the archive directory, auto-updates
646
+ references in other docs, and regenerates the index.
647
+
648
+ <file-or-slug> resolves like \`runlist use\`: an exact path wins, but a bare
649
+ slug / basename (e.g. \`archive resume-foo\`) falls back to a recursive
650
+ basename match under the doc roots. An ambiguous basename (the same name in
651
+ two places) errors with the candidate list instead of guessing.
652
+
653
+ Options:
654
+ --note "<text>" Append \`Archived — <text>\` to \`## Version History\`
655
+ in the same call (creates the section if missing).
656
+ --no-index Skip index regen. Use when multiple sessions are
657
+ working concurrently and you want a path-limited
658
+ commit that doesn't pull other agents' uncommitted
659
+ index changes into your staging area. Run \`runlist index\`
660
+ later (or wire it into a commit hook) to refresh.
661
+ --show-files Append a final \`files: a b c …\` line to stderr
662
+ listing every doc/index path the command touched
663
+ (deduped, sorted, repo-relative). Lets agents do
664
+ \`git add\` with the exact set instead of guessing.
665
+ --force Recover another session's plan (explicit path required).
666
+ --closeout-template Inject a \`## Closeout\` skeleton into the plan body
667
+ before archiving — bullets for outcomes, key
668
+ commits, deferrals. No-op if a \`## Closeout\`
669
+ section already exists. Placed just before
670
+ \`## Version History\` if present, else at end
671
+ of body. Fill it in after archive (the archived
672
+ file is still editable).
673
+ --dry-run, -n Preview changes without writing anything.`,
674
+
675
+ yardstick: `runlist yardstick — compare a plan with the owner's product goal
676
+
677
+ Configure one repository-relative Markdown source:
678
+ export const yardstick = 'docs/product-yardstick.md';
679
+
680
+ runlist yardstick show [plan] [--json]
681
+ runlist yardstick mark <plan> serves [--reason "why"]
682
+ runlist yardstick close <plan> --reason "why"
683
+ runlist yardstick fold <plan> --into <replacement-plan> --reason "why"
684
+ runlist yardstick clear <plan>
685
+
686
+ Show is read-only. Pickup cards display the same goal and literal delivers field.
687
+ Close/fold actually archive the plan through the existing lifecycle workflow.
688
+ Clear removes an assessment; it does not reopen an archived plan. Assessment
689
+ commands record explicit owner choices and never infer fit. Changed goal bodies
690
+ label older assessments for review. Writes support --dry-run, --json and
691
+ --no-index; they respect ownership and preserve archive/reference repair.
692
+ The feature is optional. No model, provider or server is required.`,
693
+
694
+ coverage: `runlist coverage — metadata and configured plan-filing coverage
695
+
696
+ Shows which docs are missing surface, module, or audit metadata.
697
+ With \`filing\` enabled in config, also lists direct/inherited plan homes,
698
+ their hub row locations and categories, and the three filing defect counts.
699
+
700
+ Options:
701
+ --json Machine-readable JSON output`,
702
+
703
+ focus: `runlist focus [status] — detailed view for one status group
704
+
705
+ Shows detailed info for all docs matching the given status (default: active).
706
+
707
+ Options:
708
+ --json Output as JSON`,
709
+
710
+ hud: `runlist hud — actionable triage for session start
711
+
712
+ Prints the runlist command primer (the verb cheat-sheet) plus, in --json mode,
713
+ pending prompts and the check-error count for programmatic callers.
714
+
715
+ Silent when there's nothing actionable — designed for SessionStart hooks where
716
+ zero noise is the right default. Distinct from \`runlist briefing\`, which
717
+ dumps the full plan-status pipeline and per-plan next_step bodies (kilobytes
718
+ on large repos). Use hud for ergonomic session boot; use briefing for
719
+ explicit "give me the full picture."
720
+
721
+ The pending-prompts line tells Claude to consume them via
722
+ \`runlist prompts use <file>\` rather than reading/cat'ing — that atomically
723
+ prints the body and archives the prompt so it cannot be double-consumed.
724
+
725
+ Recommended SessionStart hook (in ~/.claude/settings.json):
726
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "runlist hud", "timeout": 5 }] }]
727
+
728
+ Options:
729
+ --json Output as JSON ({ owned, prompts, errors, previousSelf,
730
+ fleet, recentRejections, misuseRecap, drift })`,
731
+
732
+ briefing: `runlist briefing — comprehensive live-work summary
733
+
734
+ Shows every live plan with its next step, plus doc/research counts and health.
735
+ Output grows with the corpus; use \`runlist plans\` for compact orientation or
736
+ \`runlist agent-context\` for structured agent context.
737
+
738
+ Options:
739
+ --json Output as JSON`,
740
+
741
+ context: `runlist context — full briefing (LLM-oriented)
742
+
743
+ Generates a status briefing designed for AI/LLM consumption. The default
744
+ JSON form is the full index grouped by type/status; use --compact for bounded
745
+ agent-safe JSON.
746
+
747
+ Options:
748
+ --json Output as JSON
749
+ --compact With --json, return counts + bounded next-action lists
750
+ --sections <names> With --json --compact, select agent-context sections
751
+ --max-bytes <N> With --json --compact, cap UTF-8 JSON bytes (default 16384)
752
+ --summarize Add AI summaries for expanded docs
753
+ --model <name> Model for AI summaries`,
754
+
755
+ 'agent-context': `runlist agent-context — compact bounded JSON for agents
756
+
757
+ Equivalent to \`runlist context --json --compact\`. Returns counts,
758
+ validation totals, pending prompt next item, and bounded plan action lists.
759
+
760
+ Options:
761
+ --sections <names> Comma-separated statusVocabulary,counts,prompts,plans,issues
762
+ --max-bytes <N> Maximum UTF-8 JSON bytes, including formatting (default 16384;
763
+ minimum 2048). Keeps claims, next prompt, and first action;
764
+ fails explicitly if protected information cannot fit.
765
+
766
+ Collections retain total, shown, truncated, and items. Budget metadata reports
767
+ the actual emitted bytes and whether the byte budget reduced the item lists.
768
+ Ownership claims remain local state, distinct from a plan's accountable owner.`,
769
+
770
+ stats: `runlist stats — doc health dashboard
771
+
772
+ Shows aggregated metrics: status counts, staleness, errors/warnings,
773
+ freshness, completeness, checklist progress, and audit coverage.
774
+
775
+ Options:
776
+ --json Machine-readable JSON output`,
777
+
778
+ graph: `runlist graph — visualize document relationships
779
+
780
+ Output formats:
781
+ (default) Text adjacency list
782
+ --dot Graphviz DOT format (pipe to dot -Tpng)
783
+ --json Machine-readable JSON
784
+
785
+ Filters:
786
+ --status <s1,s2> Show only docs with these statuses
787
+ --module <name> Show only docs with this module
788
+ --surface <name> Show only docs with this surface`,
789
+
790
+ deps: `runlist deps [file] — dependency tree or overview
791
+
792
+ Without a file, shows a flat overview: most blocking docs, most blocked
793
+ docs, docs with blockers, and orphans.
794
+
795
+ With a file, shows a tree: what the doc depends on (recursive) and what
796
+ depends on it.
797
+
798
+ Options:
799
+ --depth <n> Max tree depth (default: 5)
800
+ --json Machine-readable JSON output`,
801
+
802
+ modules: `runlist modules — module dashboard (plans grouped by module)
803
+
804
+ One row per module discovered in plan frontmatter. Dynamic status columns
805
+ (only statuses with ≥1 plan render). Defaults to --type plan; pass --type
806
+ to scope to docs/prompts.
807
+
808
+ Sort modes:
809
+ --sort total Plan count, desc (default)
810
+ --sort stale Stale-plan count, desc
811
+ --sort age Average age in days, desc
812
+ --sort nextstep % of plans with a next_step set, desc
813
+ --sort cleanup Triage score: (stale × avgAge) / max(total, 1)
814
+
815
+ Options:
816
+ --limit <n> Cap rows (default: 20)
817
+ --all Show every module
818
+ --json Machine-readable shape (includes _totalUnique to
819
+ detect modules: [a, b] double-counting)
820
+
821
+ A plan with \`modules: [a, b]\` counts in both rows — intentional, so
822
+ multi-module plans surface in every relevant triage view. \`(none)\` is a
823
+ literal row for plans with no module tag.`,
824
+
825
+ module: `runlist module <name> — plans for one module, grouped by status
826
+
827
+ Status groups follow config.statusOrder. Stale plans are flagged inline.
828
+
829
+ Sort modes (within each status group):
830
+ --sort status By config.statusOrder, then age (default)
831
+ --sort updated Most-recently-updated first
832
+ --sort age Oldest first
833
+
834
+ Options:
835
+ --json Machine-readable shape
836
+
837
+ Unknown module name suggests close matches (or lists what's available).`,
838
+
839
+ surfaces: `runlist surfaces — list configured surface taxonomy
840
+
841
+ Prints the values accepted in \`surfaces:\` frontmatter, one per line.
842
+ Source: \`config.taxonomy.surfaces\` in runlist.config.mjs.
843
+
844
+ Options:
845
+ --json Machine-readable shape: { surfaces: [...] }
846
+
847
+ When the project has no taxonomy configured, any surface value is accepted —
848
+ the command says so instead of printing an empty list.`,
849
+
850
+ doctor: `runlist doctor — auto-fix everything in one pass
851
+
852
+ Runs in sequence: fix broken references, repair unambiguous membership
853
+ back-references, lint --fix, move over-cap frontmatter prose into body sections,
854
+ sync hub status rows, sync dates from git, regenerate the index, then show
855
+ remaining issues.
856
+
857
+ Modes:
858
+ (default) Auto-fix pass — previews by default since 0.37.0
859
+ (F4). Use --apply (alias --yes) to actually write;
860
+ explicit --dry-run still wins over --apply if both
861
+ are passed (safety prevails).
862
+ --transactions Report pending mutation transactions. A transaction
863
+ abandoned mid-flight can reach \`failed-manual\`, and
864
+ recovery sweeps the whole repo on every mutation —
865
+ so ONE wedged transaction makes \`set\`, \`archive\`,
866
+ \`use\`, \`baton\`, and \`rename\` refuse repo-wide,
867
+ naming a file the failing command never touched.
868
+ Run this first when that happens. Add --apply to
869
+ clear the transactions whose files already agree on
870
+ one generation (no document content is touched);
871
+ the rest are reported for manual review.
872
+ --claims Report which plans are claimed by which session, how
873
+ old each claim is, and whether the owning session's
874
+ process is still alive. A claim is what makes \`set\`,
875
+ \`archive\`, \`baton\`, and \`rename\` refuse a single
876
+ plan ("Plan is busy in another session"); nothing
877
+ expires one, so a session that died holds its plan
878
+ until someone takes it back. Add --apply to release
879
+ the claims whose owning process is provably gone
880
+ (their plans return to \`active\`).
881
+ --claims --apply --older-than <24h|3d>
882
+ Also release claims runlist cannot judge — ones written
883
+ before it recorded the owning process, or held on
884
+ another machine — that are older than the duration.
885
+ That threshold is your judgement, not runlist's: it
886
+ cannot tell a dead session from a slow one.
887
+ --session Read-only: what session identity runlist resolved, from
888
+ which environment variable, and whether it names THIS
889
+ session or something coarser that its siblings share
890
+ (a host process, a terminal) — sessions sharing an id
891
+ can release each other's plans. Also reports whether
892
+ the current host's integration is installed. Run this
893
+ when a verb says "No authoritative session identity",
894
+ or on any host runlist has never been tried on.
895
+ --session --json Machine-readable identity + host-integration state.
896
+ --statuses Read-only diagnostic: detect overloaded status
897
+ buckets where one status holds plans pursuing
898
+ multiple distinct unstuck-actions. Suggests how
899
+ a bucket might split (e.g. backlog → partial /
900
+ paused / queued-after). Heuristic only — verify
901
+ before migrating.
902
+ --statuses --json Machine-readable suggestion shape for tooling.
903
+ --migrate-template Plan-template migrator for plans created before
904
+ v0.21. Auto-fixes:
905
+ - drops singular \`surface:\` when \`surfaces:\`
906
+ array is populated (same for \`module:\`/\`modules:\`)
907
+ - renames \`## Open questions\` → \`## Open Questions\`,
908
+ \`## Out of scope\` / \`## Non-goals\` → \`## Non-Goals\`
909
+ - adds \`## Version History\` section if missing,
910
+ seeded with the file's \`updated\` timestamp
911
+ Skips non-plans. Per-file diff. Doesn't touch
912
+ long next_step/current_state or unmarked phase
913
+ headings (those need human input).
914
+ --migrate-prompts Retrofit pre-existing markdown files under any docs
915
+ root's prompts/ subdirectory with proper prompt
916
+ frontmatter (type, status, created from git
917
+ history, runlist_version, context, related_plans).
918
+ Skips files that already have frontmatter.
919
+ --migrate-template <file> Migrate just one plan.
920
+ --migrate-template --include-archived
921
+ Also touch plans in the archive directory.
922
+ Default skips archived plans (they're closed
923
+ history; a "Migrated to v0.21 template" entry
924
+ in their Version History would be misleading).
925
+ --migrate-template --json Machine-readable result.
926
+ --frontmatter-fix Auto-fix the long-frontmatter warnings that
927
+ \`runlist check\` flags: \`current_state\` >1500 chars
928
+ or \`next_step\` >800 chars. Truncates the
929
+ frontmatter field at the nearest sentence
930
+ boundary under the target (1200 / 600) and
931
+ appends the remainder to a \`## Current State\`
932
+ / \`## Next Step\` body section (created above
933
+ the first H2 if absent, appended otherwise).
934
+ Plans only; honors --dry-run.
935
+ --project Report CLI/project version skew, generated command
936
+ drift, and detectable deprecated command mentions.
937
+
938
+ --apply (or --yes) opts into writes for the default auto-fix pass.
939
+ Sub-modes (--statuses, --migrate-*, --frontmatter-fix, --project) keep their
940
+ existing contracts: they write by default and honor --dry-run.`,
941
+
942
+ 'sync-status': `runlist sync-status — rewrite hub rows whose printed status drifted
943
+
944
+ A runlist / coordination / roadmap hub rows its children in a table and prints
945
+ each child's status by hand. This sweeps every hub, compares each row's status
946
+ word against the plan it links to, and rewrites the ones that drifted. Case is
947
+ preserved (\`Active\` stays capitalized), and nothing else in the cell is touched.
948
+
949
+ runlist sync-status every hub in the repo (the normal case)
950
+ runlist sync-status <hub>... narrow to named hubs
951
+ runlist sync-status --adopt also wrap managed status words in <!--s-->…<!--/s-->
952
+ runlist sync-status --dry-run --json
953
+
954
+ The status word is found positionally — comments stripped, cell's leading token,
955
+ matched against the vocabulary the CHILD's type declares — so no marker is
956
+ needed. A marker pins the span for the rows position can't read (a status sitting
957
+ behind a bolded headline). \`runlist check\` warns on positional drift and ERRORS on
958
+ marked drift: the marker is the author saying runlist owns that word.
959
+
960
+ Rows under a status column with no readable status word are reported by
961
+ \`runlist check\` and left alone here; rows in a table with no status column at all
962
+ are not findings. Not to be confused with \`runlist set <status>\`, which changes a
963
+ document's OWN status — this only rewrites what a hub prints about others.`,
964
+
965
+ 'fix-membership': `runlist fix-membership — repair unambiguous missing parent_plan back-references
966
+
967
+ A repair is safe only when one live hub has already declared the relationship
968
+ in its frontmatter runlist or body execution order and the live child plan has
969
+ no parent_plan at all. The command writes the child-relative back-reference and
970
+ updated date atomically.
971
+
972
+ It never creates or edits hub prose, never overwrites another parent, and skips
973
+ a parentless child ranked by multiple hubs as ambiguous.
974
+
975
+ runlist fix-membership every hub in the repo
976
+ runlist fix-membership <hub>... narrow to named hubs
977
+ runlist fix-membership --dry-run preview without writing
978
+ runlist fix-membership --dry-run --json`,
979
+
980
+ refs: `runlist refs <old> <new> — code-root citations of a moved document
981
+ runlist refs repair — every archived document still cited at its old path
982
+
983
+ Reference repair inside the doc roots is what archive and rename already do.
984
+ This is the other half: a source comment, docstring or string literal that
985
+ cites a document by its repo-relative path, in the roots \`codeRoots\` names.
986
+ Absent that key, nothing is scanned and nothing changes.
987
+
988
+ What it matches:
989
+ - The full repo-relative path only (\`docs/plans/<slug>.md\`). Never a bare
990
+ basename — a corpus cites slugs as words, and matching one is how a rename
991
+ corrupts an unrelated line.
992
+ - A trailing \`§\` or \`#\` anchor is kept as written: only the path is
993
+ replaced, so quotes, backticks and punctuation around it survive.
994
+
995
+ What it writes:
996
+ - Report is the default. \`--fix\` rewrites citations in comments.
997
+ - A citation inside a string literal (or bare in code) is reported in its own
998
+ group and rewritten only with \`--strings\` — a test or a guard may assert
999
+ on it.
1000
+ - A file listed in \`codeRefsUntouched\` is reported and never written.
1001
+ - A file that is not writable, or that resolves outside the repository, is
1002
+ refused by name.
1003
+
1004
+ runlist refs docs/plans/a.md docs/plans/archived/a.md
1005
+ runlist refs docs/plans/a.md docs/plans/archived/a.md --fix
1006
+ runlist refs repair report every stale archived citation
1007
+ runlist refs repair --fix --strings rewrite them, string literals included
1008
+
1009
+ \`repair\` reads each previous path from the archive directory mapping, which is
1010
+ the one move the tool leaves a readable trace of. A rename records none, so a
1011
+ renamed document is covered by the two-argument form.`,
1012
+
1013
+ 'fix-refs': `runlist fix-refs — auto-fix broken reference paths
1014
+
1015
+ Scans all docs for reference fields that point to non-existent files,
1016
+ then attempts to resolve them by matching the basename against all known
1017
+ docs. Fixes are applied by rewriting the frontmatter path.
1018
+
1019
+ Use --dry-run (-n) to preview changes without writing anything.`,
1020
+
1021
+ touch: `runlist touch <file> — bump updated date
1022
+ runlist touch --git [<file>...] — sync dates from git history
1023
+
1024
+ Without --git, updates a single file's frontmatter updated date to today.
1025
+ With --git, scans all docs (or the specified files) and syncs their updated
1026
+ date to match the last substantive git commit date, fixing date drift warnings.
1027
+ Commits that only changed the updated line are ignored so the fix converges.
1028
+
1029
+ Use --dry-run (-n) to preview changes without writing anything.`,
1030
+
1031
+ index: `runlist index [--print] — generate/update docs.md index
1032
+
1033
+ Updates the configured index file in place (writes by default as of 0.34.0).
1034
+ Use --print to dump the regenerated content to stdout without writing.
1035
+
1036
+ Use --dry-run (-n) to preview without writing.`,
1037
+
1038
+ new: `runlist new <type> <name> [body] — create a new document
1039
+
1040
+ Usage (write the draft to a file first):
1041
+ runlist new plan <slug> @/tmp/draft.md # plan from a draft
1042
+ runlist new doc <slug> @/tmp/draft.md # reference doc from a draft
1043
+ runlist new prompt <slug> @/tmp/draft.md # saved prompt (body required)
1044
+ runlist new plan <slug> # empty scaffold to fill in
1045
+
1046
+ What the draft does:
1047
+ - Draft with its own \`## \` headings → it IS the body. Only a \`# Title\` is
1048
+ added if missing; the template's outline is not appended. This holds for a
1049
+ repo's own plan/doc template too.
1050
+ - Draft with no \`## \` headings → lands in the template's first section
1051
+ (\`## Problem\` for plans).
1052
+ - Draft opening with a \`---\` frontmatter block → those keys replace the
1053
+ scaffold's (status, surfaces, modules, current_state, next_step, …), so no
1054
+ frontmatter edit is needed afterwards. \`type:\` is fixed by <type>.
1055
+ To change status later, use \`runlist set <status> <file>\`.
1056
+
1057
+ The repo's own types, starting statuses and folders are listed at the end of
1058
+ this help when it runs inside a runlist repo.
1059
+
1060
+ Body input (required for prompt, optional for plan/doc):
1061
+ @path Read body from a file (preferred)
1062
+ - Read stdin explicitly (heredoc: \`runlist new … - <<'EOF'\`)
1063
+ piped stdin Read when something is piped or redirected in
1064
+ --render-preview With --dry-run, explicitly execute template JavaScript and print its rendered document
1065
+ --body "<text>" Inline body (alias: --message), one-liners only
1066
+ <text> Inline body as 3rd positional, one-liners only
1067
+
1068
+ Inline bodies put the whole content on the command line, which breaks on
1069
+ backticks and dollar signs and trips hooks that scan commands; use @path.
1070
+
1071
+ \`<type>\` can be omitted; defaults to \`doc\`. \`<name>\` is slugified for the
1072
+ filename; a name with a \`/\` is read relative to the repo (or to --root).
1073
+
1074
+ Examples:
1075
+ runlist new plan auth-revamp @/tmp/auth-revamp.md
1076
+ runlist new prompt resume-foo @/tmp/draft.md
1077
+ runlist new plan full-spec - <<'EOF'
1078
+ ---
1079
+ status: planned
1080
+ next_step: Phase 1, extract the token store.
1081
+ ---
1082
+ ## Problem
1083
+ …
1084
+ ## Phases
1085
+ …
1086
+ EOF
1087
+ runlist new prompt cleanup-tomorrow "look at remaining lint warnings"
1088
+
1089
+ Hubs and decisions:
1090
+ runlist new hub <slug> # coordination hub (a plan)
1091
+ runlist new hub <slug> --runlist a,b,c # sprint hub plus child plans
1092
+ runlist new hub <slug> --roadmap # roadmap hub
1093
+ runlist new decision <plan> --question "<question>" [--answers "<…>"] @record.md
1094
+ Adds the next numbered entry to the plan's top-level
1095
+ decisions section with a \`Disposition: OPEN.\` line (or
1096
+ \`--disposition held\`) and the record, which is required;
1097
+ a plan with no such section gets \`## Decisions\`. With a
1098
+ register in config, the id is numbered across the corpus
1099
+ and the register gets its row, question plus --answers
1100
+ (required then), in the same locked write. Config:
1101
+ \`export const decisions = { section, prefix,
1102
+ register: { file, statusLine } }\`.
1103
+
1104
+ Scaffolding runlists (plans only):
1105
+ --runlist <a,b,c> Create a sprint runlist hub plus one child plan per slug.
1106
+ The hub carries \`runlist: [<hub>-01-a.md, <hub>-02-b.md, …]\`
1107
+ and an \`## Order of operations\` list; each child is a
1108
+ \`planned\` stub with a \`parent_plan:\` back-ref. Children
1109
+ are named by the documented \`<hub>-NN-<slug>\` convention.
1110
+ --coordination Create a prose-first coordination hub: \`execution_mode:
1111
+ coordination\` + a \`## Ranked queue\` skeleton (no children).
1112
+ Surfaces in \`runlist runlists\`, held out of the active count.
1113
+ --roadmap Create a tier-3 roadmap hub: \`execution_mode: roadmap\` + a
1114
+ \`## Runlists\` skeleton. A roadmap composes *runlists* (not
1115
+ leaf plans) and rolls their done/total up — see
1116
+ \`runlist roadmap\`. Wire child runlists via \`related_plans:\`.
1117
+ (\`--runlist\`, \`--coordination\`, \`--roadmap\` are mutually exclusive.)
1118
+
1119
+ runlist new plan auth-revamp --runlist extract,rewrite,cleanup
1120
+ runlist new plan platform --coordination
1121
+ runlist new plan q3 --roadmap
1122
+
1123
+ Plan body variants (plans only — pick one body shape):
1124
+ --lite / --minimal Trimmed plan: Problem → Phases → Version History. Drops
1125
+ the full build-up scaffold (Goals / Non-Goals / What
1126
+ Exists Today / Constraints / Decisions / Deferred /
1127
+ Closeout) for a quick plan that doesn't need it.
1128
+ --audit / --findings Audit plan: Problem → Findings (ranked) → Suggested order
1129
+ → Open Questions. The "investigated X, here's what I
1130
+ found" shape, instead of build-up phases.
1131
+ (The body variants and \`--runlist\`/\`--coordination\`/\`--roadmap\` are all
1132
+ mutually exclusive — a plan has exactly one body shape.)
1133
+
1134
+ runlist new plan quick-fix --lite
1135
+ runlist new plan perf-audit --audit
1136
+
1137
+ Other options:
1138
+ --status <s> Set initial status (defaults to first valid status for the type)
1139
+ --title <t> Override the auto-derived title
1140
+ --root <name> Create in a specific docs root. Applies to a nested name
1141
+ too: \`new doc prospects/kim --root docs\` writes
1142
+ docs/prospects/kim.md. Without it, a name containing a
1143
+ \`/\` is read relative to the repo.
1144
+ --show-files Append \`files: …\` line to stderr listing what was touched
1145
+ (the new doc + the index file). See \`runlist archive --help\`.
1146
+ --list-types Show registered types (alias: --list-templates)
1147
+
1148
+ For plans, the default status vocabulary is: in-session, active, planned,
1149
+ blocked, partial, paused, awaiting, queued-after, archived.
1150
+ For prompts: pending (default), archived.
1151
+
1152
+ Use --dry-run (-n) to preview without creating the file.`,
1153
+
1154
+ watch: `runlist watch [command] — re-run a command on file changes
1155
+
1156
+ Watches the docs root for .md file changes and re-runs the specified
1157
+ command. Defaults to 'list' if no command given.
1158
+
1159
+ Examples:
1160
+ runlist watch # re-run list on changes
1161
+ runlist watch check # re-run check on changes
1162
+ runlist watch context # live briefing`,
1163
+
1164
+ export: `runlist export — export docs as markdown, HTML, or JSON
1165
+
1166
+ Without a file, exports all docs (with optional filters).
1167
+ With a file, exports that doc plus all its dependencies.
1168
+
1169
+ Options:
1170
+ --format <md|html|json> Output format (default: md)
1171
+ --output <path> Write to file/directory (default: stdout for md/json)
1172
+ --status <s1,s2> Filter by status
1173
+ --type <t1,t2> Filter by type (plan, doc, prompt)
1174
+ --module <name> Filter by module
1175
+ --root <name> Filter by root
1176
+ --dry-run, -n Preview without writing`,
1177
+
1178
+ summary: `runlist summary <file> — AI summary of a document
1179
+
1180
+ Generates an AI-powered summary using a local model.
1181
+
1182
+ Options:
1183
+ --model <name> Model to use (default: the one \`runlist model\` shows)
1184
+ --max-tokens <n> Max tokens for generation (default: 200)
1185
+ --json Output as JSON`,
1186
+
1187
+ diff: `runlist diff [file] — show changes since last updated date
1188
+
1189
+ Shows git diffs for docs that changed after their frontmatter updated date.
1190
+ Without a file argument, shows all drifted docs.
1191
+
1192
+ Options:
1193
+ --stat Summary only (files changed, insertions/deletions)
1194
+ --since <date> Override: diff since this date instead of frontmatter
1195
+ --summarize Generate AI summary using local model
1196
+ --model <name> Model to use (default: the one \`runlist model\` shows)`,
1197
+
1198
+ lint: `runlist lint [--fix] — check and auto-fix frontmatter issues
1199
+
1200
+ Scans all docs for fixable problems:
1201
+ - Missing status (inferred via local AI model when available)
1202
+ - Missing updated date (set to today)
1203
+ - Status casing (e.g. Active → active)
1204
+ - camelCase key names (e.g. nextStep → next_step)
1205
+ - Comma-separated surface values (converted to surfaces: array)
1206
+ - Trailing whitespace in frontmatter values
1207
+ - Missing newline at end of file
1208
+
1209
+ Without --fix, reports all issues. With --fix, applies fixes in place.
1210
+ Use --dry-run (-n) with --fix to preview without writing anything.`,
1211
+
1212
+ rename: `runlist rename <old> <new> — rename doc and update references
1213
+
1214
+ Renames a document using git mv and updates all frontmatter references
1215
+ in other docs that point to the old filename.
1216
+
1217
+ Body markdown links are warned about but not auto-fixed.
1218
+ Use --dry-run (-n) to preview changes without writing anything.`,
1219
+
1220
+ migrate: `runlist migrate <field> <old-value> <new-value> [files...] — batch update a frontmatter field
1221
+
1222
+ Finds all docs where the given field equals old-value and updates it
1223
+ to new-value. With no file args, every matching doc in the project is
1224
+ rewritten (whole-bucket rename).
1225
+
1226
+ Pass one or more file args to scope the rewrite — only those files
1227
+ are considered. This is how you split one overloaded status into
1228
+ several distinct ones (e.g. moving some \`backlog\` plans to
1229
+ \`paused\` and others to \`partial\`). File args use the same matching
1230
+ as \`bulk archive\`: exact path, then substring fallback.
1231
+
1232
+ Examples:
1233
+ runlist migrate status research scoping
1234
+ runlist migrate module auth identity
1235
+ runlist migrate status backlog paused docs/plans/foo.md docs/plans/bar.md
1236
+
1237
+ Use --dry-run (-n) to preview changes without writing anything.`,
1238
+
1239
+ init: `runlist init — create starter config and docs directory
1240
+
1241
+ Creates runlist.config.mjs, docs/, and docs/docs.md in the current
1242
+ directory. Skips any files that already exist.
1243
+
1244
+ If docs/ already contains .md files, auto-detects statuses, surfaces,
1245
+ modules, and reference fields to pre-populate the config.`,
1246
+
1247
+ plans: `runlist plans — list live plans (excludes archived by default)
1248
+
1249
+ Shows documents with type: plan, excluding terminal/archive statuses,
1250
+ sorted by recent updates. Supports all query flags (--status, --module, --json,
1251
+ --sort, --group-by, etc.).
1252
+
1253
+ Stages use the plan's ships: field, independently of status. Stage groups
1254
+ follow taxonomy.milestones order and show each stage's meaning. Unknown
1255
+ values, Unset, and Invalid remain visible. Stage groups show all plans
1256
+ unless --limit is supplied. --stage @unset selects only missing values;
1257
+ plain unset is a shorthand when the repository has no stage named unset.
1258
+ Use word:<literal> to select an exact word such as word:@unset or
1259
+ word:word:beta when a stage name itself starts with word:.
1260
+
1261
+ Default plan statuses: in-session, active, planned, blocked, partial,
1262
+ paused, awaiting, queued-after, archived. Run \`runlist help statuses\` for
1263
+ the unstuck-action behind each one and canonical transitions.
1264
+
1265
+ Examples:
1266
+ runlist plans # live plans (default)
1267
+ runlist plans --include-archived # all plans including archived
1268
+ runlist plans --status active # active plans only
1269
+ runlist plans --status awaiting # plans waiting on a human decision
1270
+ runlist plans --status partial,paused # shipped-tail and parked plans
1271
+ runlist plans --module auth # plans for the auth module
1272
+ runlist plans --group module # plans grouped by module
1273
+ runlist plans --stage later # plans explicitly assigned to later
1274
+ runlist plans --stage @unset # plans without a stage
1275
+ runlist plans --group-by stage # stage groups in repository order
1276
+ runlist plans --json # JSON output`,
1277
+
1278
+ prompts: `runlist prompts — manage saved prompts (subcommand namespace)
1279
+
1280
+ Prompts are documents with \`type: prompt\`, typically saved under
1281
+ docs/prompts/. They seed future Claude sessions; consuming a prompt
1282
+ prints its body to stdout and atomically archives it (one-shot).
1283
+
1284
+ \`runlist prompt\` (singular) is an alias for \`runlist prompts\` — every
1285
+ subcommand below works under either spelling.
1286
+
1287
+ Subcommands:
1288
+ list List pending prompts (default)
1289
+ next Consume the oldest pending prompt:
1290
+ print body to stdout, flip status to archived
1291
+ use <file-or-slug> Consume a specific prompt (same as next, but
1292
+ targets the named prompt instead of picking oldest)
1293
+ resume <file-or-slug> Alias for \`use\` — same behavior, easier name
1294
+ when continuing a session
1295
+ show <file-or-slug> Read-only peek: print the body WITHOUT consuming
1296
+ (triage). \`peek\` is an alias.
1297
+ archive <file-or-slug> Archive a prompt without printing its body
1298
+ new <slug> [body] Create a new prompt (alias for
1299
+ \`runlist new prompt <slug> [body]\`)
1300
+
1301
+ \`<file-or-slug>\` accepts: an exact path (with or without .md), a bare
1302
+ slug matching a prompt basename, or a unique substring of a prompt
1303
+ path. Ambiguous substrings error with the candidate list.
1304
+
1305
+ A prompt saved by \`runlist baton\` links its plan; consuming it (\`use\`,
1306
+ \`next\`, or top-level \`runlist use\`) also claims that plan for this session.
1307
+ Pass \`--no-claim\` to read and archive the prompt without starting the plan.
1308
+
1309
+ Default prompt statuses: pending, archived.
1310
+
1311
+ Examples:
1312
+ runlist prompts # pending prompts (default)
1313
+ runlist prompts list --verbose # one row per prompt + target plan ref
1314
+ # (from related_plans, parent_plan,
1315
+ # or the first body .md link)
1316
+ runlist prompts list --include-archived # all prompts including archived
1317
+ runlist prompts list --status claimed # already-consumed prompts
1318
+ runlist prompts --json # JSON output
1319
+
1320
+ claude "$(runlist prompts next)" # consume oldest pending + run claude
1321
+ claude "$(runlist prompts use resume-foo)" # by slug
1322
+ claude "$(runlist prompts use docs/prompts/foo.md)" # by path
1323
+ claude "$(runlist prompts resume resume-foo)" # \`resume\` is an alias for \`use\`
1324
+ runlist prompt list # singular alias for \`runlist prompts list\`
1325
+
1326
+ runlist prompts show resume-foo # peek without consuming (triage)
1327
+ runlist prompts show --all # peek the WHOLE pending queue in one call
1328
+ runlist prompts show --all --limit 10 # ...capped
1329
+ runlist prompts show a b c # peek several by name
1330
+ runlist prompts next --dry-run # preview without consuming
1331
+ runlist prompts archive old-thing
1332
+ runlist prompts new my-prompt "Body text here"`,
1333
+
1334
+ baton: `runlist baton — save a resume prompt for whatever you're doing (and release the plan, if there is one)
1335
+
1336
+ Usage (write the resume to a file first, then pick the form that matches):
1337
+ runlist baton @/tmp/draft.md # a plan is in-session: save resume-<plan-slug>, release the plan
1338
+ runlist baton <plan-file> @/tmp/draft.md # hand off a named plan
1339
+ runlist baton <slug> @/tmp/draft.md # no plan: save resume-<slug>, change nothing else
1340
+ The resume can also come from stdin (\`-\`, or a pipe) or --message "..." for one-liners.
1341
+ Baton prints the prompt name it saved and, in plan mode, the git commit to run.
1342
+
1343
+ The resume (10–20 lines): the next concrete decision plus any gotchas, and the
1344
+ paths of the plans/docs it concerns — not a recap of the plan body.
1345
+
1346
+ Options:
1347
+ --status <s> Target status for the plan (default: active; plan mode only)
1348
+ --note "why" Append the reason to ## Version History (plan mode only)
1349
+ --message / --body Inline body (one-liners; prefer @path or stdin)
1350
+ --force Recover another session's plan (explicit path required)
1351
+ --replace Replace exactly one pending handoff; archive its prior text
1352
+ --json Structured repository/session/generated file result
1353
+ --dry-run, -n Preview without writing
1354
+
1355
+ Repo-specific commit hint (runlist.config.mjs):
1356
+ export function batonCommitCommand(message, paths) {
1357
+ return ['just', 'commit', message, ...paths];
1358
+ }
1359
+ The function returns argv; baton shell-quotes each argument before printing.
1360
+
1361
+ Examples:
1362
+ runlist baton @/tmp/draft.md
1363
+ runlist baton checkout-fixes @/tmp/draft.md
1364
+ runlist baton docs/plans/auth.md @/tmp/draft.md
1365
+ runlist baton docs/plans/auth.md @/tmp/new-draft.md --replace
1366
+ runlist baton --status paused --note "blocked on review" @/tmp/d.md
1367
+ cat /tmp/draft.md | runlist baton
1368
+
1369
+ Plan mode (a plan is in-session, or you pass one) publishes in one atomic
1370
+ cooperating transaction:
1371
+ 1. A resume prompt named resume-<plan-slug>, stamped with a plan: link so
1372
+ consuming it re-claims the plan (see \`runlist use\`). The prompt is
1373
+ session-local — the next session's hud surfaces it; never paste resume text
1374
+ into chat.
1375
+ 2. Releases the plan: one status flip, in-session → active by default.
1376
+ 3. Defers the shared generated index and prints exact repository-only commit
1377
+ guidance. Prompt and ownership records stay session-local and OUT of the
1378
+ pathspec.
1379
+ Which plan? Pass it explicitly, or baton resolves exactly one plan owned by
1380
+ this authoritative session. Journal entries and global in-session counts never
1381
+ grant ownership. A live pickup-hook delivery lease blocks release and force
1382
+ takeover; hooks are at-least-once and deduplicate the stable operationId.
1383
+
1384
+ Slug mode (no plan involved) saves resume-<slug> without a status change or
1385
+ commit. A bare word that names a plan is treated as that plan.
1386
+
1387
+ Baton saves nothing while a handoff for the same work is pending (resume-<name>,
1388
+ or a pending prompt linked to the plan): inspect it with \`runlist prompts show\`.
1389
+ Keep it if current; use \`--replace\` with a new draft if stale. Replacement
1390
+ preserves the prior text under archived/ and refuses multiple matches.
1391
+ With no @file, \`-\` or --message, baton reads stdin only when something is piped
1392
+ in; an open pipe that sends nothing is given up on after a moment.`,
1393
+
1394
+ stale: `runlist stale — list stale documents
1395
+
1396
+ Shows docs that haven't been updated within their staleness threshold.
1397
+ Supports all query flags (--status, --json, --sort, etc.)
1398
+
1399
+ Examples:
1400
+ runlist stale --group module Stale plans grouped by module (triage view)`,
1401
+
1402
+ actionable: `runlist actionable — list docs with next steps
1403
+
1404
+ Shows active/ready docs that have a next_step defined.
1405
+ Supports all query flags (--status, --json, --sort, etc.)`,
1406
+
1407
+ unblocks: `runlist unblocks <file> — show what completes when this doc ships
1408
+
1409
+ Shows documents that reference or depend on the given file.
1410
+ Useful for impact analysis before archiving or changing a plan.
1411
+
1412
+ The dependency edge is read from each plan's \`blockers:\` frontmatter
1413
+ (a YAML list of plan slugs or paths). \`blocked_by:\` is accepted as
1414
+ an alias since 0.39.3 — both populate the same index field, so use
1415
+ whichever name reads better.
1416
+
1417
+ Frontmatter shape:
1418
+
1419
+ ---
1420
+ type: plan
1421
+ status: blocked
1422
+ blockers:
1423
+ - foo-plan.md
1424
+ - docs/plans/bar-plan.md
1425
+ ---
1426
+
1427
+ Options:
1428
+ --json Output as JSON`,
1429
+
1430
+ health: `runlist health — plan velocity, aging, and pipeline health
1431
+
1432
+ Shows plan pipeline status, active plan aging, recently archived
1433
+ plans, and checklist progress. Plans-only view.
1434
+
1435
+ Options:
1436
+ --json Output as JSON`,
1437
+
1438
+ record: `runlist record — act on an existing native flag or decision
1439
+
1440
+ runlist record <file> accept|reject|resolve|reopen --note <reason>
1441
+ runlist record <file> held|ruled|closed|open --note <reason> [--option <option-id>]
1442
+
1443
+ Ruled selects an existing option; closed withdraws a ruling. Human authority is
1444
+ required to record or withdraw a ruling. Flag resolution here is manual.
1445
+ --dry-run prints source; --expected-revision checks a prior read; --json returns
1446
+ the durable receipt. Old evidence and history remain in the record.`,
1447
+ decision: `runlist decision — record an outcome in an existing decision
1448
+
1449
+ runlist decision <file> <id> --disposition open|held|ruled|closed --note <reason>
1450
+ Add --choice <answer> for a human ruling. Earlier outcomes remain in the source.
1451
+ --expected-revision <sha256:hash> refuses changes since a previous read.
1452
+ --dry-run prints the prepared source without writing. --json returns the saved receipt.
1453
+
1454
+ The source editor checks ownership and records the operation. Agent sessions may
1455
+ record open, held or closed outcomes; only a human may record a ruling.`,
1456
+ decisions: `runlist decisions — the open and held decisions, read from the corpus
1457
+
1458
+ runlist decisions every open and held decision, one line each
1459
+ runlist decisions <doc> one document, by path, filename or slug
1460
+ runlist decisions <ID> one record, whole, as it is written
1461
+ runlist decisions --all every disposition, not only the pending ones
1462
+ runlist decisions --json the same rows as data, each with its question, its
1463
+ document's title and what it blocks
1464
+ runlist decisions --check every item it could not read or that is incomplete; exit 1 if any
1465
+
1466
+ A decision is an item in a decisions section (a heading naming the section word
1467
+ in its first 4 words): a heading, bold lead, bullet or table row whose first
1468
+ token is an id. A register is a fenced block whose first line carries
1469
+ decisions.register.statusLine; its rows are \`ID text\`. A disposition is read
1470
+ from a \`Disposition:\` line or a row's opening word, then from prose when
1471
+ decisions.prose is on, then from the block or bold lead above it. Configure it
1472
+ with \`export const decisions = { ... }\` (see src/decisions.mjs).
1473
+
1474
+ What a decision blocks, in --json as \`blocks: [{ doc, line, kind, section, text }]\`:
1475
+ what its record says it blocks or gates (kind "stated"), and every unticked
1476
+ checklist item (kind "item") or frontmatter blocker (kind "blocker") that
1477
+ names its id, alone or inside a written range such as \`A2 to A12\`. In
1478
+ another document the id has to sit beside a link to the decision's document.`,
1479
+
1480
+ xref: `runlist xref <file...> — one document's cross-references, read before it is edited
1481
+
1482
+ runlist xref <file> named by: every document pointing at it, and whether it points back
1483
+ names: what it points at, with status, missing and one-way entries
1484
+ decisions: its records with disposition and any linked peer that
1485
+ disagrees, and records elsewhere it cites beside their document
1486
+ code: every cited file live, renamed (to where) or removed, and the
1487
+ commits since its last commit that touched a cited file
1488
+ flags: what is open on it
1489
+ runlist xref <file> --json the same card as data
1490
+ runlist xref --check across the plans: open work (an unticked item, a blocker,
1491
+ next_step) naming a removed or renamed file, and a decision open
1492
+ or held in one document and ruled or closed at a linked peer;
1493
+ exit 1 if any
1494
+ runlist xref --check --flag the same findings synced into \`runlist flags\` as check \`xref\`,
1495
+ resolving what it no longer reports
1496
+
1497
+ Code changing under a live plan is shown, never flagged: it is the normal
1498
+ state of a live plan.`,
1499
+
1500
+ desktop: `runlist desktop [--app /path/to/application] — open the native app
1501
+ runlist desktop install [--from /path/to/installer.dmg] — install the optional GUI
1502
+
1503
+ Install downloads the latest Mac Apple Silicon GUI from Runlist's GitHub
1504
+ releases, or uses a local signed DMG with --from. It verifies the Runlist
1505
+ developer signature and Apple approval before installing. An existing app is
1506
+ kept as-is unless --from is given; replacements retain a rollback copy.
1507
+ Installation does not open the app. Windows/Linux installers are not yet
1508
+ available through this command. --dry-run uses no network and changes no files.
1509
+
1510
+ Opens an installed Runlist application. Choose a trusted
1511
+ checkout in the app. The app bundles its runtime and uses private pipes;
1512
+ it opens no listening server. The CLI does not load checkout configuration
1513
+ for this command. macOS uses LaunchServices; Windows/Linux start the executable
1514
+ directly. Use --app for a custom installation path or Linux AppImage.
1515
+ The qualified release is currently macOS Apple Silicon; ports are in development.
1516
+ --dry-run prints the launch action without opening the app.`,
1517
+
1518
+ app: `runlist app [document] [--port <port>] — open the local document library
1519
+
1520
+ Starts a server on 127.0.0.1 (default port 5173) for this selected checkout.
1521
+ Open the access link it prints in your browser. Browser sessions identify as
1522
+ the local human, independently of the launching agent. Browse hubs, plans and
1523
+ configured documents with search, filters and bounded result pages; read them,
1524
+ edit Markdown, review the source diff, save with an exact revision check,
1525
+ recover private drafts, review conflicts, and undo a save.
1526
+
1527
+ Save writes Markdown; it does not commit or share through Git. Live claims
1528
+ remain owned by their CLI sessions; coordinate/release them before a human save.
1529
+ Metadata and lifecycle changes continue through the CLI. Ctrl+C stops the app.
1530
+ --dry-run previews startup without opening a server or writing editor state.
1531
+ `,
1532
+ show: `runlist show <file...> — one document's card, read-only
1533
+
1534
+ runlist show <file> title, status, next step, blockers, checklist,
1535
+ related plans and docs, documents linked from the body
1536
+ runlist show <a> <b> --json [{ path, type, title, status, summary, currentState,
1537
+ nextStep, blockers, checklist, updated,
1538
+ related: [{ field, ref, path, exists, title, status, type }],
1539
+ links: [{ path, title, status, type }] }]
1540
+
1541
+ A file is a path, filename or slug, as for \`use\`. Nothing is claimed or
1542
+ written. With --json a file that is not found is { path, error } rather than
1543
+ an exit.`,
1544
+
1545
+ model: `runlist model — the local model behind summaries and lint
1546
+
1547
+ One local server (Ollama by default) holds one model, serves one request at a
1548
+ time and unloads it after it sits idle. runlist never starts the server or
1549
+ pulls a model on its own.
1550
+
1551
+ Subcommands:
1552
+ status [--json] Default. Server, model, cap, what is loaded and its memory
1553
+ start [--force] Start \`ollama serve\` (one model, one request at a time);
1554
+ refused where no model fits unless forced
1555
+ stop [--all] Unload runlist's model now; --all unloads every loaded model
1556
+ measure [name...] Load each pulled candidate (or the named ones) in turn,
1557
+ summarise the corpus's largest documents at lower
1558
+ priority, and record peak memory and speed for this
1559
+ machine; a model is picked only once measured here
1560
+ use <name|auto> Name the model; auto takes the first measured candidate
1561
+ under the cap
1562
+ cap <gb|auto> The most memory a model may take; auto (the default) is a
1563
+ quarter of this machine's memory, at most 12 GB
1564
+
1565
+ Before a model loads here, the memory free now must hold it plus 1.5 GB and
1566
+ memory pressure must be normal; otherwise the command goes on without it.
1567
+
1568
+ Settings live in ~/.runlist/model.json. RUNLIST_MODEL, RUNLIST_MODEL_CAP_GB and
1569
+ RUNLIST_MODEL_ENDPOINT override them; \`runtime: "openai"\` there points at any
1570
+ OpenAI-compatible server (mlx_lm.server, llama-server, LM Studio).`,
1571
+
1572
+ glossary: `runlist glossary <term> — look up domain terms and related docs
1573
+
1574
+ Searches the glossary table in your docs for matching terms.
1575
+ Shows definition, related docs, and see-also entries.
1576
+
1577
+ Options:
1578
+ --list List all glossary terms
1579
+ --json Output as JSON`,
1580
+
1581
+ statuses: `runlist statuses — manage per-project status taxonomy
1582
+
1583
+ Subcommands:
1584
+ list [--type <t>] [--json] Default. Table view of every status × type with all flags.
1585
+ --type accepts comma-separated types.
1586
+ add <name> --type <t> [--like <e>] [flags...]
1587
+ Add a new status. --like <existing> clones every flag from
1588
+ another status; user flags override. Inserts before the
1589
+ first terminal/archive status. Refuses if name already
1590
+ exists or is invalid.
1591
+ set <name> --type <t> <flags...> Edit flags on an existing status. Refuses if status doesn't
1592
+ exist. Flags overwrite individually.
1593
+ remove <name> --type <t> Delete a status entry. Refuses if any docs use the status
1594
+ (lists offenders, suggests \`runlist migrate\`). Warns if an
1595
+ explicit lifecycle export references the name.
1596
+ migrate <type> One-shot conversion of array-form types.<t>.statuses to
1597
+ rich form, pulling in peer staleDays/context and per-status
1598
+ requiresModule from taxonomy.moduleRequiredFor.
1599
+
1600
+ Flags accepted by add/set:
1601
+ --context <expanded|listed|counted> Briefing layout bucket
1602
+ --staleDays <n|null> Stale threshold; null = never stale
1603
+ --requiresModule / --no-requiresModule
1604
+ --terminal / --no-terminal Closure state — excluded from active-work scope
1605
+ --archive / --no-archive Auto-move to archive dir on transition
1606
+ --skipStale / --no-skipStale
1607
+ --skipWarnings / --no-skipWarnings
1608
+ --quiet / --no-quiet Sugar for skipStale + skipWarnings (explicit overrides win)
1609
+
1610
+ Workflow flags:
1611
+ --yes Skip the confirmation prompt
1612
+ --dry-run, -n Show the diff without writing
1613
+ --ignore-lifecycle-override Write even when an explicit \`lifecycle\` export
1614
+ would silently mask the per-status flags
1615
+
1616
+ Examples:
1617
+ runlist statuses # list everything
1618
+ runlist statuses add paused --type plan --like blocked --quiet
1619
+ runlist statuses set archived --type plan --no-quiet
1620
+ runlist statuses remove obsolete --type plan
1621
+ runlist statuses migrate plan # array → rich
1622
+
1623
+ Lifecycle-override gotcha: if your config has both rich-form types and an explicit
1624
+ \`export const lifecycle\`, the runtime ignores per-status flags. The CLI refuses
1625
+ to write in that case unless you pass --ignore-lifecycle-override; the recommended
1626
+ fix is to delete the explicit \`lifecycle\` block so flags take effect.`,
1627
+
1628
+ bulk: `runlist bulk archive <f1> <f2> ... — archive multiple files at once
1629
+
1630
+ Archives each file in an independent per-item transaction: sets status to
1631
+ archived, moves to archive directory, and updates references. This is explicitly
1632
+ not all-or-none; --json reports archived/failed for every item. The index is
1633
+ regenerated once after all item attempts.
1634
+
1635
+ Use --dry-run (-n) to preview changes without writing anything.`,
1636
+
1637
+ runlist: `runlist runlist <hub> [next|add|remove|reorder] — work with an ordered group of plans
1638
+
1639
+ A "runlist" is just a plan with a \`runlist:\` array of child plan paths in its
1640
+ frontmatter — there is no separate doc type. The hub plan can have any status;
1641
+ the order of the children comes from the array.
1642
+
1643
+ Usage:
1644
+ runlist runlist <hub> Show children + their statuses, in order. The
1645
+ first pickup-able child (active / planned /
1646
+ in-session) is marked \`→\`. Archived (done) and
1647
+ parked children (blocked / partial / paused /
1648
+ awaiting / queued-after) are skipped — \`→\`
1649
+ advances to the first child you can actually
1650
+ start. Parked ≠ done: they don't count toward
1651
+ done/total.
1652
+ runlist runlist next <hub> Open the first pickup-able child (marks it
1653
+ in-session + prints it), advancing past archived
1654
+ and parked children. If every remaining child is
1655
+ parked, stops and lists them + the unstick verbs
1656
+ so you resolve a blocker first.
1657
+ runlist runlist add <hub> <child...>
1658
+ Append children to the hub's \`runlist:\` array
1659
+ (no more hand-editing the YAML). Each child can be:
1660
+ • a bare slug (\`cleanup\`) → scaffolds a
1661
+ \`planned\` stub \`<hub>-NN-<slug>.md\` next to
1662
+ the hub (mirrors \`new plan --runlist\`), or
1663
+ • a path/slug of an existing plan → wired in by a
1664
+ hub-relative ref, with its \`parent_plan:\` set
1665
+ back at the hub.
1666
+ A plain plan gains a \`runlist:\` (becomes a hub).
1667
+ Coordination hubs (body-order) aren't handled here.
1668
+ runlist runlist remove <hub> <child...>
1669
+ Drop children from the \`runlist:\` array. Children
1670
+ match by full path or short slug (\`cleanup\` finds
1671
+ \`<hub>-03-cleanup.md\`). \`--clear-parent\` also blanks
1672
+ each removed child's \`parent_plan:\` back-ref.
1673
+ runlist runlist reorder <hub> <child> --before|--after <other>
1674
+ runlist runlist reorder <hub> <c1> <c2> <c3...>
1675
+ Move one child relative to another, or pass every
1676
+ child to set a full new order.
1677
+ All three mutators take \`--dry-run\` / \`--json\` and
1678
+ keep any body \`## Order of operations\` link list in
1679
+ sync (preserving per-item ⬜/✅ markers).
1680
+
1681
+ Flags (only meaningful with \`next\`):
1682
+ --full Print full plan body instead of the card.
1683
+ --no-index Skip index regeneration.
1684
+ --show-files Emit \`files: …\` footer.
1685
+
1686
+ Common shape:
1687
+ ---
1688
+ type: plan
1689
+ status: active
1690
+ title: Auth Revamp
1691
+ runlist:
1692
+ - auth-revamp-01-extract.md
1693
+ - auth-revamp-02-rewrite.md
1694
+ - auth-revamp-03-cleanup.md
1695
+ ---
1696
+
1697
+ Child plans should set \`parent_plan:\` back at the hub — \`runlist check\` warns
1698
+ when they don't.
1699
+
1700
+ In \`runlist plans\`, a hub is tagged \`[RUNLIST]\` (not \`[ACTIVE]\`) and its
1701
+ children fold underneath it — progress (\`done/total\`) and the next pickup
1702
+ \`→\` show on the hub row, so a sprint reads as one runlist instead of N loose
1703
+ plans. Children whose hub is filtered out of the view (e.g. \`--status active\`
1704
+ when the hub is \`planned\`) still render on their own.
1705
+
1706
+ Larger, prose-first "coordination" runlists (a domain map pointing at many
1707
+ plans, marked \`execution_mode: coordination\` or named \`*-runlist\`) aren't
1708
+ folded — they're lifted into a separate \`Runlists\` section in \`runlist plans\`
1709
+ and out of the active count. \`runlist runlists\` shows that dashboard on its own.
1710
+ For these, \`runlist\`/\`runlist next\` also read order from the body when there's
1711
+ no \`runlist:\` array — a \`## Ranked queue\` table or \`## Order of operations\`
1712
+ list of markdown links (the first \`.md\` link per row/item, in order).`,
1713
+
1714
+ runlists: `runlist runlists — the coordination-hub dashboard
1715
+
1716
+ Lists every *coordination runlist*: a prose-first plan that sits above a
1717
+ cluster of others (a domain map), detected by \`execution_mode: coordination\`
1718
+ or a \`*-runlist\` / \`runlist\` slug. Each row shows the hub, its age, the rough
1719
+ size of its \`related_plans:\` cluster, a \`next → <child>\` when the hub's body
1720
+ encodes order as markdown links (\`## Ranked queue\` table / \`## Order of
1721
+ operations\` list), and a one-line descriptor.
1722
+
1723
+ This is the standalone form of the \`Runlists\` section that \`runlist plans\`
1724
+ pins beneath the leaf-plan triage list.
1725
+
1726
+ runlist runlists All runlists (a small bounded set), most stale first.
1727
+ runlist runlists --sort recent Order by recency instead (age|recent|related|title|status).
1728
+ runlist runlists --limit N Cap the list at N.
1729
+ runlist runlists --json Structured rows (path, status, childCount, nextPickup, …).`,
1730
+
1731
+ 'bulk-tag': `runlist bulk-tag [files...] — fill in type/status frontmatter on pre-existing markdown
1732
+
1733
+ Scans the docs tree for files that are missing either \`type:\` or \`status:\`
1734
+ (or have no frontmatter block at all) and writes minimal frontmatter so they
1735
+ appear in \`runlist list\`, \`query\`, and \`briefing\`.
1736
+
1737
+ Type is inferred from the file's subdir under docsRoot:
1738
+ docs/plans/foo.md → type: plan, status: planned
1739
+ docs/prompts/bar.md → type: prompt, status: pending
1740
+ docs/baz.md → type: doc, status: draft
1741
+
1742
+ Already-tagged files (both \`type:\` and \`status:\` set) are skipped. Files
1743
+ under the archive directory are excluded.
1744
+
1745
+ Flags:
1746
+ --type <t> Override inferred type for every candidate.
1747
+ --status <s> Override the per-type default status.
1748
+ --json Emit a structured candidate list.
1749
+ --dry-run (-n) Preview without writing.
1750
+
1751
+ Pass file paths as positional args to scope to those files only; otherwise
1752
+ the whole docs tree is scanned.`,
1753
+ };
1754
+
1755
+ const GLOBAL_VALUE_OPTIONS = new Set(['--config', '--root', '--type']);
1756
+ const GLOBAL_BOOLEAN_OPTIONS = new Set(['--dry-run', '-n', '--verbose']);
1757
+
1758
+ function splitGlobalArgs(args) {
1759
+ let commandIndex = -1;
1760
+ for (let i = 0; i < args.length; i += 1) {
1761
+ const arg = args[i];
1762
+ if (GLOBAL_VALUE_OPTIONS.has(arg)) {
1763
+ if (args[i + 1] === undefined || args[i + 1].startsWith('-')) die(`Missing value for global option \`${arg}\`.`);
1764
+ i += 1;
1765
+ continue;
1766
+ }
1767
+ if (GLOBAL_BOOLEAN_OPTIONS.has(arg)) continue;
1768
+ commandIndex = i;
1769
+ break;
1770
+ }
1771
+
1772
+ const command = canonicalCommand(commandIndex === -1 ? 'list' : args[commandIndex]);
1773
+ const rest = [];
1774
+ let explicitConfig = null;
1775
+ let rootArg = null;
1776
+ let typeArg = null;
1777
+ let dryRun = false;
1778
+ let verbose = false;
1779
+
1780
+ for (let i = 0; i < args.length; i += 1) {
1781
+ if (i === commandIndex) continue;
1782
+ const arg = args[i];
1783
+ const beforeCommand = commandIndex === -1 || i < commandIndex;
1784
+ if (GLOBAL_VALUE_OPTIONS.has(arg)) {
1785
+ const local = !beforeCommand && commandOwnsOption(command, arg);
1786
+ const next = args[i + 1];
1787
+ if (next === undefined || next.startsWith('-')) die(`Missing value for \`${arg}\`.`);
1788
+ if (local) rest.push(arg, next);
1789
+ else if (arg === '--config') explicitConfig = next;
1790
+ else if (arg === '--root') rootArg = next;
1791
+ else typeArg = next;
1792
+ i += 1;
1793
+ continue;
1794
+ }
1795
+ if (arg === '--dry-run' || arg === '-n') { dryRun = true; continue; }
1796
+ if (arg === '--verbose') {
1797
+ if (!beforeCommand && commandOwnsOption(command, arg)) rest.push(arg);
1798
+ else verbose = true;
1799
+ continue;
1800
+ }
1801
+ rest.push(arg);
1802
+ }
1803
+
1804
+ return { command, rest, explicitConfig, rootArg, typeArg, dryRun, verbose };
1805
+ }
1806
+
1807
+ async function main() {
1808
+ const args = process.argv.slice(2);
1809
+
1810
+ // Pre-config flags
1811
+ if (args.includes('--version') || args.includes('-v')) {
1812
+ process.stdout.write(`${pkg.version}\n`);
1813
+ return;
1814
+ }
1815
+
1816
+ // Before-command options are global. After the command, a schema-declared
1817
+ // local option wins; otherwise the historical anywhere-global form remains.
1818
+ const parsed = splitGlobalArgs(args);
1819
+ let { command, explicitConfig, rootArg, typeArg, dryRun, verbose } = parsed;
1820
+ let restArgs = parsed.rest;
1821
+ const captureHelpConfig = async topic => {
1822
+ _resolvedCommand = 'help';
1823
+ _helpTopic = topic;
1824
+ _suppressObservability = dryRun || topic === 'desktop';
1825
+ if (topic === 'desktop') return;
1826
+ try { _resolvedConfig = await resolveConfig(process.cwd(), explicitConfig); } catch { /* help works with broken config */ }
1827
+ };
1828
+
1829
+ // Tolerate accidentally pasting the command prefix twice, while leaving all
1830
+ // remaining arguments to the normal `use` grammar and path validation.
1831
+ if (command === 'use') {
1832
+ while ((restArgs[0] === 'runlist' || restArgs[0] === 'dotmd') && restArgs[1] === 'use') restArgs = restArgs.slice(2);
1833
+ }
1834
+
1835
+ // Reconstruct the active global flags for proxy commands (e.g. `watch`) that
1836
+ // re-invoke the CLI in a child process and must propagate them through.
1837
+ const globalFlagArgs = () => {
1838
+ const out = [];
1839
+ if (explicitConfig) out.push('--config', explicitConfig);
1840
+ if (rootArg) out.push('--root', rootArg);
1841
+ if (typeArg) out.push('--type', typeArg);
1842
+ if (dryRun) out.push('--dry-run');
1843
+ if (verbose) out.push('--verbose');
1844
+ return out;
1845
+ };
1846
+
1847
+ // Apply global --root / --type filters to an index in place. Shared by the
1848
+ // common index path below AND the early-dispatched commands (plans, runlists,
1849
+ // presets) that build their own index, so filtering is consistent everywhere.
1850
+ // Recomputes BOTH countsByStatus and countsByType so filtered JSON never
1851
+ // reports corpus-wide tallies.
1852
+ function applyIndexFilters(idx) {
1853
+ if (rootArg) {
1854
+ idx.docs = idx.docs.filter(d => d.root === rootArg || d.root.endsWith('/' + rootArg) || d.root.split('/').pop() === rootArg);
1855
+ }
1856
+ if (typeArg) {
1857
+ const types = typeArg.split(',').map(t => t.trim()).filter(Boolean);
1858
+ idx.docs = idx.docs.filter(d => types.includes(d.type));
1859
+ }
1860
+ if (rootArg || typeArg) {
1861
+ idx.errors = idx.errors.filter(e => idx.docs.some(d => d.path === e.path));
1862
+ idx.warnings = idx.warnings.filter(w => idx.docs.some(d => d.path === w.path));
1863
+ idx.countsByStatus = {};
1864
+ idx.countsByType = {};
1865
+ for (const doc of idx.docs) {
1866
+ const status = doc.status ?? 'unknown';
1867
+ idx.countsByStatus[status] = (idx.countsByStatus[status] ?? 0) + 1;
1868
+ const type = doc.type || 'unknown';
1869
+ if (!idx.countsByType[type]) idx.countsByType[type] = {};
1870
+ idx.countsByType[type][status] = (idx.countsByType[type][status] ?? 0) + 1;
1871
+ }
1872
+ }
1873
+ }
1874
+
1875
+ if (command === 'help' || command === '--help' || command === '-h') {
1876
+ const topic = restArgs[0];
1877
+ await captureHelpConfig(topic ? (HELP[topic] || HELP[`help:${topic}`] ? topic : '(unknown)') : '_main');
1878
+ if (topic) {
1879
+ const key = `help:${topic}`;
1880
+ if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
1881
+ if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
1882
+ process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: runlist <cmd> --help\n`);
1883
+ process.exitCode = 1;
1884
+ _exitFailureMessage = 'Unknown help topic';
1885
+ return;
1886
+ }
1887
+ process.stdout.write(`${HELP._main}\n`);
1888
+ return;
1889
+ }
1890
+
1891
+ const dispatchPolicy = commandPolicy(command);
1892
+ _resolvedCommand = command;
1893
+ const doctorSubMode = command === 'doctor' && (
1894
+ args.includes('--statuses') || args.includes('--migrate-template')
1895
+ || args.includes('--migrate-prompts') || args.includes('--frontmatter-fix')
1896
+ || args.includes('--project')
1897
+ );
1898
+ const doctorExplicitApply = args.includes('--apply') || args.includes('--yes');
1899
+ const effectiveDryRun = dryRun || (command === 'doctor' && !doctorSubMode && !doctorExplicitApply);
1900
+ const passiveMachineContext = command === 'agent-context'
1901
+ || (command === 'context' && args.includes('--json') && args.includes('--compact'));
1902
+ // Git/frontmatter drift is validation work, not index construction. Keep the
1903
+ // bounded history scan on commands that report or repair that drift; ordinary
1904
+ // reads still run schema/reference validation without walking 10k commits.
1905
+ const gitStaleness = command === 'check' || command === 'doctor';
1906
+ _suppressObservability = effectiveDryRun || command === 'hud' || passiveMachineContext;
1907
+
1908
+ // Per-command help
1909
+ if (args.includes('--help') || args.includes('-h')) {
1910
+ requireCommandPolicy(command, dispatchPolicy);
1911
+ await captureHelpConfig(command);
1912
+ process.stdout.write(`${HELP[command] ?? commandUsage(command)}\n`);
1913
+ if (command === 'new') {
1914
+ // Best effort: outside a runlist repo, or with a broken config, the
1915
+ // static help above is the whole answer.
1916
+ try {
1917
+ const repoConfig = _resolvedConfig;
1918
+ if (repoConfig?.configFound !== false) {
1919
+ const { newHelpForRepo } = await import('../src/new.mjs');
1920
+ process.stdout.write(`\n${newHelpForRepo(repoConfig)}\n`);
1921
+ }
1922
+ } catch { /* static help already printed */ }
1923
+ }
1924
+ return;
1925
+ }
1926
+
1927
+ if (command === 'completions') {
1928
+ requireCommandPolicy(command, dispatchPolicy);
1929
+ try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
1930
+ const { runCompletions } = await import('../src/completions.mjs');
1931
+ runCompletions(restArgs);
1932
+ return;
1933
+ }
1934
+
1935
+ if (command === 'desktop') {
1936
+ requireCommandPolicy(command, dispatchPolicy);
1937
+ try { restArgs=validateCommandArgs(command,restArgs); } catch(err) { die(err.message); }
1938
+ if(restArgs[0]==='install') {
1939
+ const {runDesktopInstall}=await import('../src/desktop-install.mjs');
1940
+ await runDesktopInstall(restArgs,{dryRun});
1941
+ } else {
1942
+ const {runDesktop}=await import('../src/desktop.mjs');
1943
+ runDesktop(restArgs,{dryRun});
1944
+ }
1945
+ return;
1946
+ }
1947
+
1948
+ let config;
1949
+ try {
1950
+ config = await resolveConfig(process.cwd(), explicitConfig);
1951
+ } catch (err) {
1952
+ if (command === 'guard') {
1953
+ process.stdout.write('{}\n');
1954
+ return;
1955
+ }
1956
+ // A prompt hook runs on every user message; a broken config must not
1957
+ // turn each one into an error.
1958
+ if (command === 'hud' && restArgs.includes('--prompt-submit')) return;
1959
+ throw err;
1960
+ }
1961
+ _resolvedConfig = config;
1962
+ // The one place that reaches an OpenCode session running without the
1963
+ // integration — whatever verb it reaches for first. stderr, so `--json`
1964
+ // consumers are untouched, and once per session so it informs rather than
1965
+ // nags. `install`/`doctor`/`update` are skipped: they report this themselves,
1966
+ // and telling `runlist install opencode` to run `runlist install opencode` is
1967
+ // noise.
1968
+ if (!['install', 'doctor', 'update', 'hud', 'guard'].includes(command)) {
1969
+ const { degradedIdentityNotice } = await import('../src/host-integration.mjs');
1970
+ const notice = degradedIdentityNotice(config?.repoRoot, { version: pkg.version });
1971
+ if (notice) process.stderr.write(`${notice}\n`);
1972
+ }
1973
+
1974
+ const suppressSideEffects = effectiveDryRun || command === 'hud' || command === 'guard' || passiveMachineContext;
1975
+ Object.defineProperty(config, '_execution', {
1976
+ value: { dryRun, passive: command === 'hud' || passiveMachineContext, suppressSideEffects, gitStaleness },
1977
+ enumerable: false,
1978
+ });
1979
+ if (!suppressSideEffects) {
1980
+ const { migrateStateDirectory } = await import('../src/state-migration.mjs');
1981
+ const { LEGACY_STATE_DIR, STATE_DIR } = await import('../src/naming.mjs');
1982
+ const migration = migrateStateDirectory(config.repoRoot);
1983
+ if (migration.status === 'migrated') {
1984
+ process.stderr.write(`[runlist] migrated ${LEGACY_STATE_DIR}/ → ${STATE_DIR}/ (session state moved; repository files unchanged)\n`);
1985
+ } else if (migration.status === 'refused') {
1986
+ warn(migration.message);
1987
+ }
1988
+ }
1989
+ // Unknown names may still be user-defined query presets. Every built-in
1990
+ // dispatcher branch, including mutators above the shared index path, must be
1991
+ // present in the centralized command policy registry.
1992
+ if (!config.presets[command]) requireCommandPolicy(command, dispatchPolicy);
1993
+
1994
+ try {
1995
+ restArgs = validateCommandArgs(command, restArgs, { preset: Boolean(config.presets[command]) });
1996
+ } catch (err) {
1997
+ die(err.message);
1998
+ }
1999
+
2000
+ // Init — runInit re-resolves the config from disk internally (after any
2001
+ // starter-config write), so we don't need to pre-pass it.
2002
+ if (command === 'init') {
2003
+ const { runInit } = await import('../src/init.mjs');
2004
+ await runInit(process.cwd(), config, { dryRun });
2005
+ return;
2006
+ }
2007
+
2008
+ // Watch is a proxy — re-inject the active globals so the child re-resolves
2009
+ // the same config/filters.
2010
+ if (command === 'watch') { const { runWatch } = await import('../src/watch.mjs'); runWatch([...globalFlagArgs(), ...restArgs], config); return; }
2011
+
2012
+ // Hook commands (`hud`, `guard`) fire in EVERY repo via the globally-enabled
2013
+ // plugin — `guard` runs on every Bash/Read/Edit. They must stay silent where
2014
+ // runlist isn't used, so don't nag them about a missing config (they no-op
2015
+ // cleanly on their own). The warning is still useful for interactive commands.
2016
+ const HOOK_COMMANDS = new Set(['hud', 'guard']);
2017
+ if (!config.configFound && command !== 'init' && !HOOK_COMMANDS.has(command)) {
2018
+ warn('No runlist config found — using defaults. Run `runlist init` to create one.');
2019
+ }
2020
+
2021
+ if (config.configWarnings && config.configWarnings.length > 0) {
2022
+ for (const w of config.configWarnings) {
2023
+ warn(w);
2024
+ }
2025
+ }
2026
+
2027
+ if (verbose) {
2028
+ process.stderr.write(`Config: ${config.configPath ?? 'none'}\n`);
2029
+ const roots = config.docsRoots || [config.docsRoot];
2030
+ process.stderr.write(`Docs root${roots.length > 1 ? 's' : ''}: ${roots.join(', ')}\n`);
2031
+ process.stderr.write(`Repo root: ${config.repoRoot}\n`);
2032
+ }
2033
+
2034
+ // A printed list shows status, title, age and next step, none of which the
2035
+ // validating passes produce, so it reads the index the way `prompts` and
2036
+ // `hud` do. `--json` emits each document's warnings and errors, so it still
2037
+ // pays for the full pass.
2038
+ const listIndexOptions = listArgs => (listArgs.includes('--json') ? {} : { fast: true });
2039
+
2040
+ // Preset aliases (user config can override built-in commands below)
2041
+ if ((command === 'stale' || command === 'actionable') && !config.configuredPresetNames.has(command)) {
2042
+ const { buildIndex } = await import('../src/index.mjs');
2043
+ const { runQuery } = await import('../src/query.mjs');
2044
+ const { statusMetadataFor } = await import('../src/status-metadata.mjs');
2045
+ const index = buildIndex(config, listIndexOptions(restArgs));
2046
+ applyIndexFilters(index);
2047
+ const docs = index.docs.filter(doc => {
2048
+ const metadata = statusMetadataFor(config, doc.type, doc.status);
2049
+ if (command === 'stale') return doc.isStale && !metadata?.skipStale;
2050
+ return metadata?.context === 'expanded'
2051
+ && doc.hasNextStep
2052
+ && !metadata.terminal
2053
+ && !metadata.archive
2054
+ && !isArchivedPath(doc.path, config);
2055
+ });
2056
+ runQuery({ ...index, docs }, ['--sort', 'updated', '--all', ...restArgs], config, { preset: command, type: typeArg, root: rootArg });
2057
+ return;
2058
+ }
2059
+ if (config.presets[command]) {
2060
+ const { buildIndex } = await import('../src/index.mjs');
2061
+ const { runQuery } = await import('../src/query.mjs');
2062
+ const index = buildIndex(config, listIndexOptions([...config.presets[command], ...restArgs]));
2063
+ applyIndexFilters(index);
2064
+ runQuery(index, [...config.presets[command], ...restArgs], config, { preset: command, type: typeArg, root: rootArg });
2065
+ return;
2066
+ }
2067
+
2068
+ // Built-in list commands — stable across projects regardless of preset config.
2069
+ // Two views per type:
2070
+ // `runlist plans` triage — top 10 by recency, flat with right-aligned [TAG]
2071
+ // `runlist plans status` pipeline — grouped by status, no per-row tag, all plans
2072
+ if (command === 'plans') {
2073
+ const { buildIndex } = await import('../src/index.mjs');
2074
+ const { runQuery, parseQueryArgs } = await import('../src/query.mjs');
2075
+ const index = buildIndex(config, listIndexOptions(restArgs));
2076
+ applyIndexFilters(index);
2077
+ const sub = restArgs[0];
2078
+ let defaults;
2079
+ let extras = restArgs;
2080
+ if (sub === 'status') {
2081
+ defaults = ['--type', 'plan', '--exclude-archived', '--sort', 'status', '--all'];
2082
+ extras = restArgs.slice(1);
2083
+ } else {
2084
+ const stageGrouped = parseQueryArgs(restArgs).group === 'stage';
2085
+ defaults = ['--type', 'plan', '--exclude-archived', '--sort', 'updated',
2086
+ ...(stageGrouped && !restArgs.includes('--limit') ? ['--all'] : ['--limit', '10'])];
2087
+ }
2088
+ runQuery(index, [...defaults, ...extras], config, { preset: 'plans', type: typeArg, root: rootArg });
2089
+ return;
2090
+ }
2091
+ // `runlist runlists` (plural) — the coordination-hub dashboard (the `Runlists`
2092
+ // section of `runlist plans`, standalone). Distinct from `runlist runlist <hub>`
2093
+ // (singular), which walks one hub's children.
2094
+ if (command === 'runlists') {
2095
+ const { buildIndex } = await import('../src/index.mjs');
2096
+ const { runRunlists } = await import('../src/query.mjs');
2097
+ const index = buildIndex(config, listIndexOptions(restArgs));
2098
+ applyIndexFilters(index);
2099
+ runRunlists(index, restArgs, config);
2100
+ return;
2101
+ }
2102
+ // `runlist roadmaps` (dashboard over every roadmap hub) and `runlist roadmap
2103
+ // [<hub>] [next]` (one roadmap, or pick up the next action across its
2104
+ // runlists). The tier-3 layer above `runlists`.
2105
+ if (command === 'roadmaps') {
2106
+ const { buildIndex } = await import('../src/index.mjs');
2107
+ const { runRoadmaps } = await import('../src/roadmap.mjs');
2108
+ const index = buildIndex(config, listIndexOptions(restArgs));
2109
+ applyIndexFilters(index);
2110
+ runRoadmaps(index, restArgs, config);
2111
+ return;
2112
+ }
2113
+ if (command === 'roadmap') {
2114
+ const { buildIndex } = await import('../src/index.mjs');
2115
+ const index = buildIndex(config);
2116
+ applyIndexFilters(index);
2117
+ if (restArgs[0] === 'next') {
2118
+ const { runRoadmapNext } = await import('../src/roadmap.mjs');
2119
+ await runRoadmapNext(index, restArgs.slice(1), config, { dryRun });
2120
+ return;
2121
+ }
2122
+ const { runRoadmap } = await import('../src/roadmap.mjs');
2123
+ runRoadmap(index, restArgs, config);
2124
+ return;
2125
+ }
2126
+ if (command === 'prompts') {
2127
+ const { runPrompts } = await import('../src/prompts.mjs');
2128
+ await runPrompts(restArgs, config, { dryRun, verbose });
2129
+ return;
2130
+ }
2131
+ // Top-level `runlist use [file]` — the single "start engaging with this doc"
2132
+ // verb. Dispatches by the target doc's type: prompt → consume + archive,
2133
+ // plan → mark in-session + print, doc → print. With no file: consume oldest
2134
+ // pending prompt. See src/use.mjs for the dispatch table.
2135
+ if (command === 'use') {
2136
+ const { runUse } = await import('../src/use.mjs');
2137
+ await runUse(restArgs, config, { dryRun });
2138
+ return;
2139
+ }
2140
+ // `runlist baton [plan] <@draft|->` — the one-command handoff: save the resume
2141
+ // prompt, release the plan (one status flip), print the exact commit. See
2142
+ // src/baton.mjs for why this is a single verb and not a skill choreography.
2143
+ if (command === 'baton') {
2144
+ const { runBaton } = await import('../src/baton.mjs');
2145
+ await runBaton(restArgs, config, { dryRun });
2146
+ return;
2147
+ }
2148
+ // `runlist next` is a top-level alias for `runlist use` with no arg — consume
2149
+ // the oldest pending prompt. Wired separately so agents who reach for the
2150
+ // literal verb "next" don't bounce off an Unknown-command. Any positional
2151
+ // arg is ignored (a named file goes through `use`).
2152
+ if (command === 'next') {
2153
+ const { runUse } = await import('../src/use.mjs');
2154
+ await runUse(restArgs.filter(a => a.startsWith('-')), config, { dryRun });
2155
+ return;
2156
+ }
2157
+
2158
+ // Commands that handle their own index building
2159
+ if (command === 'app') { const { runApp } = await import('../src/app.mjs'); await runApp(restArgs, config, { dryRun }); return; }
2160
+ if (command === 'yardstick') { const { runYardstick } = await import('../src/yardstick-command.mjs'); await runYardstick(restArgs, config, { dryRun }); return; }
2161
+ if (command === 'diff') { const { runDiff } = await import('../src/diff.mjs'); runDiff(restArgs, config); return; }
2162
+ if (command === 'summary') { const { runSummary } = await import('../src/summary.mjs'); runSummary(restArgs, config); return; }
2163
+ if (command === 'deps') { const { runDeps } = await import('../src/deps.mjs'); runDeps(restArgs, config); return; }
2164
+ if (command === 'unblocks') { const { runUnblocks } = await import('../src/deps.mjs'); runUnblocks(restArgs, config); return; }
2165
+ if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
2166
+ if (command === 'flags') { const { runFlags } = await import('../src/flags.mjs'); runFlags(restArgs, config); return; }
2167
+ if (command === 'flag') { const { runFlag } = await import('../src/flags.mjs'); runFlag(restArgs, config); return; }
2168
+ if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
2169
+ if (command === 'model') { const { runModel } = await import('../src/model.mjs'); await runModel(restArgs, config); return; }
2170
+ if (command === 'show') { const { runShow } = await import('../src/show.mjs'); runShow(restArgs, config); return; }
2171
+ if (command === 'xref') {
2172
+ const { runXref } = await import('../src/xref.mjs');
2173
+ const findings = runXref(restArgs, config);
2174
+ if (restArgs.includes('--check') && findings.length) {
2175
+ const first = findings[0];
2176
+ _exitFailureMessage = `${findings.length} cross-reference finding(s); first: ${first.file}:${first.line}: ${first.text}`;
2177
+ }
2178
+ return;
2179
+ }
2180
+ if (command === 'record') {const {runRecordAction}=await import('../src/record-action.mjs');runRecordAction(restArgs,config,{dryRun});return;}
2181
+ if (command === 'decision') {const {runDecisionAction}=await import('../src/decision-action.mjs');await runDecisionAction(restArgs,config,{dryRun});return;}
2182
+ if (command === 'decisions') {
2183
+ const { runDecisions } = await import('../src/decisions.mjs');
2184
+ const result = runDecisions(restArgs, config);
2185
+ if (result?.defects?.length) {
2186
+ const first = result.defects[0];
2187
+ _exitFailureMessage = `${result.defects.length} decision defect(s); first: ${first.doc}:${first.line} ${first.message}`;
2188
+ }
2189
+ return;
2190
+ }
2191
+ if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
2192
+
2193
+ // Lifecycle commands
2194
+ if (command === 'hud' && restArgs.includes('--prompt-submit')) {
2195
+ const { runPromptSubmitHud } = await import('../src/hud.mjs');
2196
+ const { readHookStdin } = await import('../src/guard.mjs');
2197
+ await runPromptSubmitHud(config, { readStdin: readHookStdin });
2198
+ return;
2199
+ }
2200
+ if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
2201
+ if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config, { dryRun }); return; }
2202
+ if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config, { dryRun }); return; }
2203
+ if (command === 'install') { const { runInstall } = await import('../src/install.mjs'); runInstall(restArgs, config, { dryRun }); return; }
2204
+ if (command === 'errors') { const { runErrors } = await import('../src/errors-read.mjs'); runErrors(restArgs); return; }
2205
+ if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
2206
+ if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
2207
+ if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
2208
+ die(`\`runlist ${command}\` was removed — use the ownership-aware lifecycle verbs:\n runlist use <file> # atomically claim + mark in-session + print the plan\n runlist set <status> <file> # transition and release ownership when leaving in-session\n runlist archive <file> # close out atomically`);
2209
+ }
2210
+ if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
2211
+ if (command === 'handoff') { die('`runlist handoff` was removed in 0.31.0. Use `runlist prompts new <name>` to create a saved prompt instead. The .dotmd/handoffs/ sidecar mechanism no longer exists; see CHANGELOG.'); }
2212
+ if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
2213
+ if (command === 'set') { const { runSet } = await import('../src/lifecycle.mjs'); await runSet(restArgs, config, { dryRun }); return; }
2214
+ if (command === 'ship') { const { runShip } = await import('../src/ship.mjs'); await runShip(restArgs, config, { dryRun }); return; }
2215
+ if (command === 'archive') { const { runArchive } = await import('../src/lifecycle.mjs'); runArchive(restArgs, config, { dryRun }); return; }
2216
+ if (command === 'bulk' && restArgs[0] === 'archive') { const { runBulkArchive } = await import('../src/lifecycle.mjs'); runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
2217
+ if (command === 'bulk' && restArgs[0] === 'tag') { const { runBulkTag } = await import('../src/bulk-tag.mjs'); runBulkTag(restArgs.slice(1), config, { dryRun }); return; }
2218
+ if (command === 'bulk-tag') { const { runBulkTag } = await import('../src/bulk-tag.mjs'); runBulkTag(restArgs, config, { dryRun }); return; }
2219
+ if (command === 'touch') { const { runTouch } = await import('../src/lifecycle.mjs'); runTouch(restArgs, config, { dryRun }); return; }
2220
+ if (command === 'new') { const { runNew } = await import('../src/new.mjs'); await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
2221
+ if (command === 'lint') { const { runLint } = await import('../src/lint.mjs'); runLint(restArgs, config, { dryRun }); return; }
2222
+ if (command === 'rename') { const { runRename } = await import('../src/rename.mjs'); await runRename(restArgs, config, { dryRun }); return; }
2223
+ if (command === 'migrate') { const { runMigrate } = await import('../src/migrate.mjs'); runMigrate(restArgs, config, { dryRun }); return; }
2224
+ if (command === 'fix-refs') { const { runFixRefs } = await import('../src/fix-refs.mjs'); runFixRefs(restArgs, config, { dryRun }); return; }
2225
+ if (command === 'refs') { const { runRefs } = await import('../src/code-refs.mjs'); runRefs(restArgs, config, { dryRun }); return; }
2226
+ if (command === 'fix-membership') { const { runFixMembership } = await import('../src/fix-membership.mjs'); await runFixMembership(restArgs, config, { dryRun }); return; }
2227
+ if (command === 'sync-status') { const { runSyncStatus } = await import('../src/sync-status.mjs'); await runSyncStatus(restArgs, config, { dryRun }); return; }
2228
+ if (command === 'self-check') {
2229
+ const { runDoctor } = await import('../src/doctor.mjs');
2230
+ runDoctor(['--project', ...restArgs], config, { dryRun });
2231
+ return;
2232
+ }
2233
+ if (command === 'doctor') {
2234
+ // 0.37.0 (F4): the default auto-fix loop previews by default; --apply
2235
+ // (alias --yes) writes. Explicit --dry-run still works and wins over
2236
+ // --apply (safety prevails). The F4 flip applies ONLY to the default
2237
+ // auto-fix path — sub-modes (--statuses, --migrate-template,
2238
+ // --migrate-prompts) keep their existing "write unless --dry-run"
2239
+ // contract because they're explicit one-shots the user opted into.
2240
+ const doctorDryRun = doctorSubMode ? dryRun : (dryRun || !doctorExplicitApply);
2241
+ const filtered = restArgs.filter(a => a !== '--apply' && a !== '--yes');
2242
+ const { runDoctor } = await import('../src/doctor.mjs');
2243
+ // Awaited because --claims releases through `runSet`, which is async; the
2244
+ // other modes return undefined and are unaffected.
2245
+ await runDoctor(filtered, config, { dryRun: doctorDryRun });
2246
+ return;
2247
+ }
2248
+ if (command === 'statuses') { const { runStatuses } = await import('../src/statuses.mjs'); await runStatuses(restArgs, config, { dryRun, type: typeArg }); return; }
2249
+
2250
+ // All remaining commands need the index + render modules
2251
+ if (command === 'check' && restArgs.includes('--flag') && !dryRun && (rootArg || typeArg)) {
2252
+ die('`--flag` runs on the whole repository; drop the --root / --type filters.');
2253
+ }
2254
+ const { buildIndex } = await import('../src/index.mjs');
2255
+ const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage, buildReferenceValidationCoverage } = await import('../src/render.mjs');
2256
+ const { runFocus, runQuery } = await import('../src/query.mjs');
2257
+ // `runlist check` is the one shared-buildIndex command that should auto-heal a
2258
+ // drifted index block (frontmatter edits by direct Edit/Write, `lint --fix`,
2259
+ // etc. leave the README out of sync; demanding the user run `runlist index`
2260
+ // each time was pure noise). Print/dry-run/read-only callers (`json`, `list`,
2261
+ // `query`, `index --print`, ...) stay opt-out so they never mutate disk.
2262
+ const checkHasPathScope = command === 'check' && restArgs.some(arg => !arg.startsWith('-'));
2263
+ const AUTO_HEAL_INDEX_COMMANDS = new Set(['check']);
2264
+ const index = buildIndex(config, {
2265
+ autoHealIndex: AUTO_HEAL_INDEX_COMMANDS.has(command) && !checkHasPathScope && !dryRun,
2266
+ invokeHooks: !suppressSideEffects,
2267
+ });
2268
+
2269
+ applyIndexFilters(index);
2270
+
2271
+ if (verbose) {
2272
+ process.stderr.write(`Docs found: ${index.docs.length}\n`);
2273
+ }
2274
+
2275
+ if (command === 'json') {
2276
+ process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
2277
+ return;
2278
+ }
2279
+
2280
+ if (command === 'list') {
2281
+ if (args.includes('--json')) {
2282
+ process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
2283
+ } else if (args.includes('--verbose')) {
2284
+ process.stdout.write(renderVerboseList(index, config));
2285
+ } else {
2286
+ process.stdout.write(renderCompactList(index, config));
2287
+ }
2288
+ return;
2289
+ }
2290
+
2291
+ if (command === 'check') {
2292
+ const fix = args.includes('--fix');
2293
+ const errorsOnly = args.includes('--errors-only');
2294
+ const noCollapse = args.includes('--no-collapse');
2295
+ const verbose = args.includes('--verbose');
2296
+ // `--min-docs N` overrides the configured floor for this run (CI passes it
2297
+ // without editing config). Its value is not a path target.
2298
+ const minDocsFlagIdx = restArgs.indexOf('--min-docs');
2299
+ const minDocsRaw = minDocsFlagIdx === -1 ? null : restArgs[minDocsFlagIdx + 1];
2300
+ if (minDocsFlagIdx !== -1 && !/^\d+$/.test(minDocsRaw ?? '')) {
2301
+ die('`--min-docs` needs a positive integer, e.g. `runlist check --min-docs 500`.');
2302
+ }
2303
+ const minDocsOverride = minDocsRaw == null ? null : Number(minDocsRaw);
2304
+ const minDocsValueIdx = minDocsFlagIdx === -1 ? -1 : minDocsFlagIdx + 1;
2305
+ const checkTargets = restArgs.filter((arg, i) => !arg.startsWith('-') && i !== minDocsValueIdx);
2306
+ const { applyScanFloor } = await import('../src/validate.mjs');
2307
+ const scanFloorConfig = minDocsOverride == null ? config : { ...config, minDocs: minDocsOverride };
2308
+ const applyFloor = (idx) => applyScanFloor(idx, scanFloorConfig, { scoped: checkTargets.length > 0 });
2309
+ const skippedCheckHooks = config._execution?.suppressSideEffects
2310
+ ? ['validate', 'transformDoc', 'formatSnapshot', 'renderCheck']
2311
+ .filter(name => typeof config.hooks?.[name] === 'function')
2312
+ : [];
2313
+ // `--flag` puts each error on the flags list, attributed to this check, and
2314
+ // resolves the check's earlier flags it no longer reports. Whole-repo runs
2315
+ // only: a scoped run cannot tell a fixed error from one outside its scope.
2316
+ const flagCheckErrors = async (checkIndex) => {
2317
+ if (!args.includes('--flag') || dryRun) return;
2318
+ if (checkTargets.length > 0) die('`--flag` runs on the whole repository; drop the path arguments.');
2319
+ const { syncCheckFlags } = await import('../src/flags.mjs');
2320
+ const findings = checkIndex.errors.filter(e => e.path).map(e => ({ file: e.path, text: e.message }));
2321
+ const complete = checkIndex.scanCoverage?.complete !== false && skippedCheckHooks.length === 0 && !checkIndex.errors.some(e => !e.path);
2322
+ const { added, resolved } = syncCheckFlags(config, 'runlist check', findings, { complete });
2323
+ process.stderr.write(`flags: ${added} added, ${resolved} resolved\n`);
2324
+ };
2325
+ const checkJson = (checkIndex) => {
2326
+ const builtInPassed = checkIndex.errors.length === 0;
2327
+ const complete = skippedCheckHooks.length === 0 && checkIndex.scanCoverage?.complete !== false && checkIndex.filingCoverage?.complete !== false;
2328
+ return {
2329
+ filingCoverage: scopeFilingCoverage(checkIndex.filingCoverage, checkIndex.docs),
2330
+ stageCoverage: checkIndex.docs.filter(doc => doc.type === 'plan').reduce((coverage, doc) => {
2331
+ const stage = readPlanStage(doc.ships);
2332
+ coverage[stage.invalid ? 'invalid' : stage.word === null ? 'unset' : 'set'] += 1;
2333
+ return coverage;
2334
+ }, { set: 0, unset: 0, invalid: 0 }),
2335
+ docsScanned: checkIndex.docs.length,
2336
+ documentsChecked: checkIndex.docs.map(doc => doc.path),
2337
+ scanCoverage: checkIndex.scanCoverage,
2338
+ errors: checkIndex.errors,
2339
+ warnings: errorsOnly ? [] : checkIndex.warnings,
2340
+ errorCount: checkIndex.errors.length,
2341
+ warningCount: checkIndex.warnings.length,
2342
+ referenceValidation: buildReferenceValidationCoverage(checkIndex, config),
2343
+ passed: complete ? builtInPassed : null,
2344
+ ...(complete ? {} : {
2345
+ builtInPassed,
2346
+ validationPreview: { status: skippedCheckHooks.length ? 'built-in-only' : 'incomplete', skippedHooks: skippedCheckHooks },
2347
+ }),
2348
+ };
2349
+ };
2350
+ const writeCheckPreviewNote = () => {
2351
+ if (skippedCheckHooks.length > 0) {
2352
+ process.stdout.write(`[preview] Custom ${skippedCheckHooks.join(', ')} hook${skippedCheckHooks.length === 1 ? '' : 's'} skipped; results below cover built-in behavior only.\n`);
2353
+ }
2354
+ };
2355
+
2356
+ if (fix && checkTargets.length > 0) {
2357
+ die('`runlist check --fix` does not support path-scoped checks yet. Run `runlist check <path>` to validate a subset, or `runlist check --fix` to fix the whole docs tree.');
2358
+ }
2359
+
2360
+ if (fix) {
2361
+ // Auto-fix: broken refs, mechanical membership back-references, lint,
2362
+ // hub status tokens, then rebuild the index.
2363
+ const { fixBrokenRefs } = await import('../src/fix-refs.mjs');
2364
+ const { fixMembershipBackrefs } = await import('../src/fix-membership.mjs');
2365
+ const { runLint } = await import('../src/lint.mjs');
2366
+ const { syncHubStatuses } = await import('../src/sync-status.mjs');
2367
+ fixBrokenRefs(config, { dryRun, quiet: false });
2368
+ fixMembershipBackrefs(config, { docs: buildIndex(config).docs, dryRun, quiet: false });
2369
+ runLint(['--fix'], config, { dryRun });
2370
+ // Rewrites drifted status TOKENS only. Adding markers is a content edit to
2371
+ // prose the user wrote, so it stays opt-in behind `sync-status --adopt`.
2372
+ syncHubStatuses(config, { docs: buildIndex(config).docs, dryRun, quiet: false });
2373
+ if (config.indexPath) {
2374
+ if (!dryRun) {
2375
+ const { writeRenderedIndex } = await import('../src/index-file.mjs');
2376
+ writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
2377
+ process.stdout.write('Index regenerated.\n');
2378
+ } else {
2379
+ process.stdout.write('[dry-run] Would regenerate index.\n');
2380
+ }
2381
+ }
2382
+ // Show remaining issues
2383
+ const freshIndex = buildIndex(config);
2384
+ applyIndexFilters(freshIndex);
2385
+ applyPathScopeToIndex(freshIndex, config, checkTargets);
2386
+ applyFloor(freshIndex);
2387
+ await flagCheckErrors(freshIndex);
2388
+ if (args.includes('--json')) {
2389
+ process.stdout.write(JSON.stringify(checkJson(freshIndex), null, 2) + '\n');
2390
+ } else {
2391
+ writeCheckPreviewNote();
2392
+ process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse, verbose }));
2393
+ }
2394
+ if (freshIndex.errors.length > 0) {
2395
+ process.exitCode = 1;
2396
+ _exitFailureMessage = checkFailureSummary(freshIndex.errors);
2397
+ _exitFailureFamily = checkFailureFamily(freshIndex);
2398
+ }
2399
+ return;
2400
+ }
2401
+
2402
+ applyPathScopeToIndex(index, config, checkTargets);
2403
+ applyFloor(index);
2404
+ await flagCheckErrors(index);
2405
+
2406
+ if (args.includes('--json')) {
2407
+ process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
2408
+ if (index.errors.length > 0) {
2409
+ process.exitCode = 1;
2410
+ _exitFailureMessage = checkFailureSummary(index.errors);
2411
+ _exitFailureFamily = checkFailureFamily(index);
2412
+ }
2413
+ return;
2414
+ }
2415
+
2416
+ writeCheckPreviewNote();
2417
+ process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse, verbose }));
2418
+ if (index.errors.length > 0) {
2419
+ process.exitCode = 1;
2420
+ _exitFailureMessage = checkFailureSummary(index.errors);
2421
+ _exitFailureFamily = checkFailureFamily(index);
2422
+ }
2423
+ return;
2424
+ }
2425
+
2426
+ if (command === 'coverage') {
2427
+ if (args.includes('--json')) {
2428
+ process.stdout.write(`${JSON.stringify(buildCoverage(index, config), null, 2)}\n`);
2429
+ } else {
2430
+ process.stdout.write(renderCoverage(index, config));
2431
+ }
2432
+ return;
2433
+ }
2434
+
2435
+ if (command === 'stats') {
2436
+ const { buildStats, renderStats, renderStatsJson } = await import('../src/stats.mjs');
2437
+ const stats = buildStats(index, config);
2438
+ if (args.includes('--json')) {
2439
+ process.stdout.write(renderStatsJson(stats));
2440
+ } else {
2441
+ process.stdout.write(renderStats(stats, config));
2442
+ }
2443
+ return;
2444
+ }
2445
+
2446
+ if (command === 'index') {
2447
+ if (!config.indexPath) {
2448
+ die('Index generation is not configured. Add an `index` section to your runlist.config.mjs.');
2449
+ }
2450
+ const print = args.includes('--print');
2451
+ const { renderIndexFile, writeRenderedIndex } = await import('../src/index-file.mjs');
2452
+ if (!print) {
2453
+ const { authorizeRepoGeneratedPath } = await import('../src/managed-path.mjs');
2454
+ authorizeRepoGeneratedPath(config.indexPath, config, { kind: 'Generated index destination' });
2455
+ }
2456
+ const rendered = renderIndexFile(index, config);
2457
+ if (print) {
2458
+ process.stdout.write(rendered);
2459
+ } else if (dryRun) {
2460
+ process.stdout.write(`[dry-run] Would update ${config.indexPath}\n`);
2461
+ } else {
2462
+ writeRenderedIndex(() => buildIndex(config, { fast: true }), config);
2463
+ process.stdout.write(`Updated ${config.indexPath}\n`);
2464
+ }
2465
+ return;
2466
+ }
2467
+
2468
+ if (command === 'focus') { runFocus(index, restArgs, config); return; }
2469
+ if (command === 'query') { runQuery(index, restArgs, config, { type: typeArg, root: rootArg }); return; }
2470
+ // `runlist grep <term>` — ergonomic alias for `query --keyword <term> --body`.
2471
+ // Unlimited by default (grep semantics) unless the caller bounds it themselves.
2472
+ if (command === 'grep') {
2473
+ let term = null;
2474
+ const passthrough = [];
2475
+ for (let i = 0; i < restArgs.length; i++) {
2476
+ const arg = restArgs[i];
2477
+ if (QUERY_VALUE_FLAGS.has(arg)) { passthrough.push(arg, restArgs[i + 1]); i += 1; continue; }
2478
+ if (arg.startsWith('-') || term !== null) { passthrough.push(arg); continue; }
2479
+ term = arg;
2480
+ }
2481
+ if (!term) die('Usage: runlist grep <term> [query flags]\n\nSearches frontmatter fields AND document bodies; alias for `runlist query --keyword <term> --body --all`.');
2482
+ const defaults = ['--keyword', term, '--body'];
2483
+ if (!passthrough.includes('--limit') && !passthrough.includes('--all')) defaults.push('--all');
2484
+ runQuery(index, [...defaults, ...passthrough], config);
2485
+ return;
2486
+ }
2487
+ if (command === 'modules' || command === 'module') {
2488
+ // D3: default `--type plan` when the user didn't pass --type explicitly.
2489
+ // applyIndexFilters already narrowed by typeArg if it was set; if not, the
2490
+ // index still spans all types, and the dashboard would mix plans/docs/prompts
2491
+ // into the same module rows. Narrow here so the docs/prompts case stays a
2492
+ // deliberate `--type doc` opt-in (deferred per plan).
2493
+ const scoped = typeArg ? index : { ...index, docs: index.docs.filter(d => d.type === 'plan') };
2494
+ if (command === 'modules') {
2495
+ const { runModulesDashboard } = await import('../src/modules.mjs');
2496
+ runModulesDashboard(scoped, restArgs, config);
2497
+ } else {
2498
+ const { runModuleDetail } = await import('../src/modules.mjs');
2499
+ runModuleDetail(scoped, restArgs, config);
2500
+ }
2501
+ return;
2502
+ }
2503
+ if (command === 'surfaces') {
2504
+ const { runSurfaces } = await import('../src/surfaces.mjs');
2505
+ runSurfaces(restArgs, config);
2506
+ return;
2507
+ }
2508
+
2509
+ if (command === 'agent-context') {
2510
+ const { buildAgentContext, parseAgentContextOptions } = await import('../src/agent-context.mjs');
2511
+ const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot'].filter(name => typeof config.hooks?.[name] === 'function');
2512
+ process.stdout.write(JSON.stringify(buildAgentContext(index, config, {
2513
+ ...parseAgentContextOptions(restArgs),
2514
+ roots: rootArg ? [rootArg] : null,
2515
+ types: typeArg ? typeArg.split(',').map(value => value.trim()).filter(Boolean) : null,
2516
+ skippedHooks,
2517
+ }), null, 2) + '\n');
2518
+ return;
2519
+ }
2520
+
2521
+ if (command === 'briefing') {
2522
+ if (args.includes('--json')) {
2523
+ const { statusMetadataFor } = await import('../src/status-metadata.mjs');
2524
+ const plans = index.docs.filter(d => d.type === 'plan');
2525
+ const docs = index.docs.filter(d => d.type === 'doc');
2526
+ const research = index.docs.filter(d => d.type === 'research');
2527
+ const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale).length;
2528
+ // Coordination hubs are runlists, not actionable plans — split them out of
2529
+ // inSession/active into their own `runlists` array so the JSON mirrors the
2530
+ // rendered briefing. Empty on repos with no coordination hubs.
2531
+ const { buildCoordinationIndex } = await import('../src/runlist.mjs');
2532
+ const coordination = buildCoordinationIndex(index, config);
2533
+ const isHub = (d) => coordination.has(d.path);
2534
+ const closedStatuses = new Set([...config.lifecycle.archiveStatuses, ...config.lifecycle.terminalStatuses]);
2535
+ const isLiveHub = (d) => isHub(d) && !closedStatuses.has(d.status) && !isArchivedPath(d.path, config);
2536
+ process.stdout.write(JSON.stringify({
2537
+ plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active' && !isHub(d)).map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), focus: plans.filter(d => statusMetadataFor(config, 'plan', d.status)?.context === 'expanded' && !isHub(d)).map(d => ({ path: d.path, title: d.title, status: d.status, nextStep: d.nextStep })), runlists: plans.filter(isLiveHub).map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0 })) },
2538
+ docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
2539
+ research: { total: research.length, active: research.filter(d => d.status === 'active').length },
2540
+ stale, errorCount: index.errors.length, warningCount: index.warnings.length,
2541
+ }, null, 2) + '\n');
2542
+ } else {
2543
+ process.stdout.write(renderBriefing(index, config));
2544
+ }
2545
+ return;
2546
+ }
2547
+
2548
+ if (command === 'context') {
2549
+ const summarize = args.includes('--summarize');
2550
+ const compact = args.includes('--compact');
2551
+ const modelIdx = args.indexOf('--model');
2552
+ const model = modelIdx !== -1 && args[modelIdx + 1] ? args[modelIdx + 1] : undefined;
2553
+
2554
+ if (args.includes('--json')) {
2555
+ if (compact) {
2556
+ const { buildAgentContext, parseAgentContextOptions } = await import('../src/agent-context.mjs');
2557
+ const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot'].filter(name => typeof config.hooks?.[name] === 'function');
2558
+ process.stdout.write(JSON.stringify(buildAgentContext(index, config, {
2559
+ ...parseAgentContextOptions(restArgs),
2560
+ roots: rootArg ? [rootArg] : null,
2561
+ types: typeArg ? typeArg.split(',').map(value => value.trim()).filter(Boolean) : null,
2562
+ skippedHooks,
2563
+ }), null, 2) + '\n');
2564
+ return;
2565
+ }
2566
+ const byStatus = {};
2567
+ for (const doc of index.docs) {
2568
+ const s = doc.status ?? 'unknown';
2569
+ if (!byStatus[s]) byStatus[s] = [];
2570
+ byStatus[s].push(doc);
2571
+ }
2572
+ const byType = {};
2573
+ for (const doc of index.docs) {
2574
+ if (doc.type) {
2575
+ if (!byType[doc.type]) byType[doc.type] = [];
2576
+ byType[doc.type].push(doc);
2577
+ }
2578
+ }
2579
+ if (summarize && !config._execution?.suppressSideEffects) {
2580
+ const { summarizeDocBody } = await import('../src/ai.mjs');
2581
+ const { extractFrontmatter } = await import('../src/frontmatter.mjs');
2582
+ const { readFileSync } = await import('node:fs');
2583
+ const limit = 5;
2584
+ for (let i = 0; i < index.docs.length && i < limit; i++) {
2585
+ try {
2586
+ const absPath = path.resolve(config.repoRoot, index.docs[i].path);
2587
+ const raw = readFileSync(absPath, 'utf8');
2588
+ const { body } = extractFrontmatter(raw);
2589
+ if (body?.trim()) {
2590
+ const meta = { title: index.docs[i].title, status: index.docs[i].status, path: index.docs[i].path };
2591
+ index.docs[i].aiSummary = config.hooks.summarizeDoc
2592
+ ? config.hooks.summarizeDoc(body, meta)
2593
+ : summarizeDocBody(body, meta, { model });
2594
+ }
2595
+ } catch { /* skip */ }
2596
+ }
2597
+ }
2598
+ const { statusMetadataFor } = await import('../src/status-metadata.mjs');
2599
+ const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale);
2600
+ process.stdout.write(JSON.stringify({
2601
+ generatedAt: new Date().toISOString(),
2602
+ ...(summarize && config._execution?.suppressSideEffects
2603
+ ? { summaryPreview: { status: 'skipped-preview', reason: 'side-effect-free preview' } }
2604
+ : {}),
2605
+ docsByType: Object.keys(byType).length > 0 ? byType : undefined,
2606
+ docsByStatus: byStatus,
2607
+ countsByStatus: index.countsByStatus,
2608
+ stale: stale.map(d => ({ path: d.path, title: d.title, daysSinceUpdate: d.daysSinceUpdate })),
2609
+ errorCount: index.errors.length,
2610
+ warningCount: index.warnings.length,
2611
+ }, null, 2) + '\n');
2612
+ return;
2613
+ }
2614
+ process.stdout.write(renderContext(index, config, { summarize, model }));
2615
+ return;
2616
+ }
2617
+
2618
+ if (command === 'graph') {
2619
+ const { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } = await import('../src/graph.mjs');
2620
+ const statusFilter = (() => { const i = args.indexOf('--status'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
2621
+ const moduleFilter = (() => { const i = args.indexOf('--module'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
2622
+ const surfaceFilter = (() => { const i = args.indexOf('--surface'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
2623
+ const graph = buildGraph(index, config, {
2624
+ statuses: statusFilter?.split(',') ?? null,
2625
+ module: moduleFilter,
2626
+ surface: surfaceFilter,
2627
+ });
2628
+ if (args.includes('--dot')) {
2629
+ process.stdout.write(renderGraphDot(graph, config));
2630
+ } else if (args.includes('--json')) {
2631
+ process.stdout.write(renderGraphJson(graph));
2632
+ } else {
2633
+ process.stdout.write(renderGraphText(graph, config));
2634
+ }
2635
+ return;
2636
+ }
2637
+
2638
+ requireCommandPolicy(command, null);
2639
+ }
2640
+
2641
+ // F17a: opt-in JSONL journal of every CLI invocation. The dispatch tail
2642
+ // records argv / exit / elapsed-ms / err once main() either returns or
2643
+ // throws — config is captured into the module-level _resolvedConfig the
2644
+ // moment it's loaded, so even early dispatcher errors (after config) get
2645
+ // journaled.
2646
+ let _resolvedConfig = null;
2647
+ let _resolvedCommand = null;
2648
+ let _suppressObservability = false;
2649
+ let _exitFailureMessage = null;
2650
+ let _exitFailureFamily = null;
2651
+ let _helpTopic = null;
2652
+ const _startMs = Date.now();
2653
+ const _invocationArgs = process.argv.slice(2);
2654
+
2655
+ function checkFailureSummary(errors) {
2656
+ const first = errors[0];
2657
+ return `${errors.length} check error(s); first: ${first.path ? `${first.path}: ` : ''}${first.message}`;
2658
+ }
2659
+
2660
+ function checkFailureFamily(index) {
2661
+ return index.scanCoverage?.complete === false || index.errors.some(e => e.meta?.kind === 'scan-floor') ? 'command-error' : 'validation';
2662
+ }
2663
+
2664
+ function _journalExit(err) {
2665
+ if (_suppressObservability || _resolvedCommand === 'hud' || _invocationArgs.includes('--dry-run') || _invocationArgs.includes('-n')) return;
2666
+ const code = Number(process.exitCode ?? 0);
2667
+ const failure = err ?? (code !== 0 ? { name: 'ExitStatus', message: _exitFailureMessage ?? `exited with status ${code}`, family: _exitFailureFamily } : null);
2668
+ try {
2669
+ recordCliInvocation({
2670
+ config: _resolvedConfig,
2671
+ startMs: _startMs,
2672
+ args: _invocationArgs,
2673
+ err: failure,
2674
+ version: pkg.version,
2675
+ helpTopic: _helpTopic,
2676
+ });
2677
+ } catch { /* never break exit on journal failure */ }
2678
+ // A command that reports its own failure through the exit code (check with
2679
+ // errors, a failed update step) is a failure too.
2680
+ if (failure) {
2681
+ try {
2682
+ recordGlobalError({
2683
+ config: _resolvedConfig,
2684
+ startMs: _startMs,
2685
+ args: _invocationArgs,
2686
+ err: failure,
2687
+ version: pkg.version,
2688
+ });
2689
+ } catch { /* never break exit on error-log failure */ }
2690
+ }
2691
+ }
2692
+
2693
+ // A reader that stops early (`runlist flags | head`) closes the pipe; the rest
2694
+ // of the output has nowhere to go, which is not a failure.
2695
+ process.stdout.on('error', err => {
2696
+ if (err.code === 'EPIPE') process.exit(0);
2697
+ throw err;
2698
+ });
2699
+
2700
+ main()
2701
+ .then(() => { _journalExit(null); })
2702
+ .catch(err => {
2703
+ let out = err.message;
2704
+ // F17c: append a repeat-failure tip when the journal shows this same shape
2705
+ // has already failed in this session within the lookup window. Lookup is
2706
+ // a no-op when the journal is disabled or RUNLIST_NO_HINTS=1.
2707
+ try {
2708
+ const hint = findRepeatFailureHint(sanitizeTelemetryArgv(_invocationArgs), _resolvedConfig);
2709
+ if (hint) out = `${out}\n\nTip: ${hint}`;
2710
+ } catch { /* hint must never break error reporting */ }
2711
+ process.stderr.write(`${out}\n`);
2712
+ process.exitCode = 1;
2713
+ _journalExit(err);
2714
+ });