dotmd-cli 0.77.3 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -60
- package/assets/opencode/plugin.js +45 -8
- package/bin/dotmd.mjs +274 -283
- package/package.json +2 -2
- package/scripts/postinstall.mjs +4 -4
- package/src/atomic-mutation.mjs +1 -1
- package/src/baton.mjs +86 -50
- package/src/check-collapse.mjs +5 -5
- package/src/claude-commands.mjs +6 -2
- package/src/commands.mjs +8 -8
- package/src/config.mjs +2 -2
- package/src/deps.mjs +1 -1
- package/src/doctor.mjs +17 -17
- package/src/fix-membership.mjs +1 -1
- 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 +13 -10
- 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 +15 -12
- package/src/output-identity.mjs +7 -2
- package/src/pickup-card.mjs +2 -2
- package/src/pickup.mjs +2 -2
- package/src/prompts.mjs +18 -15
- 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/use.mjs +6 -2
- package/src/validate.mjs +9 -9
- package/src/watch.mjs +1 -1
package/src/lifecycle.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
|
|
36
36
|
export function renderLifecycleMutation(raw, updates, historyEntry, { createSection = false, bodyTransform = null } = {}) {
|
|
37
37
|
raw = normalizeEol(raw);
|
|
38
|
-
if (!raw.startsWith('---\n')) throw new Error('Document has no frontmatter block. Retrofit it with `
|
|
38
|
+
if (!raw.startsWith('---\n')) throw new Error('Document has no frontmatter block. Retrofit it with `runlist bulk-tag` first.');
|
|
39
39
|
const endMarker = raw.indexOf('\n---\n', 4);
|
|
40
40
|
if (endMarker === -1) throw new Error('Document has an unclosed frontmatter block.');
|
|
41
41
|
let frontmatter = raw.slice(4, endMarker);
|
|
@@ -192,7 +192,7 @@ export function regenIndex(config, options = {}) {
|
|
|
192
192
|
return true;
|
|
193
193
|
} catch (err) {
|
|
194
194
|
if (options.throwOnError) throw err;
|
|
195
|
-
warn(`Could not regenerate index (run \`
|
|
195
|
+
warn(`Could not regenerate index (run \`runlist index\`): ${err.message}`);
|
|
196
196
|
return false;
|
|
197
197
|
}
|
|
198
198
|
}
|
|
@@ -360,10 +360,10 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
360
360
|
let newStatus = argv[1];
|
|
361
361
|
|
|
362
362
|
if (!opts.suppressDeprecation) {
|
|
363
|
-
process.stderr.write(dim('`
|
|
363
|
+
process.stderr.write(dim('`runlist status <file> <status>` is deprecated; prefer `runlist set <status> [<file>]` (note: <status> first; <file> optional when a plan is in-session). Removed in a future major.\n'));
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
if (!input) { die('Usage:
|
|
366
|
+
if (!input) { die('Usage: runlist status <file> <new-status>'); }
|
|
367
367
|
|
|
368
368
|
let filePath = resolveDocArg(input, config);
|
|
369
369
|
const sourceAuthorization = authorizeManagedSource(filePath, config, { kind: 'Status source' });
|
|
@@ -394,7 +394,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
394
394
|
newStatus = await promptChoice('Which status?', effectiveOrder);
|
|
395
395
|
if (!newStatus) die('No status selected.');
|
|
396
396
|
} else {
|
|
397
|
-
die('Usage:
|
|
397
|
+
die('Usage: runlist status <file> <new-status>');
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
@@ -526,7 +526,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
526
526
|
// this so concurrent agents can do path-limited commits without pulling
|
|
527
527
|
// each other's uncommitted index changes into the staging area.
|
|
528
528
|
if (noIndex) {
|
|
529
|
-
process.stderr.write(dim('(index not regenerated — run `
|
|
529
|
+
process.stderr.write(dim('(index not regenerated — run `runlist index` to refresh)\n'));
|
|
530
530
|
} else if (!opts.deferIndex) {
|
|
531
531
|
regenIndex(config);
|
|
532
532
|
}
|
|
@@ -568,7 +568,7 @@ export async function startPlan(argv, config, opts = {}) {
|
|
|
568
568
|
|
|
569
569
|
// Interactive: pick from active/planned plans
|
|
570
570
|
if (!input) {
|
|
571
|
-
if (!isInteractive()) die('Usage:
|
|
571
|
+
if (!isInteractive()) die('Usage: runlist use <plan>');
|
|
572
572
|
const index = buildIndex(config);
|
|
573
573
|
const sessionId = authoritativeSessionId();
|
|
574
574
|
const candidates = pickupCandidates(index, config, sessionId);
|
|
@@ -672,7 +672,7 @@ export async function startPlan(argv, config, opts = {}) {
|
|
|
672
672
|
} else {
|
|
673
673
|
process.stderr.write(`${green('▶ Started')}: ${repoPath} (${oldStatus ?? 'unset'} → in-session)\n\n`);
|
|
674
674
|
if (fullBody) {
|
|
675
|
-
const header = `[
|
|
675
|
+
const header = `[runlist] in-session: ${repoPath} — close with: runlist set <status> ${repoPath}\n---\n`;
|
|
676
676
|
process.stdout.write(header);
|
|
677
677
|
const content = (body ?? '').trim();
|
|
678
678
|
if (content) process.stdout.write(content + '\n');
|
|
@@ -707,7 +707,7 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
707
707
|
}
|
|
708
708
|
const input = argv[0];
|
|
709
709
|
|
|
710
|
-
if (!input) { die('Usage:
|
|
710
|
+
if (!input) { die('Usage: runlist archive <file>'); }
|
|
711
711
|
|
|
712
712
|
let filePath = resolveDocArg(input, config);
|
|
713
713
|
const sourceAuthorization = authorizeManagedSource(filePath, config, { kind: 'Archive source' });
|
|
@@ -861,7 +861,7 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
861
861
|
if (selfRefsFixed) out.write('Updated references in archived file.\n');
|
|
862
862
|
if (updatedRefCount > 0) out.write(`Updated references in ${updatedRefCount} file(s).\n`);
|
|
863
863
|
if (config.indexPath && indexRegenerated) out.write('Index regenerated.\n');
|
|
864
|
-
if (config.indexPath && noIndex) out.write(dim('(index not regenerated — run `
|
|
864
|
+
if (config.indexPath && noIndex) out.write(dim('(index not regenerated — run `runlist index` to refresh)\n'));
|
|
865
865
|
|
|
866
866
|
const touched = [oldRepoPath, newRepoPath, ...refTouchedPaths];
|
|
867
867
|
if (config.indexPath && indexRegenerated) touched.push(config.indexPath);
|
|
@@ -931,7 +931,7 @@ export async function runSet(argv, config, opts = {}) {
|
|
|
931
931
|
const newStatus = argv[0];
|
|
932
932
|
let input = argv[1];
|
|
933
933
|
|
|
934
|
-
if (!newStatus) die('Usage:
|
|
934
|
+
if (!newStatus) die('Usage: runlist set <status> [<path>]');
|
|
935
935
|
let sessionId = null;
|
|
936
936
|
if (!input) {
|
|
937
937
|
sessionId = authoritativeSessionId();
|
|
@@ -1051,7 +1051,7 @@ export async function runSet(argv, config, opts = {}) {
|
|
|
1051
1051
|
// `@<draft-file>` stays an obvious metavariable: the old `@draft` read as
|
|
1052
1052
|
// a runnable path and got copied verbatim into failing batons. If it is
|
|
1053
1053
|
// copied anyway, readBodyInput recognizes the <…> form and says so.
|
|
1054
|
-
warn(`wrapping up? leave a baton so the next session picks up cleanly — write your resume notes to a file, then \`
|
|
1054
|
+
warn(`wrapping up? leave a baton so the next session picks up cleanly — write your resume notes to a file, then \`runlist baton ${path.basename(filePath, '.md')} @<draft-file>\` saves a resume prompt (no copy-paste into chat).`);
|
|
1055
1055
|
}
|
|
1056
1056
|
}
|
|
1057
1057
|
return result;
|
|
@@ -1063,7 +1063,7 @@ export function runBulkArchive(argv, config, opts = {}) {
|
|
|
1063
1063
|
const noIndex = argv.includes('--no-index') || opts.noIndex;
|
|
1064
1064
|
const showFiles = argv.includes('--show-files') || opts.showFiles;
|
|
1065
1065
|
const inputs = argv.filter(a => !a.startsWith('-'));
|
|
1066
|
-
if (inputs.length === 0) die('Usage:
|
|
1066
|
+
if (inputs.length === 0) die('Usage: runlist bulk archive <file1> <file2> ... or <glob>');
|
|
1067
1067
|
|
|
1068
1068
|
const allFiles = collectDocFiles(config);
|
|
1069
1069
|
const matched = [];
|
|
@@ -1124,7 +1124,7 @@ export function runBulkArchive(argv, config, opts = {}) {
|
|
|
1124
1124
|
catch (err) { indexError = err.message; }
|
|
1125
1125
|
if (config.indexPath && indexRegenerated) process.stdout.write('Index regenerated.\n');
|
|
1126
1126
|
} else if (config.indexPath) {
|
|
1127
|
-
process.stdout.write(dim('(index not regenerated — run `
|
|
1127
|
+
process.stdout.write(dim('(index not regenerated — run `runlist index` to refresh)\n'));
|
|
1128
1128
|
}
|
|
1129
1129
|
if (showFiles) {
|
|
1130
1130
|
const all = [...bulkTouched];
|
|
@@ -1248,8 +1248,8 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
1248
1248
|
return;
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
|
-
if (inputs.length > 1) die('Multiple files require `
|
|
1252
|
-
if (!input) { die('Usage:
|
|
1251
|
+
if (inputs.length > 1) die('Multiple files require `runlist touch --git <file...>`.');
|
|
1252
|
+
if (!input) { die('Usage: runlist touch <file>\n runlist touch --git Bulk-sync dates from git history'); }
|
|
1253
1253
|
|
|
1254
1254
|
let filePath = resolveDocArg(input, config);
|
|
1255
1255
|
filePath = authorizeManagedSource(filePath, config, { kind: 'Touch source' }).path;
|
|
@@ -1358,7 +1358,7 @@ export function updateFrontmatter(filePath, updates) {
|
|
|
1358
1358
|
// Name the remedy in the error: this is where every status verb lands when a
|
|
1359
1359
|
// doc was created outside dotmd, and "no frontmatter block" alone left
|
|
1360
1360
|
// sessions retrying other verbs instead of fixing the doc.
|
|
1361
|
-
if (!raw.startsWith('---\n')) throw new Error(`${filePath} has no frontmatter block. Retrofit it first:
|
|
1361
|
+
if (!raw.startsWith('---\n')) throw new Error(`${filePath} has no frontmatter block. Retrofit it first: runlist bulk-tag ${filePath} --type <type> --status <status>`);
|
|
1362
1362
|
|
|
1363
1363
|
const endMarker = raw.indexOf('\n---\n', 4);
|
|
1364
1364
|
if (endMarker === -1) throw new Error(`${filePath} has unclosed frontmatter block.`);
|
package/src/lint.mjs
CHANGED
|
@@ -218,7 +218,7 @@ export function runLint(argv, config, opts = {}) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
process.stdout.write(`\nRun ${bold('
|
|
221
|
+
process.stdout.write(`\nRun ${bold('runlist lint --fix')} to auto-fix.\n`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
if (nonFixable.length > 0) {
|
package/src/migrate-prompts.mjs
CHANGED
|
@@ -167,5 +167,5 @@ export function runMigratePrompts(argv, config, opts = {}) {
|
|
|
167
167
|
process.stdout.write(dim(` [${c.kind}] ${c.detail}\n`));
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
if (dryRun) process.stdout.write(`\nRun ${bold('
|
|
170
|
+
if (dryRun) process.stdout.write(`\nRun ${bold('runlist doctor --migrate-prompts')} without --dry-run to apply.\n`);
|
|
171
171
|
}
|
package/src/migrate-template.mjs
CHANGED
|
@@ -163,8 +163,8 @@ export function runMigrateTemplate(argv, config, opts = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
if (dryRun) {
|
|
166
|
-
process.stdout.write(`\nRun ${bold('
|
|
166
|
+
process.stdout.write(`\nRun ${bold('runlist doctor --migrate-template')} without --dry-run to apply.\n`);
|
|
167
167
|
} else {
|
|
168
|
-
process.stdout.write(`\n${green('Done.')} Re-run ${bold('
|
|
168
|
+
process.stdout.write(`\n${green('Done.')} Re-run ${bold('runlist check')} to see remaining issues.\n`);
|
|
169
169
|
}
|
|
170
170
|
}
|
package/src/migrate.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export function runMigrate(argv, config, opts = {}) {
|
|
|
23
23
|
const fileArgs = positional.slice(3);
|
|
24
24
|
|
|
25
25
|
if (!field || !oldValue || !newValue) {
|
|
26
|
-
die('Usage:
|
|
26
|
+
die('Usage: runlist migrate <field> <old-value> <new-value> [files...]');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const allFiles = collectDocFiles(config);
|
package/src/misuse-read.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readMisuseEntries,
|
|
2
|
+
import { readMisuseEntries, globalMisuseLogPaths } from './journal.mjs';
|
|
3
3
|
import { dim, red, yellow } from './color.mjs';
|
|
4
4
|
|
|
5
5
|
// `dotmd misuse` — read the cross-repo guard log. Every wrong-move the
|
|
@@ -19,11 +19,10 @@ function parseArgs(argv) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function runMisuse(argv, _config) {
|
|
22
|
-
|
|
23
|
-
if (!existsSync(file)) {
|
|
22
|
+
if (!globalMisuseLogPaths().some(file => existsSync(file))) {
|
|
24
23
|
process.stderr.write(
|
|
25
|
-
'No misuse log yet. The PreToolUse guard (`
|
|
26
|
-
'Wire it up: add a PreToolUse hook that runs `
|
|
24
|
+
'No misuse log yet. The PreToolUse guard (`runlist guard`) writes here when it intercepts a wrong move.\n' +
|
|
25
|
+
'Wire it up: add a PreToolUse hook that runs `runlist guard` (see `runlist help guard`).\n',
|
|
27
26
|
);
|
|
28
27
|
return;
|
|
29
28
|
}
|
package/src/modules.mjs
CHANGED
|
@@ -268,7 +268,7 @@ export function runModulesDashboard(index, argv, config) {
|
|
|
268
268
|
|
|
269
269
|
if (!flags.all && rows.length > flags.limit) {
|
|
270
270
|
const hidden = rows.length - flags.limit;
|
|
271
|
-
process.stdout.write('\n' + dim(` ${hidden} more module${hidden === 1 ? '' : 's'} ·
|
|
271
|
+
process.stdout.write('\n' + dim(` ${hidden} more module${hidden === 1 ? '' : 's'} · runlist modules --all\n`));
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -283,7 +283,7 @@ export function runModuleDetail(index, argv, config) {
|
|
|
283
283
|
positional.push(arg);
|
|
284
284
|
}
|
|
285
285
|
const name = positional[0];
|
|
286
|
-
if (!name) die('Usage:
|
|
286
|
+
if (!name) die('Usage: runlist module <name>');
|
|
287
287
|
|
|
288
288
|
const { rows } = aggregateModules(index.docs, config);
|
|
289
289
|
const row = rows.find(r => r.name === name);
|
|
@@ -292,7 +292,7 @@ export function runModuleDetail(index, argv, config) {
|
|
|
292
292
|
const suggestions = suggestCandidates(name, candidates);
|
|
293
293
|
const hint = suggestions.length
|
|
294
294
|
? `Did you mean: ${suggestions.join(', ')}?`
|
|
295
|
-
: `Available: ${candidates.slice(0, 5).join(', ')}${candidates.length > 5 ? ', …' : ''} (run \`
|
|
295
|
+
: `Available: ${candidates.slice(0, 5).join(', ')}${candidates.length > 5 ? ', …' : ''} (run \`runlist modules\` for the full list).`;
|
|
296
296
|
die(`Module '${name}' not found. ${hint}`);
|
|
297
297
|
}
|
|
298
298
|
|
package/src/new.mjs
CHANGED
|
@@ -132,7 +132,10 @@ ${ctx?.bodyInput?.trim() ?? ''}
|
|
|
132
132
|
description: 'Execution plan — build-up shape (Problem → Phases → Closeout) with phase status markers and Version History',
|
|
133
133
|
dir: 'plans',
|
|
134
134
|
targetRoot: 'plans',
|
|
135
|
-
|
|
135
|
+
// A scaffolded plan is scoped, not being worked; `dotmd use` starts it.
|
|
136
|
+
// Defaulting to `active` forced a second, guarded status write for every
|
|
137
|
+
// plan that was only being written down.
|
|
138
|
+
defaultStatus: 'planned',
|
|
136
139
|
// Body input lands in the Problem section. Plans don't have an Overview;
|
|
137
140
|
// Problem is the established opening section in the build-up shape.
|
|
138
141
|
acceptsBody: true,
|
|
@@ -335,7 +338,7 @@ export function readBodyInput(source) {
|
|
|
335
338
|
if (isBodyPlaceholder(file)) {
|
|
336
339
|
die(`\`@${file}\` is a placeholder, not a real path — write the body to a file first, then pass it:\n` +
|
|
337
340
|
` @/tmp/my-draft.md read the body from that file\n` +
|
|
338
|
-
` - read the body from stdin (\`cat notes.md |
|
|
341
|
+
` - read the body from stdin (\`cat notes.md | runlist …\`)\n` +
|
|
339
342
|
` --message "..." short one-liners only`);
|
|
340
343
|
}
|
|
341
344
|
die(`Body file not found: ${file}`);
|
|
@@ -444,8 +447,8 @@ ${bodyInput?.trim() ?? ''}
|
|
|
444
447
|
|
|
445
448
|
${steps}
|
|
446
449
|
|
|
447
|
-
Pick up the next child with \`
|
|
448
|
-
first non-archived child. \`
|
|
450
|
+
Pick up the next child with \`runlist runlist next ${hubSlug}\` — it targets the
|
|
451
|
+
first non-archived child. \`runlist runlist ${hubSlug}\` shows the sequence + status.
|
|
449
452
|
|
|
450
453
|
## Version History
|
|
451
454
|
|
|
@@ -490,7 +493,7 @@ function roadmapHubBody(title, hubSlug, bodyInput, today) {
|
|
|
490
493
|
# ${title}
|
|
491
494
|
|
|
492
495
|
> One-paragraph: the domain this roadmap composes. A roadmap points at *runlists*
|
|
493
|
-
> (not leaf plans) and rolls their done/total up — see \`
|
|
496
|
+
> (not leaf plans) and rolls their done/total up — see \`runlist roadmap ${hubSlug}\`.
|
|
494
497
|
|
|
495
498
|
## Scope
|
|
496
499
|
|
|
@@ -684,7 +687,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
684
687
|
name = await promptText(`${typeName} name: `);
|
|
685
688
|
if (!name) die('No name provided.');
|
|
686
689
|
} else {
|
|
687
|
-
die(`Usage:
|
|
690
|
+
die(`Usage: runlist new <type> <name> [body]\n types: ${[...knownTypes].join(', ')}\n body: inline text | piped stdin (auto) | "@path" (file) | --body "..."`);
|
|
688
691
|
}
|
|
689
692
|
}
|
|
690
693
|
|
|
@@ -731,7 +734,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
731
734
|
if (planShapes.length > 0 && typeName !== 'plan') {
|
|
732
735
|
const flag = planShapes[0][0];
|
|
733
736
|
const usage = flag === '--runlist' ? '--runlist a,b,c' : flag;
|
|
734
|
-
die(`${flag} only applies to plans. Use:
|
|
737
|
+
die(`${flag} only applies to plans. Use: runlist new plan <name> ${usage}`);
|
|
735
738
|
}
|
|
736
739
|
if (planShapes.length > 1) {
|
|
737
740
|
die(`${planShapes.map(([f]) => f).join(' and ')} are mutually exclusive — a plan has one body shape. Pick one.`);
|
|
@@ -784,7 +787,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
784
787
|
}
|
|
785
788
|
|
|
786
789
|
if (template.requiresBody && (!bodyInput || !bodyInput.trim())) {
|
|
787
|
-
die(`\`${typeName}\` template requires a body. Pipe stdin (\`cat draft.md |
|
|
790
|
+
die(`\`${typeName}\` template requires a body. Pipe stdin (\`cat draft.md | runlist new ${typeName} <slug>\`), pass @path, --body "...", or inline text.`);
|
|
788
791
|
}
|
|
789
792
|
|
|
790
793
|
// Fail-fast when the user passes body input to a template that doesn't
|
|
@@ -814,10 +817,10 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
814
817
|
const builtinAccepts = Boolean(builtin && (builtin.acceptsBody || builtin.requiresBody));
|
|
815
818
|
let cause;
|
|
816
819
|
if (isOverride && builtinAccepts) {
|
|
817
|
-
const where = config.configPath ? toRepoPath(config.configPath, config.repoRoot) : '
|
|
820
|
+
const where = config.configPath ? toRepoPath(config.configPath, config.repoRoot) : 'runlist.config.mjs';
|
|
818
821
|
cause = `Your config (${where}) overrides the built-in \`${typeName}\` template, and the override drops body acceptance.\nFix: in that override, add \`acceptsBody: true\` AND interpolate \`\${ctx?.bodyInput?.trim() ?? ''}\` into your \`body\` fn (e.g., inside \`## Problem\`). Or drop the override to use the built-in.`;
|
|
819
822
|
} else {
|
|
820
|
-
cause = `Either drop the body, switch to a template that accepts it, or set \`acceptsBody: true\` on your custom \`${typeName}\` template in
|
|
823
|
+
cause = `Either drop the body, switch to a template that accepts it, or set \`acceptsBody: true\` on your custom \`${typeName}\` template in runlist.config.mjs.`;
|
|
821
824
|
}
|
|
822
825
|
die(`\`${typeName}\` template does not accept body input, but body was passed via ${bodyInputSource}.${hint}\n${cause}`);
|
|
823
826
|
}
|
|
@@ -906,7 +909,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
906
909
|
+ ` ${repoPath}\n\n`
|
|
907
910
|
+ `A name with a \`/\` is read relative to the repo.\n`
|
|
908
911
|
+ `To place it under a root instead:\n`
|
|
909
|
-
+ `
|
|
912
|
+
+ ` runlist new ${typeName} ${name} --root ${path.basename(targetRoot)}\n`
|
|
910
913
|
+ ` → ${toRepoPath(rooted, config.repoRoot)}\n\n`
|
|
911
914
|
+ `Roots: ${allRoots.map(root => path.basename(root)).join(', ')}`);
|
|
912
915
|
}
|
|
@@ -1036,7 +1039,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
1036
1039
|
// confusingly). Tell the agent the next step explicitly, and flag a gitignored
|
|
1037
1040
|
// target for any type so "why won't this commit" never happens silently.
|
|
1038
1041
|
if (typeName === 'prompt') {
|
|
1039
|
-
process.stdout.write(dim('Session-local — no need to commit. The next session runs `
|
|
1042
|
+
process.stdout.write(dim('Session-local — no need to commit. The next session runs `runlist use` (or `runlist use ' + repoPath + '`) to consume it.\n'));
|
|
1040
1043
|
}
|
|
1041
1044
|
// Teach the field-length contract at the moment the fields get written —
|
|
1042
1045
|
// learning it from a cap warning later sends sessions into hand-trim /
|
package/src/output-identity.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
export const HTML_FALLBACK_DIR = '
|
|
4
|
+
export const HTML_FALLBACK_DIR = '__runlist';
|
|
5
|
+
// Exports written by dotmd-cli 0.78.0 and earlier put fallbacks under this
|
|
6
|
+
// directory, and an old export tree may still hold it. It stays reserved so a
|
|
7
|
+
// document's own path can never land inside a stale fallback directory.
|
|
8
|
+
export const LEGACY_HTML_FALLBACK_DIR = '__dotmd';
|
|
9
|
+
const RESERVED_FALLBACK_DIRS = [HTML_FALLBACK_DIR, LEGACY_HTML_FALLBACK_DIR];
|
|
5
10
|
|
|
6
11
|
function normalizeRelative(value, kind) {
|
|
7
12
|
if (typeof value !== 'string' || value.includes('\0')) {
|
|
@@ -59,7 +64,7 @@ export function allocateOutputIdentities(docs) {
|
|
|
59
64
|
preferred,
|
|
60
65
|
conflict: preferred.toLowerCase() === 'index.html'
|
|
61
66
|
|| preferred.toLowerCase().startsWith('index.html/')
|
|
62
|
-
|| preferred.toLowerCase().startsWith(`${
|
|
67
|
+
|| RESERVED_FALLBACK_DIRS.some(dir => preferred.toLowerCase().startsWith(`${dir.toLowerCase()}/`)),
|
|
63
68
|
});
|
|
64
69
|
}
|
|
65
70
|
|
package/src/pickup-card.mjs
CHANGED
|
@@ -208,7 +208,7 @@ export function buildCard(filePath, raw, config) {
|
|
|
208
208
|
// Render the card to human-format string.
|
|
209
209
|
export function renderCard(card) {
|
|
210
210
|
const lines = [];
|
|
211
|
-
lines.push(`[
|
|
211
|
+
lines.push(`[runlist] in-session: ${card.path} — close with: runlist set <status> ${card.path}`);
|
|
212
212
|
lines.push('---');
|
|
213
213
|
lines.push(`# ${card.title}`);
|
|
214
214
|
const meta = [card.status, card.updated && `updated ${card.updated}`].filter(Boolean).join(' · ');
|
|
@@ -266,7 +266,7 @@ export function renderCard(card) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
lines.push('');
|
|
269
|
-
lines.push(dim(`Body: ${formatBytes(card.bodyBytes)}. \`
|
|
269
|
+
lines.push(dim(`Body: ${formatBytes(card.bodyBytes)}. \`runlist set in-session ${card.path} --full\` for everything, or Read the file with offset/limit to target a section by line number.`));
|
|
270
270
|
return lines.join('\n') + '\n';
|
|
271
271
|
}
|
|
272
272
|
|
package/src/pickup.mjs
CHANGED
|
@@ -251,7 +251,7 @@ export function prepareOwnershipMigration(oldRepoPath, newPath, config, { sessio
|
|
|
251
251
|
// `rename` has no --force of its own: a rename carries the claim across to
|
|
252
252
|
// the new path rather than ending it, so the takeover has to happen first.
|
|
253
253
|
throw planBusyError(oldRepoPath, ownership,
|
|
254
|
-
`release it first with \`
|
|
254
|
+
`release it first with \`runlist set <status> ${oldRepoPath} --force\`, then rename.`);
|
|
255
255
|
}
|
|
256
256
|
const identity = plannedPlanIdentity(newPath, config);
|
|
257
257
|
const recordPath = recordPathForIdentity(identity, config);
|
|
@@ -459,7 +459,7 @@ export function releaseVanishedPlanClaim(claim, config, { now = new Date().toISO
|
|
|
459
459
|
throw new Error(`Claim changed under us for ${claim.plan}; re-run to see the current state.`);
|
|
460
460
|
}
|
|
461
461
|
if (existsSync(path.resolve(config.repoRoot, ownership.plan))) {
|
|
462
|
-
throw new Error(`${ownership.plan} exists after all — release it with \`
|
|
462
|
+
throw new Error(`${ownership.plan} exists after all — release it with \`runlist set active\` instead.`);
|
|
463
463
|
}
|
|
464
464
|
replaceSnapshot(snapshot, JSON.stringify({
|
|
465
465
|
schema: OWNERSHIP_SCHEMA,
|
package/src/prompts.mjs
CHANGED
|
@@ -33,9 +33,9 @@ export async function runPrompts(argv, config, opts = {}) {
|
|
|
33
33
|
const sub = argv[0];
|
|
34
34
|
|
|
35
35
|
if (sub && REMOVED_SUBCOMMANDS.has(sub)) {
|
|
36
|
-
die(`\`
|
|
36
|
+
die(`\`runlist prompts ${sub}\` was removed — a prompt is pending or archived, and nothing else.\n`
|
|
37
37
|
+ 'A prompt directs work, so parking one instead of archiving it leaves work state outside the\n'
|
|
38
|
-
+ 'plan that owns it. Lift its content into that plan, then `
|
|
38
|
+
+ 'plan that owns it. Lift its content into that plan, then `runlist prompts archive <file>`.');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (!sub || !SUBCOMMANDS.has(sub)) {
|
|
@@ -196,7 +196,7 @@ async function runPromptsNext(argv, config, opts = {}) {
|
|
|
196
196
|
}
|
|
197
197
|
const head = queue[0];
|
|
198
198
|
if (!head.abs) die(`Could not resolve path: ${head.doc.path}`);
|
|
199
|
-
return consumePrompt(head.abs, config, opts);
|
|
199
|
+
return consumePrompt(head.abs, config, { ...opts, noClaim: argv.includes('--no-claim') });
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// Resolve user input to a prompt path. Tries (in order): exact path,
|
|
@@ -242,11 +242,12 @@ export function resolvePromptInput(input, config, options = {}) {
|
|
|
242
242
|
|
|
243
243
|
async function runPromptsUse(argv, config, opts = {}) {
|
|
244
244
|
const input = argv.find(a => !a.startsWith('-'));
|
|
245
|
-
if (!input) die('Usage:
|
|
245
|
+
if (!input) die('Usage: runlist prompts use <file-or-slug>');
|
|
246
246
|
const noIndex = argv.includes('--no-index') || opts.noIndex;
|
|
247
247
|
const showFiles = argv.includes('--show-files') || opts.showFiles;
|
|
248
|
+
const noClaim = argv.includes('--no-claim');
|
|
248
249
|
const filePath = resolvePromptInput(input, config);
|
|
249
|
-
return consumePrompt(filePath, config, { ...opts, noIndex, showFiles });
|
|
250
|
+
return consumePrompt(filePath, config, { ...opts, noIndex, showFiles, noClaim });
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
export async function consumePrompt(filePath, config, opts) {
|
|
@@ -269,7 +270,9 @@ export async function consumePrompt(filePath, config, opts) {
|
|
|
269
270
|
const planRef = asString(parsed.plan);
|
|
270
271
|
let linkedClaim = null;
|
|
271
272
|
let claimSkipReason = null;
|
|
272
|
-
if (planRef) {
|
|
273
|
+
if (planRef && opts.noClaim) {
|
|
274
|
+
claimSkipReason = `--no-claim, ${planRef} left as it is`;
|
|
275
|
+
} else if (planRef) {
|
|
273
276
|
const outcome = prepareLinkedPromptClaim(planRef, config, path.dirname(filePath));
|
|
274
277
|
if (outcome?.skipped) claimSkipReason = outcome.reason;
|
|
275
278
|
else linkedClaim = outcome;
|
|
@@ -371,8 +374,8 @@ export async function writeConsumedBody(body, archivedPath, write = null, linked
|
|
|
371
374
|
}
|
|
372
375
|
}
|
|
373
376
|
catch (err) {
|
|
374
|
-
const completion = linkedPlan ? ` Then complete the linked claim with \`
|
|
375
|
-
throw new Error(`Prompt was consumed but body output failed (${err.code ?? err.message}). It will not be emitted again; recover it with \`
|
|
377
|
+
const completion = linkedPlan ? ` Then complete the linked claim with \`runlist use ${linkedPlan}\`.` : '';
|
|
378
|
+
throw new Error(`Prompt was consumed but body output failed (${err.code ?? err.message}). It will not be emitted again; recover it with \`runlist prompts show ${archivedPath}\`.${completion}`);
|
|
376
379
|
}
|
|
377
380
|
}
|
|
378
381
|
|
|
@@ -392,7 +395,7 @@ export async function writeConsumedBody(body, archivedPath, write = null, linked
|
|
|
392
395
|
function explainUnclaimablePlan(disposition, repoPath, status, startable) {
|
|
393
396
|
switch (disposition.kind) {
|
|
394
397
|
case 'parked':
|
|
395
|
-
return `${repoPath} is ${status} — \`
|
|
398
|
+
return `${repoPath} is ${status} — \`runlist set ${startable} ${repoPath}\` to unpark it, then \`runlist use ${repoPath}\``;
|
|
396
399
|
case 'busy':
|
|
397
400
|
return `${repoPath} is claimed by another session (${disposition.owner})`;
|
|
398
401
|
case 'terminal':
|
|
@@ -403,7 +406,7 @@ function explainUnclaimablePlan(disposition, repoPath, status, startable) {
|
|
|
403
406
|
case 'unconfigured-status':
|
|
404
407
|
return `${repoPath} has a status this repo does not configure (${status ?? 'none'})`;
|
|
405
408
|
case 'ownership-corrupt':
|
|
406
|
-
return `${repoPath} has an unreadable ownership record — \`
|
|
409
|
+
return `${repoPath} has an unreadable ownership record — \`runlist doctor --claims\``;
|
|
407
410
|
default:
|
|
408
411
|
return `${repoPath} cannot be claimed (${disposition.kind})`;
|
|
409
412
|
}
|
|
@@ -501,7 +504,7 @@ function runPromptsShow(argv, config) {
|
|
|
501
504
|
const inputs = argv.filter((a, i) => !a.startsWith('-') && i !== limitValueIdx);
|
|
502
505
|
|
|
503
506
|
if (!inputs.length && !all) {
|
|
504
|
-
die('Usage:
|
|
507
|
+
die('Usage: runlist prompts show <file-or-slug>...\n runlist prompts show --all [--limit N] # peek the whole pending queue');
|
|
505
508
|
}
|
|
506
509
|
if (inputs.length && all) die('Pass either prompt names or --all, not both.');
|
|
507
510
|
|
|
@@ -529,7 +532,7 @@ function runPromptsShow(argv, config) {
|
|
|
529
532
|
if (shown > 0) process.stdout.write('\n');
|
|
530
533
|
process.stdout.write(dim(`──── ${repoPath} [${status}] ────\n`));
|
|
531
534
|
} else {
|
|
532
|
-
process.stderr.write(dim(`${repoPath} [${status}] — read-only peek; \`
|
|
535
|
+
process.stderr.write(dim(`${repoPath} [${status}] — read-only peek; \`runlist use ${repoPath}\` to consume\n`));
|
|
533
536
|
}
|
|
534
537
|
process.stdout.write(body);
|
|
535
538
|
if (!body.endsWith('\n')) process.stdout.write('\n');
|
|
@@ -542,13 +545,13 @@ function runPromptsShow(argv, config) {
|
|
|
542
545
|
const truncated = total > targets.length;
|
|
543
546
|
if (multi || truncated) {
|
|
544
547
|
const suffix = truncated ? ` of ${total} (use --limit to change)` : '';
|
|
545
|
-
process.stderr.write(dim(`\n${shown} prompt${shown === 1 ? '' : 's'}${suffix} — read-only peek; \`
|
|
548
|
+
process.stderr.write(dim(`\n${shown} prompt${shown === 1 ? '' : 's'}${suffix} — read-only peek; \`runlist use <file>\` to consume one\n`));
|
|
546
549
|
}
|
|
547
550
|
}
|
|
548
551
|
|
|
549
552
|
function runPromptsArchive(argv, config, opts = {}) {
|
|
550
553
|
const input = argv.find(a => !a.startsWith('-'));
|
|
551
|
-
if (!input) die('Usage:
|
|
554
|
+
if (!input) die('Usage: runlist prompts archive <file-or-slug>');
|
|
552
555
|
const noIndex = argv.includes('--no-index') || opts.noIndex;
|
|
553
556
|
const showFiles = argv.includes('--show-files') || opts.showFiles;
|
|
554
557
|
const filePath = resolvePromptInput(input, config);
|
|
@@ -565,7 +568,7 @@ function runPromptsArchive(argv, config, opts = {}) {
|
|
|
565
568
|
|
|
566
569
|
async function runPromptsNew(argv, config, opts = {}) {
|
|
567
570
|
if (!argv[0] || argv[0].startsWith('-')) {
|
|
568
|
-
die('Usage:
|
|
571
|
+
die('Usage: runlist prompts new <slug> [body]\n body: inline text | piped stdin (auto) | "@path" (file) | --body "..."');
|
|
569
572
|
}
|
|
570
573
|
return runNew(['prompt', ...argv], config, opts);
|
|
571
574
|
}
|
package/src/query.mjs
CHANGED
|
@@ -83,7 +83,7 @@ export function runQuery(index, argv, config, opts = {}) {
|
|
|
83
83
|
if (opts.type && !filters.types) filters.types = opts.type.split(',').map(v => v.trim()).filter(Boolean);
|
|
84
84
|
if (opts.root && !filters.root) filters.root = opts.root;
|
|
85
85
|
if (filters.body && !filters.keyword) {
|
|
86
|
-
die('`--body` extends a keyword search into document bodies — pass `--keyword <term>` (or use `
|
|
86
|
+
die('`--body` extends a keyword search into document bodies — pass `--keyword <term>` (or use `runlist grep <term>`).');
|
|
87
87
|
}
|
|
88
88
|
const docs = filterDocs(index.docs, filters, config, opts.gitMetadataOptions);
|
|
89
89
|
const summaryPreviewSkipped = filters.summarize && config._execution?.suppressSideEffects;
|
|
@@ -193,7 +193,7 @@ export function runRunlists(index, argv, config) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
const roadmapPointer = roadmapCount > 0
|
|
196
|
-
? dim(` ${roadmapCount} roadmap${roadmapCount === 1 ? '' : 's'} ·
|
|
196
|
+
? dim(` ${roadmapCount} roadmap${roadmapCount === 1 ? '' : 's'} · runlist roadmaps\n`)
|
|
197
197
|
: '';
|
|
198
198
|
|
|
199
199
|
if (hubs.length === 0) {
|
|
@@ -206,7 +206,7 @@ export function runRunlists(index, argv, config) {
|
|
|
206
206
|
const shown = hubs.slice(0, limit);
|
|
207
207
|
renderCoordinationSection(shown, coordination, maxWidth, hubs.length);
|
|
208
208
|
const hidden = hubs.length - shown.length;
|
|
209
|
-
if (hidden > 0) process.stdout.write(dim(` ${hidden} more ·
|
|
209
|
+
if (hidden > 0) process.stdout.write(dim(` ${hidden} more · runlist runlists --limit ${hubs.length}\n`));
|
|
210
210
|
if (roadmapPointer) process.stdout.write(roadmapPointer);
|
|
211
211
|
process.stdout.write('\n');
|
|
212
212
|
}
|
|
@@ -651,7 +651,7 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
|
|
|
651
651
|
const mainHidden = mainAll.length - mainShown.length;
|
|
652
652
|
if (mainHidden > 0) {
|
|
653
653
|
process.stdout.write('\n');
|
|
654
|
-
process.stdout.write(dim(` ${mainHidden} more ${noun} ·
|
|
654
|
+
process.stdout.write(dim(` ${mainHidden} more ${noun} · runlist ${noun} --all · runlist ${noun} status\n`));
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
// Roadmaps tier — pinned above Runlists, with the recursive grand total.
|
|
@@ -659,7 +659,7 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
|
|
|
659
659
|
renderRoadmapsSection(roadmapShown, roadmap, maxWidth, roadmapAll.length);
|
|
660
660
|
const roadmapHidden = roadmapAll.length - roadmapShown.length;
|
|
661
661
|
if (roadmapHidden > 0) {
|
|
662
|
-
process.stdout.write(dim(` ${roadmapHidden} more roadmaps ·
|
|
662
|
+
process.stdout.write(dim(` ${roadmapHidden} more roadmaps · runlist roadmaps\n`));
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
|
|
@@ -667,7 +667,7 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
|
|
|
667
667
|
renderCoordinationSection(coordShown, coordination, maxWidth, coordAll.length);
|
|
668
668
|
const coordHidden = coordAll.length - coordShown.length;
|
|
669
669
|
if (coordHidden > 0) {
|
|
670
|
-
process.stdout.write(dim(` ${coordHidden} more runlists ·
|
|
670
|
+
process.stdout.write(dim(` ${coordHidden} more runlists · runlist ${noun} --all\n`));
|
|
671
671
|
}
|
|
672
672
|
}
|
|
673
673
|
|
|
@@ -684,7 +684,7 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
|
|
|
684
684
|
&& !archiveStatuses.has(v.doc.status)
|
|
685
685
|
&& !isArchivedPath(v.doc.path, config)).length;
|
|
686
686
|
if (filterHidden > 0) {
|
|
687
|
-
process.stdout.write(dim(` ${filterHidden} runlist${filterHidden === 1 ? '' : 's'} hidden by filter ·
|
|
687
|
+
process.stdout.write(dim(` ${filterHidden} runlist${filterHidden === 1 ? '' : 's'} hidden by filter · runlist runlists\n`));
|
|
688
688
|
}
|
|
689
689
|
}
|
|
690
690
|
|
|
@@ -696,7 +696,7 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
|
|
|
696
696
|
const hidden = totalAll - totalShown;
|
|
697
697
|
if (hidden > 0) {
|
|
698
698
|
process.stdout.write('\n');
|
|
699
|
-
process.stdout.write(dim(` ${hidden} more ${noun} ·
|
|
699
|
+
process.stdout.write(dim(` ${hidden} more ${noun} · runlist ${noun} --all · runlist ${noun} status\n`));
|
|
700
700
|
}
|
|
701
701
|
|
|
702
702
|
process.stdout.write('\n');
|
|
@@ -876,7 +876,7 @@ function renderHubBlock(hub, info, children, maxWidth, topMaxSlug) {
|
|
|
876
876
|
// bird's-eye), distinct from the Runlists section's per-hub rollup. `dotmd
|
|
877
877
|
// roadmap <hub>` expands one into its child rows.
|
|
878
878
|
function renderRoadmapsSection(roadmapDocs, roadmap, maxWidth, total) {
|
|
879
|
-
process.stdout.write(`\n${bold(`Roadmaps (${total ?? roadmapDocs.length})`)} ${dim('·
|
|
879
|
+
process.stdout.write(`\n${bold(`Roadmaps (${total ?? roadmapDocs.length})`)} ${dim('· runlist roadmap')}\n`);
|
|
880
880
|
const maxSlug = Math.min(34, Math.max(...roadmapDocs.map(d => hubLabel(d).length)));
|
|
881
881
|
for (const doc of roadmapDocs) {
|
|
882
882
|
const info = roadmap?.get(doc.path);
|
package/src/rename.mjs
CHANGED
|
@@ -16,9 +16,9 @@ export async function runRename(argv, config, opts = {}) {
|
|
|
16
16
|
const positional = argv.filter(arg => !arg.startsWith('-'));
|
|
17
17
|
const oldInput = positional[0];
|
|
18
18
|
let newInput = positional[1];
|
|
19
|
-
if (!oldInput) die('Usage:
|
|
19
|
+
if (!oldInput) die('Usage: runlist rename <old> <new>');
|
|
20
20
|
if (!newInput) {
|
|
21
|
-
if (!isInteractive()) die('Usage:
|
|
21
|
+
if (!isInteractive()) die('Usage: runlist rename <old> <new>');
|
|
22
22
|
newInput = await promptText('New name: ');
|
|
23
23
|
if (!newInput) die('No name provided.');
|
|
24
24
|
}
|