dotmd-cli 0.78.0 → 0.79.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/README.md +66 -63
- package/assets/opencode/plugin.js +11 -6
- package/bin/dotmd.mjs +272 -285
- package/package.json +2 -2
- package/scripts/postinstall.mjs +4 -4
- package/src/atomic-mutation.mjs +1 -1
- package/src/baton.mjs +7 -7
- package/src/check-collapse.mjs +5 -5
- package/src/claude-commands.mjs +6 -2
- package/src/commands.mjs +4 -4
- package/src/config.mjs +2 -2
- package/src/deps.mjs +1 -1
- package/src/doctor.mjs +17 -17
- package/src/fix-membership.mjs +4 -2
- package/src/frontmatter-fix.mjs +1 -1
- package/src/git.mjs +1 -1
- package/src/glossary.mjs +3 -3
- package/src/graph.mjs +1 -1
- package/src/guard.mjs +8 -8
- package/src/health.mjs +2 -2
- package/src/hints.mjs +7 -7
- package/src/host-integration.mjs +41 -22
- package/src/hub-membership.mjs +1 -1
- package/src/hud.mjs +14 -14
- package/src/index-file.mjs +2 -2
- package/src/init.mjs +24 -24
- package/src/install.mjs +4 -4
- package/src/journal.mjs +40 -7
- package/src/lifecycle.mjs +17 -17
- package/src/lint.mjs +1 -1
- package/src/migrate-prompts.mjs +1 -1
- package/src/migrate-template.mjs +2 -2
- package/src/migrate.mjs +1 -1
- package/src/misuse-read.mjs +4 -5
- package/src/modules.mjs +3 -3
- package/src/new.mjs +11 -11
- package/src/output-identity.mjs +7 -2
- package/src/pickup-card.mjs +2 -2
- package/src/pickup.mjs +2 -2
- package/src/prompts.mjs +12 -12
- package/src/query.mjs +9 -9
- package/src/rename.mjs +2 -2
- package/src/render.mjs +20 -20
- package/src/roadmap.mjs +5 -5
- package/src/runlist.mjs +11 -11
- package/src/ship.mjs +2 -2
- package/src/skill-drift.mjs +19 -6
- package/src/statuses.mjs +16 -16
- package/src/summary.mjs +1 -1
- package/src/surfaces.mjs +1 -1
- package/src/sync-status.mjs +4 -4
- package/src/update.mjs +11 -11
- package/src/validate.mjs +9 -9
- package/src/watch.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotmd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.1",
|
|
4
4
|
"description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"homepage": "https://github.com/reowens/dotmd#readme",
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test": "node --test test/*.test.mjs",
|
|
44
|
+
"test": "node --import ./test/setup-env.mjs --test test/*.test.mjs",
|
|
45
45
|
"postinstall": "node scripts/postinstall.mjs",
|
|
46
46
|
"preversion": "node scripts/release-preflight.mjs && npm test",
|
|
47
47
|
"version": "node scripts/prepare-version-commit.mjs",
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -36,17 +36,17 @@ try {
|
|
|
36
36
|
|
|
37
37
|
if ((process.env.RUNLIST_AUTO_PLUGIN_UPDATE ?? process.env.DOTMD_AUTO_PLUGIN_UPDATE) === '1' && hasClaude) {
|
|
38
38
|
spawnSync('claude', ['plugin', 'update', 'dotmd@dotmd'], { stdio: 'ignore', timeout: 60000 });
|
|
39
|
-
process.stdout.write('
|
|
39
|
+
process.stdout.write('runlist: refreshed the Claude Code plugin — restart your session (or /reload-plugins) to apply.\n');
|
|
40
40
|
} else {
|
|
41
41
|
// The CLI just installed fresh, so only the plugin can be stale — point at
|
|
42
42
|
// the targeted refresh rather than the full `dotmd update` (CLI + plugin).
|
|
43
43
|
const nudge = hasClaude
|
|
44
|
-
? '
|
|
45
|
-
: '
|
|
44
|
+
? 'runlist CLI installed. Using the Claude Code plugin? Run `runlist update --plugin-only` to refresh it, then restart.'
|
|
45
|
+
: 'runlist CLI installed.';
|
|
46
46
|
process.stdout.write(`${nudge}\n`);
|
|
47
47
|
}
|
|
48
48
|
if (hasOpencode) {
|
|
49
|
-
process.stdout.write('
|
|
49
|
+
process.stdout.write('runlist: OpenCode detected — run `runlist install opencode` for per-session plan ownership and a session-start briefing.\n');
|
|
50
50
|
}
|
|
51
51
|
} catch {
|
|
52
52
|
// Best effort only — never break the install.
|
package/src/atomic-mutation.mjs
CHANGED
|
@@ -203,7 +203,7 @@ function transactionRepairMessage(manifestPath, manifest, reason) {
|
|
|
203
203
|
...(manifest.gitIndex?.retainedPaths ?? []),
|
|
204
204
|
].filter(Boolean);
|
|
205
205
|
return `${reason}\nTransaction recovery refused to guess. Manifest: ${manifestPath}\n` +
|
|
206
|
-
`Start with \`
|
|
206
|
+
`Start with \`runlist doctor --transactions\` — it reports this transaction's state and resolves it when the canonical files agree on one generation.\n` +
|
|
207
207
|
`If it reports the generations as ambiguous, inspect the canonical files and recovery artifacts, then restore one complete generation and remove the manifest:\n` +
|
|
208
208
|
`${artifacts.map(item => ` ${item}`).join('\n') || ' (no content artifacts)'}`;
|
|
209
209
|
}
|
package/src/baton.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export function findOwnedPlan(config, index = null) {
|
|
|
31
31
|
// next decision, and a prompt assembled from frontmatter reads like a handoff
|
|
32
32
|
// while carrying nothing the plan doesn't already say.
|
|
33
33
|
const BODY_USAGE = `Nothing saved: baton needs the resume you wrote, passed as @<file> or - (stdin).
|
|
34
|
-
|
|
34
|
+
runlist baton [<plan-or-slug>] @/tmp/draft.md`;
|
|
35
35
|
|
|
36
36
|
// A handoff that lands beside a pending one leaves two prompts for the same
|
|
37
37
|
// work, and the next session picks whichever sorts first. Baton used to step to
|
|
@@ -60,7 +60,7 @@ function pendingHandoffs(promptPath, planPath, config) {
|
|
|
60
60
|
function refusePendingHandoff(pending) {
|
|
61
61
|
const lines = pending.map(p => ` ${p}`).join('\n');
|
|
62
62
|
const slug = path.basename(pending[0], '.md');
|
|
63
|
-
die(`Nothing saved: a handoff for this is already pending:\n${lines}\nUse it (\`
|
|
63
|
+
die(`Nothing saved: a handoff for this is already pending:\n${lines}\nUse it (\`runlist use ${slug}\`) or archive it (\`runlist prompts archive ${pending[0]}\`), then run baton again.`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Is this positional a filesystem reference (must resolve, typos die) or a
|
|
@@ -87,7 +87,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
87
87
|
if (a === '--force') { force = true; continue; }
|
|
88
88
|
if (a === '--json') continue;
|
|
89
89
|
if (!a.startsWith('-') || a === '-' || a.startsWith('@')) { positionals.push(a); continue; }
|
|
90
|
-
die(`Unknown flag for \`
|
|
90
|
+
die(`Unknown flag for \`runlist baton\`: ${a}`);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
let planArg = null;
|
|
@@ -138,10 +138,10 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
138
138
|
if (owned.plan) {
|
|
139
139
|
planPath = path.resolve(config.repoRoot, owned.plan.path);
|
|
140
140
|
} else if (owned.owned?.length > 1) {
|
|
141
|
-
die(`Multiple plans are owned by this session; pass one explicitly:\n${owned.owned.map(d => '
|
|
141
|
+
die(`Multiple plans are owned by this session; pass one explicitly:\n${owned.owned.map(d => ' runlist baton ' + d.path + ' @/tmp/draft.md').join('\n')}`);
|
|
142
142
|
} else {
|
|
143
143
|
const diagnostics = owned.diagnostics?.length ? `\nIgnored ownership records:\n${owned.diagnostics.map(d => ` ${d}`).join('\n')}` : '';
|
|
144
|
-
die(`No valid in-session plan is owned by this session, so baton needs a name for the resume prompt:\n
|
|
144
|
+
die(`No valid in-session plan is owned by this session, so baton needs a name for the resume prompt:\n runlist baton <slug> @/tmp/draft.md # saves resume-<slug>, touches nothing else\nHanding off a specific plan? runlist baton <plan-file> @/tmp/draft.md${diagnostics}`);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -162,7 +162,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
162
162
|
const raw = readFileSync(planPath, 'utf8');
|
|
163
163
|
const { frontmatter: fmRaw } = extractFrontmatter(raw);
|
|
164
164
|
if (!fmRaw) {
|
|
165
|
-
die(`${repoPath} has no frontmatter block — baton can't flip its status.\nFix the doc first (\`
|
|
165
|
+
die(`${repoPath} has no frontmatter block — baton can't flip its status.\nFix the doc first (\`runlist bulk-tag ${repoPath} --type plan --status in-session\`), or save the prompt without a status flip: runlist baton ${path.basename(planPath, '.md')} @/tmp/draft.md`);
|
|
166
166
|
}
|
|
167
167
|
const fm = parseSimpleFrontmatter(fmRaw);
|
|
168
168
|
const docType = asString(fm.type);
|
|
@@ -176,7 +176,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
176
176
|
die(`Invalid status \`${status}\` for type \`${docType ?? 'plan'}\`\nValid: ${[...validStatuses].join(', ')}`);
|
|
177
177
|
}
|
|
178
178
|
if (status === 'in-session') {
|
|
179
|
-
die('`
|
|
179
|
+
die('`runlist baton --status in-session` contradicts baton release semantics. Choose active/paused/awaiting/partial/blocked.');
|
|
180
180
|
}
|
|
181
181
|
assertPlanMutationAuthorized(repoPath, config, { sessionId: authoritativeSessionId(), force });
|
|
182
182
|
// Before the plan-completion step: a refusal must leave nothing changed.
|
package/src/check-collapse.mjs
CHANGED
|
@@ -14,19 +14,19 @@ const CATEGORIES = [
|
|
|
14
14
|
key: 'updated-behind-git',
|
|
15
15
|
match: /^frontmatter `updated:.*` is behind git history/,
|
|
16
16
|
label: 'docs have `updated` behind git history',
|
|
17
|
-
fix: '
|
|
17
|
+
fix: 'runlist touch --git',
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
key: 'singular-module',
|
|
21
|
-
match: /^`module:` \(singular\) is deprecated.*Run `
|
|
21
|
+
match: /^`module:` \(singular\) is deprecated.*Run `runlist lint --fix`/,
|
|
22
22
|
label: 'docs use deprecated singular `module:`',
|
|
23
|
-
fix: '
|
|
23
|
+
fix: 'runlist lint --fix',
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
key: 'singular-surface',
|
|
27
|
-
match: /^`surface:` \(singular\) is deprecated.*Run `
|
|
27
|
+
match: /^`surface:` \(singular\) is deprecated.*Run `runlist lint --fix`/,
|
|
28
28
|
label: 'docs use deprecated singular `surface:`',
|
|
29
|
-
fix: '
|
|
29
|
+
fix: 'runlist lint --fix',
|
|
30
30
|
},
|
|
31
31
|
];
|
|
32
32
|
|
package/src/claude-commands.mjs
CHANGED
|
@@ -16,7 +16,10 @@ import path from 'node:path';
|
|
|
16
16
|
// NEVER touched. Every dotmd-stamped file is fair game, including legacy ones
|
|
17
17
|
// dotmd no longer generates (e.g. the old baton.md).
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Nothing writes these files any more, so only the legacy banner can exist in
|
|
20
|
+
// the wild; the current spelling is accepted too so a banner-gated teardown
|
|
21
|
+
// never depends on which name stamped the file.
|
|
22
|
+
const GENERATED_MARKERS = ['<!-- runlist-generated:', '<!-- dotmd-generated:'];
|
|
20
23
|
|
|
21
24
|
// The marker sits just below the YAML frontmatter Claude Code surfaces as the
|
|
22
25
|
// command description. That description can be long (the retired plans.md baked
|
|
@@ -25,7 +28,8 @@ const GENERATED_MARKER = '<!-- dotmd-generated:';
|
|
|
25
28
|
// are tiny command files, so reading them in full is cheap.
|
|
26
29
|
function isGeneratedCommandFile(filePath) {
|
|
27
30
|
try {
|
|
28
|
-
|
|
31
|
+
const contents = readFileSync(filePath, 'utf8');
|
|
32
|
+
return GENERATED_MARKERS.some(marker => contents.includes(marker));
|
|
29
33
|
} catch {
|
|
30
34
|
return false;
|
|
31
35
|
}
|
package/src/commands.mjs
CHANGED
|
@@ -244,7 +244,7 @@ export function commandCompletionWords(name) {
|
|
|
244
244
|
export function commandUsage(name) {
|
|
245
245
|
const definition = commandDefinition(name);
|
|
246
246
|
if (!definition) return null;
|
|
247
|
-
return definition.forms.map(commandForm => `
|
|
247
|
+
return definition.forms.map(commandForm => `runlist ${definition.name}${commandForm.syntax ? ` ${commandForm.syntax}` : ''}`).join('\n');
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
function optionMap(forms) {
|
|
@@ -267,12 +267,12 @@ function scanArgs(name, argv, options, passthrough = false, dashPositionalsAfter
|
|
|
267
267
|
if (!option) {
|
|
268
268
|
if (passthrough) continue;
|
|
269
269
|
if (positional.length >= dashPositionalsAfter) { positional.push(arg); continue; }
|
|
270
|
-
throw new Error(`Unknown flag for \`
|
|
270
|
+
throw new Error(`Unknown flag for \`runlist ${name}\`: ${arg}`);
|
|
271
271
|
}
|
|
272
272
|
if (option.arity === 1) {
|
|
273
273
|
const next = argv[i + 1];
|
|
274
274
|
if (next === undefined || options.has(next)) {
|
|
275
|
-
throw new Error(`Missing value for \`${arg}\` in \`
|
|
275
|
+
throw new Error(`Missing value for \`${arg}\` in \`runlist ${name}\`.`);
|
|
276
276
|
}
|
|
277
277
|
i += 1;
|
|
278
278
|
} else if (option.arity === 'optional' && argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) {
|
|
@@ -317,7 +317,7 @@ export function validateCommandArgs(name, argv, { preset = false } = {}) {
|
|
|
317
317
|
const preliminary = scanArgs(canonical, normalized, allOptions, allPassthrough, dashPositionalsAfter);
|
|
318
318
|
const selected = definition.forms.find(commandForm => commandForm.subcommands.includes(preliminary[0]))
|
|
319
319
|
?? definition.forms.find(commandForm => commandForm.subcommands.length === 0);
|
|
320
|
-
if (!selected) throw new Error(`Unknown subcommand for \`
|
|
320
|
+
if (!selected) throw new Error(`Unknown subcommand for \`runlist ${canonical}\`: ${preliminary[0] ?? '(missing)'}`);
|
|
321
321
|
const selectedOptions = optionMap([selected]);
|
|
322
322
|
const positional = scanArgs(canonical, normalized, selectedOptions, selected.passthrough, selected.dashPositionalsAfter);
|
|
323
323
|
const args = selected.subcommands.includes(positional[0]) ? positional.slice(1) : positional;
|
package/src/config.mjs
CHANGED
|
@@ -218,10 +218,10 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
218
218
|
const skipStaleEffective = p.skipStale === true || quietImpliesSkipStale;
|
|
219
219
|
const skipWarningsEffective = p.skipWarnings === true || quietImpliesSkipWarnings;
|
|
220
220
|
if (skipStaleEffective && p.staleDays != null) {
|
|
221
|
-
warn(`
|
|
221
|
+
warn(`runlist config: status "${typeName}.${name}" has skipStale: true and staleDays: ${p.staleDays} — staleDays is ignored. Drop one to silence this warning.`);
|
|
222
222
|
}
|
|
223
223
|
if (skipWarningsEffective && p.requiresModule) {
|
|
224
|
-
warn(`
|
|
224
|
+
warn(`runlist config: status "${typeName}.${name}" has skipWarnings: true and requiresModule: true — the module requirement can never fire. Drop one to silence this warning.`);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
|
package/src/deps.mjs
CHANGED
|
@@ -252,7 +252,7 @@ function renderFlatJson(graph, forwardMap, reverseMap, docByPath) {
|
|
|
252
252
|
export function runUnblocks(argv, config) {
|
|
253
253
|
const input = argv.find(a => !a.startsWith('-'));
|
|
254
254
|
const json = argv.includes('--json');
|
|
255
|
-
if (!input) die('Usage:
|
|
255
|
+
if (!input) die('Usage: runlist unblocks <file>');
|
|
256
256
|
|
|
257
257
|
const filePath = resolveDocArg(input, config);
|
|
258
258
|
const repoPath = toRepoPath(filePath, config.repoRoot);
|
package/src/doctor.mjs
CHANGED
|
@@ -99,7 +99,7 @@ function runDoctorTransactions(argv, config, opts = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
if (resolvable.length) {
|
|
101
101
|
process.stdout.write(`\n${resolvable.length} resolvable — the canonical files already agree on one generation.\n`);
|
|
102
|
-
process.stdout.write(dim('Run `
|
|
102
|
+
process.stdout.write(dim('Run `runlist doctor --transactions --apply` to clear them (no document content is touched).\n'));
|
|
103
103
|
}
|
|
104
104
|
const stuck = report.filter(item => !item.resolvable && !clearedIds.has(item.id) && (item.status === 'failed-manual' || !item.readable));
|
|
105
105
|
if (stuck.length) {
|
|
@@ -152,7 +152,7 @@ async function runDoctorClaims(argv, config, opts = {}) {
|
|
|
152
152
|
// status into. Deciding by existence here rather than by catching
|
|
153
153
|
// runSet's "File not found" keeps the bypass narrow and explicit.
|
|
154
154
|
if (existsSync(path.resolve(config.repoRoot, claim.plan))) {
|
|
155
|
-
await runSet(['active', claim.plan], config, { force: true, sessionId: operator, note: 'Claim released by `
|
|
155
|
+
await runSet(['active', claim.plan], config, { force: true, sessionId: operator, note: 'Claim released by `runlist doctor --claims` — the owning session was gone.' });
|
|
156
156
|
} else {
|
|
157
157
|
releaseVanishedPlanClaim(claim, config);
|
|
158
158
|
claim.vanished = true;
|
|
@@ -201,13 +201,13 @@ async function runDoctorClaims(argv, config, opts = {}) {
|
|
|
201
201
|
const pendingDead = dead.filter(claim => !releasedSet.has(claim.plan));
|
|
202
202
|
if (pendingDead.length) {
|
|
203
203
|
process.stdout.write(yellow(`\n${pendingDead.length} held by a session whose process is gone.\n`));
|
|
204
|
-
process.stdout.write(dim('Run `
|
|
204
|
+
process.stdout.write(dim('Run `runlist doctor --claims --apply` to release them.\n'));
|
|
205
205
|
}
|
|
206
206
|
const unjudgeable = claims.filter(claim =>
|
|
207
207
|
!claim.corrupt && claim.liveness !== 'dead' && !releasedSet.has(claim.plan));
|
|
208
208
|
if (unjudgeable.length && olderThanMs === null) {
|
|
209
209
|
process.stdout.write(dim(`\n${unjudgeable.length} cannot be judged from here — no owning process was recorded, or it is on another machine.\n`));
|
|
210
|
-
process.stdout.write(dim('If you know those sessions are over: `
|
|
210
|
+
process.stdout.write(dim('If you know those sessions are over: `runlist doctor --claims --apply --older-than 24h`.\n'));
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -228,10 +228,10 @@ function runDoctorSession(argv) {
|
|
|
228
228
|
for (const line of identity.advice) process.stdout.write(` → ${line}\n`);
|
|
229
229
|
|
|
230
230
|
process.stdout.write('\n' + bold('Host integration\n'));
|
|
231
|
-
if (oc.foreign) process.stdout.write(` ${yellow('!')} opencode: a dotmd.js
|
|
231
|
+
if (oc.foreign) process.stdout.write(` ${yellow('!')} opencode: a dotmd.js runlist did not write — ${oc.path}\n`);
|
|
232
232
|
else if (!oc.exists) process.stdout.write(` ${dim('·')} opencode: not installed — ${dim(oc.path)}\n`);
|
|
233
|
-
else process.stdout.write(` ${oc.stale ? yellow('!') : green('✓')} opencode: ${oc.version}${oc.stale ? ` (CLI is ${dotmdVersion()} — run \`
|
|
234
|
-
process.stdout.write(dim(' Claude Code ships as a plugin — `
|
|
233
|
+
else process.stdout.write(` ${oc.stale ? yellow('!') : green('✓')} opencode: ${oc.version}${oc.stale ? ` (CLI is ${dotmdVersion()} — run \`runlist update\`)` : ''}\n`);
|
|
234
|
+
process.stdout.write(dim(' Claude Code ships as a plugin — `runlist install` reports both hosts.\n'));
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
export function runDoctor(argv, config, opts = {}) {
|
|
@@ -273,7 +273,7 @@ export function runDoctor(argv, config, opts = {}) {
|
|
|
273
273
|
const modeNote = dryRun
|
|
274
274
|
? dim('[preview — run with --apply to write]')
|
|
275
275
|
: dim('[applying changes]');
|
|
276
|
-
process.stdout.write(bold('
|
|
276
|
+
process.stdout.write(bold('runlist doctor') + ' ' + modeNote + '\n\n');
|
|
277
277
|
if (dryRun) {
|
|
278
278
|
const skippedHooks = ['validate', 'transformDoc', 'formatSnapshot', 'renderCheck']
|
|
279
279
|
.filter(name => typeof config.hooks?.[name] === 'function');
|
|
@@ -335,7 +335,7 @@ export function runDoctor(argv, config, opts = {}) {
|
|
|
335
335
|
process.stdout.write('[dry-run] No retired slash-command files to remove.\n');
|
|
336
336
|
} else {
|
|
337
337
|
for (const r of wouldRemove) {
|
|
338
|
-
process.stdout.write(`[dry-run] Would remove retired .claude/commands/${r.name} (guidance now ships via the
|
|
338
|
+
process.stdout.write(`[dry-run] Would remove retired .claude/commands/${r.name} (guidance now ships via the runlist plugin).\n`);
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
} else {
|
|
@@ -344,7 +344,7 @@ export function runDoctor(argv, config, opts = {}) {
|
|
|
344
344
|
process.stdout.write('Nothing to clean up.\n');
|
|
345
345
|
} else {
|
|
346
346
|
for (const r of removed) {
|
|
347
|
-
process.stdout.write(`${green('Removed')} retired .claude/commands/${r.name} (guidance now ships via the
|
|
347
|
+
process.stdout.write(`${green('Removed')} retired .claude/commands/${r.name} (guidance now ships via the runlist plugin)\n`);
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
}
|
|
@@ -369,7 +369,7 @@ export function runDoctor(argv, config, opts = {}) {
|
|
|
369
369
|
process.stdout.write('\n' + bold('Session identity') + '\n');
|
|
370
370
|
process.stdout.write(`${yellow('!')} ${identity.summary}\n`);
|
|
371
371
|
for (const line of identity.advice) process.stdout.write(dim(` → ${line}\n`));
|
|
372
|
-
process.stdout.write(dim(' `
|
|
372
|
+
process.stdout.write(dim(' `runlist doctor --session` for the full picture.\n'));
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -387,7 +387,7 @@ function findDeprecatedCommandMentions(config) {
|
|
|
387
387
|
for (const filePath of docs) {
|
|
388
388
|
let raw = '';
|
|
389
389
|
try { raw = readFileSync(filePath, 'utf8'); } catch { continue; }
|
|
390
|
-
if (/\
|
|
390
|
+
if (/\b(?:dotmd|runlist|rl) status\b/.test(raw) || /\b(?:dotmd|runlist|rl) (pickup|unpickup|release|finish)\b/.test(raw)) {
|
|
391
391
|
matches.push(toRepoPath(filePath, config.repoRoot));
|
|
392
392
|
}
|
|
393
393
|
}
|
|
@@ -445,7 +445,7 @@ function runDoctorProject(config, { json = false } = {}) {
|
|
|
445
445
|
return result;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
process.stdout.write(bold('
|
|
448
|
+
process.stdout.write(bold('runlist doctor --project') + '\n\n');
|
|
449
449
|
process.stdout.write(`- running CLI version: ${result.cliVersion ?? 'unknown'}\n`);
|
|
450
450
|
process.stdout.write(`- package dependency: ${result.packageDependency ?? '(none found)'}\n`);
|
|
451
451
|
process.stdout.write(`- stale Claude commands: ${claudeCommandWarnings.length}\n`);
|
|
@@ -456,18 +456,18 @@ function runDoctorProject(config, { json = false } = {}) {
|
|
|
456
456
|
process.stdout.write('- docs mentioning deprecated commands: 0\n');
|
|
457
457
|
}
|
|
458
458
|
if (docsWithoutFrontmatter.length) {
|
|
459
|
-
process.stdout.write(yellow(`- docs without a frontmatter block: ${docsWithoutFrontmatter.length} — every status verb (\`set\`, \`archive\`, \`baton\`) dies on these. Fix:
|
|
459
|
+
process.stdout.write(yellow(`- docs without a frontmatter block: ${docsWithoutFrontmatter.length} — every status verb (\`set\`, \`archive\`, \`baton\`) dies on these. Fix: runlist bulk-tag <file> --type <type> --status <status>`) + '\n');
|
|
460
460
|
for (const file of docsWithoutFrontmatter.slice(0, 10)) process.stdout.write(` - ${file}\n`);
|
|
461
461
|
} else {
|
|
462
462
|
process.stdout.write('- docs without a frontmatter block: 0\n');
|
|
463
463
|
}
|
|
464
464
|
if (planStatusGaps.length) {
|
|
465
|
-
process.stdout.write(yellow(`- plan status vocab missing: ${planStatusGaps.join(', ')} — \`
|
|
465
|
+
process.stdout.write(yellow(`- plan status vocab missing: ${planStatusGaps.join(', ')} — \`runlist use\` and \`runlist baton\` depend on these; add them to types.plan.statuses in runlist.config.mjs`) + '\n');
|
|
466
466
|
} else {
|
|
467
467
|
process.stdout.write('- plan status vocab: ok\n');
|
|
468
468
|
}
|
|
469
469
|
if (skillDriftWarnings.length) {
|
|
470
|
-
process.stdout.write(yellow(`- canonical workflow block: drifted — CLAUDE.md and the plugin SKILL.md teach different workflows. Reconcile the block between the \`
|
|
470
|
+
process.stdout.write(yellow(`- canonical workflow block: drifted — CLAUDE.md and the plugin SKILL.md teach different workflows. Reconcile the block between the \`runlist:canonical-workflow\` markers in both files.`) + '\n');
|
|
471
471
|
} else {
|
|
472
472
|
process.stdout.write('- canonical workflow block: in sync\n');
|
|
473
473
|
}
|
|
@@ -559,7 +559,7 @@ function runDoctorStatuses(config, { json = false } = {}) {
|
|
|
559
559
|
return;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
process.stdout.write(bold('
|
|
562
|
+
process.stdout.write(bold('runlist doctor --statuses') + '\n\n');
|
|
563
563
|
|
|
564
564
|
if (suggestions.length === 0) {
|
|
565
565
|
process.stdout.write(`No overloaded status buckets detected (min bucket size: ${MIN_BUCKET_SIZE}).\n`);
|
package/src/fix-membership.mjs
CHANGED
|
@@ -21,7 +21,7 @@ function resolveHubArgs(args, docs) {
|
|
|
21
21
|
const [match] = matches;
|
|
22
22
|
if (!isHubDoc(match)) {
|
|
23
23
|
die(`${match.path} is not a hub — it has no \`runlist:\` and no \`execution_mode: coordination|roadmap\`. `
|
|
24
|
-
+ 'Run `
|
|
24
|
+
+ 'Run `runlist fix-membership` with no argument to sweep every hub.');
|
|
25
25
|
}
|
|
26
26
|
paths.add(match.path);
|
|
27
27
|
}
|
|
@@ -73,7 +73,9 @@ export function fixMembershipBackrefs(config, {
|
|
|
73
73
|
const ambiguous = planned.ambiguous.map(item => ({ path: item.childPath, hubs: item.hubPaths }));
|
|
74
74
|
const managed = [
|
|
75
75
|
...updates.map(update => update.path),
|
|
76
|
-
|
|
76
|
+
// Array.from, not `.values().map`: iterator helpers are Node 22+, and the
|
|
77
|
+
// package supports Node 20.
|
|
78
|
+
...Array.from(guardByPath.values(), guard => guard.path),
|
|
77
79
|
];
|
|
78
80
|
if (managed.length) authorizeManagedSweep(managed, config, { kind: 'Membership repair source' });
|
|
79
81
|
|
package/src/frontmatter-fix.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export function runFrontmatterFix(config, opts = {}) {
|
|
|
61
61
|
|
|
62
62
|
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
63
63
|
const banner = dryRun ? dim(' [preview — run without --dry-run to write]') : '';
|
|
64
|
-
out.write(bold('
|
|
64
|
+
out.write(bold('runlist doctor --frontmatter-fix') + banner + '\n\n');
|
|
65
65
|
|
|
66
66
|
if (results.length === 0) {
|
|
67
67
|
out.write(green('No over-cap fields found.') + '\n');
|
package/src/git.mjs
CHANGED
|
@@ -36,7 +36,7 @@ function ensureGit() {
|
|
|
36
36
|
if (gitChecked) return;
|
|
37
37
|
const result = spawnSync('git', ['--version'], { encoding: 'utf8' });
|
|
38
38
|
if (result.error) {
|
|
39
|
-
throw new Error('git is not installed or not found in PATH.
|
|
39
|
+
throw new Error('git is not installed or not found in PATH. runlist requires git for this operation.');
|
|
40
40
|
}
|
|
41
41
|
gitChecked = true;
|
|
42
42
|
}
|
package/src/glossary.mjs
CHANGED
|
@@ -183,9 +183,9 @@ export function runGlossary(argv, config) {
|
|
|
183
183
|
const term = argv.find(a => !a.startsWith('-'));
|
|
184
184
|
|
|
185
185
|
const result = loadGlossary(config);
|
|
186
|
-
if (!result) die('No glossary configured. Add glossary: { path, section } to your
|
|
186
|
+
if (!result) die('No glossary configured. Add glossary: { path, section } to your runlist config.');
|
|
187
187
|
if (!result.found) {
|
|
188
|
-
die(`Glossary section "## ${result.section}" not found in ${result.path}. Add the section, or update glossary.section in
|
|
188
|
+
die(`Glossary section "## ${result.section}" not found in ${result.path}. Add the section, or update glossary.section in runlist.config.mjs.`);
|
|
189
189
|
}
|
|
190
190
|
if (result.entries.length === 0) {
|
|
191
191
|
die(`Glossary section "## ${result.section}" found in ${result.path} but contains no recognizable entries (expected markdown table or schema→UI bullets).`);
|
|
@@ -212,7 +212,7 @@ export function runGlossary(argv, config) {
|
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
if (!term) die('Usage:
|
|
215
|
+
if (!term) die('Usage: runlist glossary <term> | --list | --json');
|
|
216
216
|
|
|
217
217
|
const matches = matchTerm(term, entries);
|
|
218
218
|
|
package/src/graph.mjs
CHANGED
|
@@ -208,7 +208,7 @@ function _renderGraphText(graph, config) {
|
|
|
208
208
|
export function renderGraphDot(graph, config) {
|
|
209
209
|
const { nodes, edges } = graph;
|
|
210
210
|
const lines = [];
|
|
211
|
-
lines.push('digraph
|
|
211
|
+
lines.push('digraph runlist {');
|
|
212
212
|
lines.push(' rankdir=LR;');
|
|
213
213
|
lines.push(' node [shape=box, style="rounded,filled", fontname="Helvetica"];');
|
|
214
214
|
lines.push('');
|
package/src/guard.mjs
CHANGED
|
@@ -269,9 +269,9 @@ function evalBash(command, config, inspectGitPaths, baseCwd) {
|
|
|
269
269
|
rule: 'commit-prompt',
|
|
270
270
|
detail: `git ${git.subcommand} ${targets.join(' ')}`,
|
|
271
271
|
reason:
|
|
272
|
-
`Saved prompts (${targets.join(', ')}) are session-local
|
|
272
|
+
`Saved prompts (${targets.join(', ')}) are session-local runlist artifacts, not source to commit. ` +
|
|
273
273
|
`Don't git add/commit them — commit your other changes without the prompt in the pathspec. ` +
|
|
274
|
-
`The next session consumes a prompt with \`
|
|
274
|
+
`The next session consumes a prompt with \`runlist use <file>\` (or \`runlist use\` for the oldest pending), which prints the body and archives it atomically.`,
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -283,9 +283,9 @@ function evalBash(command, config, inspectGitPaths, baseCwd) {
|
|
|
283
283
|
rule: 'cat-prompt',
|
|
284
284
|
detail: `${cmd0} ${promptTokens.join(' ')}`,
|
|
285
285
|
reason:
|
|
286
|
-
`${promptTokens.join(', ')} is a saved
|
|
286
|
+
`${promptTokens.join(', ')} is a saved runlist prompt. To start work from it, run \`runlist use ${promptTokens[0]}\` — ` +
|
|
287
287
|
`it commits archive/claim before at-most-once body output (prevents double-consumption). ` +
|
|
288
|
-
`Just peeking or triaging (not consuming)? \`
|
|
288
|
+
`Just peeking or triaging (not consuming)? \`runlist prompts show ${promptTokens[0]}\` reads it without archiving. Don't \`${cmd0}\` it directly.`,
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
|
|
@@ -308,9 +308,9 @@ function evalRead(filePath, config) {
|
|
|
308
308
|
rule: 'read-prompt',
|
|
309
309
|
detail: filePath,
|
|
310
310
|
reason:
|
|
311
|
-
`${filePath} is a saved
|
|
312
|
-
`Just peeking or triaging (not consuming)? \`
|
|
313
|
-
`Surveying the whole queue? \`
|
|
311
|
+
`${filePath} is a saved runlist prompt. To start work from it, run \`runlist use ${filePath}\` — it commits archive/claim before at-most-once body output so it can't be double-consumed. ` +
|
|
312
|
+
`Just peeking or triaging (not consuming)? \`runlist prompts show ${filePath}\` reads it without archiving. ` +
|
|
313
|
+
`Surveying the whole queue? \`runlist prompts show --all\` peeks every pending prompt in one call — don't Read them file by file.`,
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
316
|
|
|
@@ -396,7 +396,7 @@ function emit(result) {
|
|
|
396
396
|
hookSpecificOutput.permissionDecisionReason = result.reason;
|
|
397
397
|
} else {
|
|
398
398
|
// warn — allow the call but teach the agent the dotmd-native path.
|
|
399
|
-
hookSpecificOutput.additionalContext = `[
|
|
399
|
+
hookSpecificOutput.additionalContext = `[runlist] ${result.reason}`;
|
|
400
400
|
}
|
|
401
401
|
process.stdout.write(JSON.stringify({ hookSpecificOutput }) + '\n');
|
|
402
402
|
}
|
package/src/health.mjs
CHANGED
|
@@ -123,7 +123,7 @@ export function runHealth(argv, config) {
|
|
|
123
123
|
|
|
124
124
|
// Roadmaps (tier-3) — pinned above Runlists with the recursive grand total.
|
|
125
125
|
if (roadmapHubs.length > 0) {
|
|
126
|
-
process.stdout.write(`${bold('Roadmaps:')} ${roadmapHubs.length} ${dim('·
|
|
126
|
+
process.stdout.write(`${bold('Roadmaps:')} ${roadmapHubs.length} ${dim('· runlist roadmap')}\n`);
|
|
127
127
|
for (const doc of roadmapHubs.slice(0, 8)) {
|
|
128
128
|
const slug = hubLabel(doc).padEnd(28);
|
|
129
129
|
const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : '?d';
|
|
@@ -142,7 +142,7 @@ export function runHealth(argv, config) {
|
|
|
142
142
|
// surfaced as their own tally so they don't inflate the active count. Newest
|
|
143
143
|
// first, mirroring `dotmd runlists`; capped with a "more" footer.
|
|
144
144
|
if (runlistHubs.length > 0) {
|
|
145
|
-
process.stdout.write(`${bold('Runlists:')} ${runlistHubs.length} ${dim('·
|
|
145
|
+
process.stdout.write(`${bold('Runlists:')} ${runlistHubs.length} ${dim('· runlist runlists')}\n`);
|
|
146
146
|
for (const doc of runlistHubs.slice(0, 8)) {
|
|
147
147
|
const slug = hubLabel(doc).padEnd(28);
|
|
148
148
|
const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : '?d';
|
package/src/hints.mjs
CHANGED
|
@@ -17,32 +17,32 @@ const TEMPLATES = [
|
|
|
17
17
|
{
|
|
18
18
|
match: /Too many arguments|Usage:/i,
|
|
19
19
|
hint: ({ count, argv }) =>
|
|
20
|
-
`${count}× the same shape on \`${argv[0]} ${argv[1] ?? ''}\` in this session. Run \`
|
|
20
|
+
`${count}× the same shape on \`${argv[0]} ${argv[1] ?? ''}\` in this session. Run \`runlist ${argv[0]} --help\` for the expected positional args.`,
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
match: /Already (consumed|archived)/i,
|
|
24
24
|
hint: ({ count, argv }) =>
|
|
25
|
-
`${count}× attempts to use a path that is already archived. Use \`
|
|
25
|
+
`${count}× attempts to use a path that is already archived. Use \`runlist prompts list\` to see what is actually pending, or \`runlist next\` for the oldest live prompt.`,
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
match: /No pending prompts/i,
|
|
29
29
|
hint: ({ count }) =>
|
|
30
|
-
`${count}× \`
|
|
30
|
+
`${count}× \`runlist next\` with no pending prompts in the queue. Either queue one with \`runlist new prompt <slug> "..."\` or pass an explicit prompt file to \`runlist use\`.`,
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
match: /Unknown command/i,
|
|
34
34
|
hint: ({ count }) =>
|
|
35
|
-
`${count}× the same unknown command. Run \`
|
|
35
|
+
`${count}× the same unknown command. Run \`runlist --help\` to list available commands; the dispatch already prints a did-you-mean for close misses.`,
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
match: /File not found|does not resolve/i,
|
|
39
39
|
hint: ({ count, argv }) =>
|
|
40
|
-
`${count}× pointing at a path that doesn't exist. Confirm the file with \`
|
|
40
|
+
`${count}× pointing at a path that doesn't exist. Confirm the file with \`runlist query\` or \`runlist plans\` — paths resolve relative to repo root or doc roots, not the cwd.`,
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
match: /Unknown status|Unknown surface/i,
|
|
44
44
|
hint: ({ count }) =>
|
|
45
|
-
`${count}× rejected by the taxonomy validator. Run \`
|
|
45
|
+
`${count}× rejected by the taxonomy validator. Run \`runlist statuses list\` or \`runlist surfaces\` to print the valid values for this project.`,
|
|
46
46
|
},
|
|
47
47
|
];
|
|
48
48
|
|
|
@@ -122,7 +122,7 @@ export function findRepeatFailureHint(failingArgv, config) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
return `${count}× the same failing shape on \`${failingArgv[0]}\` in this session (last attempt ${ageMin}m ago). Check the args — \`
|
|
125
|
+
return `${count}× the same failing shape on \`${failingArgv[0]}\` in this session (last attempt ${ageMin}m ago). Check the args — \`runlist ${failingArgv[0]} --help\` shows what's expected.`;
|
|
126
126
|
} catch {
|
|
127
127
|
return null;
|
|
128
128
|
}
|