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.
- package/LICENSE +21 -0
- package/README.md +1114 -6
- package/assets/app/app.css +283 -0
- package/assets/app/app.mjs +381 -0
- package/assets/app/block-editor.mjs +182 -0
- package/assets/app/block-model.mjs +52 -0
- package/assets/app/document-create.mjs +86 -0
- package/assets/app/document-lifecycle.mjs +36 -0
- package/assets/app/document-stages.mjs +22 -0
- package/assets/app/document-views.mjs +33 -0
- package/assets/app/document-yardstick.mjs +258 -0
- package/assets/app/editor-navigation.mjs +51 -0
- package/assets/app/filing-navigation.mjs +138 -0
- package/assets/app/git-changes.mjs +80 -0
- package/assets/app/git-commit.mjs +46 -0
- package/assets/app/index.html +70 -0
- package/assets/app/library-navigation.mjs +93 -0
- package/assets/app/outline.mjs +18 -0
- package/assets/app/quick-navigation.mjs +52 -0
- package/assets/app/record-navigation.mjs +78 -0
- package/assets/app/recovery-center.mjs +53 -0
- package/assets/app/semantic-search.mjs +14 -0
- package/assets/app/settings.mjs +15 -0
- package/assets/app/shared.mjs +104 -0
- package/assets/app/stage-source.mjs +75 -0
- package/assets/app/template-editor.mjs +47 -0
- package/assets/app/theme.mjs +25 -0
- package/assets/app/transport.mjs +43 -0
- package/assets/contracts/source-editing-v1.md +232 -0
- package/assets/opencode/plugin.js +162 -0
- package/assets/schemas/runlist-record-v1.schema.json +912 -0
- package/bin/dotmd.mjs +5 -0
- package/bin/runlist.mjs +2714 -0
- package/package.json +53 -6
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
- package/runlist.config.example.mjs +370 -0
- package/scripts/postinstall.mjs +56 -0
- package/src/agent-context.mjs +201 -0
- package/src/ai.mjs +30 -0
- package/src/app-create.mjs +169 -0
- package/src/app-filing.mjs +139 -0
- package/src/app-git-commits.mjs +48 -0
- package/src/app-git.mjs +180 -0
- package/src/app-library.mjs +152 -0
- package/src/app-lifecycle-worker.mjs +12 -0
- package/src/app-lifecycle.mjs +79 -0
- package/src/app-records.mjs +134 -0
- package/src/app-search.mjs +80 -0
- package/src/app-semantic-search.mjs +105 -0
- package/src/app-service.mjs +208 -0
- package/src/app-yardstick.mjs +269 -0
- package/src/app.mjs +102 -0
- package/src/atomic-mutation.mjs +1736 -0
- package/src/baton.mjs +435 -0
- package/src/body-link.mjs +48 -0
- package/src/bulk-tag.mjs +174 -0
- package/src/check-collapse.mjs +75 -0
- package/src/claude-commands.mjs +75 -0
- package/src/cli-editor.mjs +8 -0
- package/src/code-refs.mjs +545 -0
- package/src/codex-integration.mjs +81 -0
- package/src/color.mjs +14 -0
- package/src/commands.mjs +374 -0
- package/src/completions.mjs +93 -0
- package/src/config-edit.mjs +606 -0
- package/src/config.mjs +705 -0
- package/src/decision-action.mjs +58 -0
- package/src/decision.mjs +220 -0
- package/src/decisions.mjs +787 -0
- package/src/deps.mjs +317 -0
- package/src/desktop-install.mjs +158 -0
- package/src/desktop-runtime.mjs +4 -0
- package/src/desktop.mjs +25 -0
- package/src/diff.mjs +101 -0
- package/src/doctor.mjs +591 -0
- package/src/durable-rename.mjs +55 -0
- package/src/errors-read.mjs +44 -0
- package/src/export.mjs +496 -0
- package/src/extractors.mjs +101 -0
- package/src/filing.mjs +255 -0
- package/src/fix-membership.mjs +134 -0
- package/src/fix-refs.mjs +174 -0
- package/src/flags.mjs +318 -0
- package/src/frontmatter-fix.mjs +195 -0
- package/src/frontmatter.mjs +252 -0
- package/src/git-commit-filters.mjs +32 -0
- package/src/git-commit-protocol.mjs +198 -0
- package/src/git-commit-signing.mjs +75 -0
- package/src/git-commit-store.mjs +26 -0
- package/src/git-commit-support.mjs +16 -0
- package/src/git-commit-worker.mjs +32 -0
- package/src/git.mjs +909 -0
- package/src/glossary-check.mjs +35 -0
- package/src/glossary.mjs +243 -0
- package/src/graph.mjs +298 -0
- package/src/guard.mjs +488 -0
- package/src/health.mjs +212 -0
- package/src/hints.mjs +129 -0
- package/src/host-integration.mjs +339 -0
- package/src/hub-membership.mjs +284 -0
- package/src/hub.mjs +292 -0
- package/src/hud.mjs +436 -0
- package/src/index-file.mjs +163 -0
- package/src/index.mjs +478 -0
- package/src/init.mjs +512 -0
- package/src/install.mjs +222 -0
- package/src/journal-read.mjs +116 -0
- package/src/journal.mjs +472 -0
- package/src/lifecycle.mjs +1444 -0
- package/src/lint.mjs +386 -0
- package/src/managed-path.mjs +192 -0
- package/src/markdown-code-spans.mjs +58 -0
- package/src/migrate-prompts.mjs +171 -0
- package/src/migrate-template.mjs +170 -0
- package/src/migrate.mjs +90 -0
- package/src/misuse-read.mjs +61 -0
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/modules.mjs +355 -0
- package/src/naming.mjs +55 -0
- package/src/native-action.mjs +59 -0
- package/src/native-record.mjs +244 -0
- package/src/new.mjs +1321 -0
- package/src/output-identity.mjs +111 -0
- package/src/parent-plan.mjs +58 -0
- package/src/parse-cache.mjs +99 -0
- package/src/path-containment.mjs +15 -0
- package/src/pickup-card.mjs +293 -0
- package/src/pickup.mjs +671 -0
- package/src/prompt.mjs +25 -0
- package/src/prompts.mjs +585 -0
- package/src/query.mjs +977 -0
- package/src/record-action.mjs +11 -0
- package/src/record-create.mjs +82 -0
- package/src/reference-planner.mjs +388 -0
- package/src/reference-values.mjs +11 -0
- package/src/rename.mjs +107 -0
- package/src/render.mjs +678 -0
- package/src/roadmap.mjs +184 -0
- package/src/runlist.mjs +944 -0
- package/src/section.mjs +225 -0
- package/src/semantic-search-client.mjs +58 -0
- package/src/ship.mjs +166 -0
- package/src/show.mjs +101 -0
- package/src/skill-drift.mjs +98 -0
- package/src/source-editor.mjs +491 -0
- package/src/stages.mjs +73 -0
- package/src/state-migration.mjs +123 -0
- package/src/stats.mjs +200 -0
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +721 -0
- package/src/summary.mjs +74 -0
- package/src/surfaces.mjs +28 -0
- package/src/sync-status.mjs +316 -0
- package/src/template-store.mjs +62 -0
- package/src/update.mjs +338 -0
- package/src/use.mjs +58 -0
- package/src/util.mjs +337 -0
- package/src/validate.mjs +794 -0
- package/src/watch.mjs +56 -0
- package/src/xref.mjs +406 -0
- package/src/yardstick-command.mjs +178 -0
- package/src/yardstick.mjs +112 -0
package/bin/runlist.mjs
ADDED
|
@@ -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
|
+
});
|