dotmd-cli 0.87.0 → 0.87.2
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 +64 -4
- package/bin/dotmd.mjs +117 -14
- package/package.json +1 -1
- package/runlist.config.example.mjs +1 -1
- package/src/ai.mjs +16 -42
- package/src/baton.mjs +1 -1
- package/src/body-link.mjs +11 -3
- package/src/commands.mjs +11 -0
- package/src/config.mjs +2 -0
- package/src/decisions.mjs +233 -21
- package/src/diff.mjs +2 -2
- package/src/errors-read.mjs +30 -0
- package/src/journal.mjs +26 -0
- package/src/lint.mjs +2 -2
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/show.mjs +101 -0
- package/src/summary.mjs +1 -1
- package/src/validate.mjs +1 -1
package/README.md
CHANGED
|
@@ -97,20 +97,21 @@ Restart Claude Code, or run `/reload-plugins`, after a plugin update.
|
|
|
97
97
|
```bash
|
|
98
98
|
runlist init # create config, docs/, and the generated index
|
|
99
99
|
runlist new plan auth-refresh # scaffold a typed document
|
|
100
|
-
runlist
|
|
101
|
-
runlist
|
|
100
|
+
runlist plans # compact live plan dashboard
|
|
101
|
+
runlist briefing # comprehensive active-work view
|
|
102
102
|
runlist check # validate schema, references, and lifecycle shape
|
|
103
103
|
runlist doctor # preview repairs; add --apply to write
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
`runlist
|
|
106
|
+
`runlist plans` is the compact orientation view. `runlist briefing` lists live
|
|
107
|
+
plans with next steps and grows with the corpus. `runlist context` is the fuller
|
|
107
108
|
human/LLM briefing, while `runlist agent-context` emits bounded structured JSON for
|
|
108
109
|
agent integrations.
|
|
109
110
|
|
|
110
111
|
## Core Workflow
|
|
111
112
|
|
|
112
113
|
```bash
|
|
113
|
-
runlist
|
|
114
|
+
runlist plans
|
|
114
115
|
runlist use docs/plans/auth-refresh.md
|
|
115
116
|
runlist set awaiting docs/plans/auth-refresh.md --note "Need API owner decision"
|
|
116
117
|
runlist set active docs/plans/auth-refresh.md --note "Decision received"
|
|
@@ -259,6 +260,12 @@ The register block is the fenced block whose first line carries `statusLine`.
|
|
|
259
260
|
The row is the question plus `--answers` (what each answer leaves in place),
|
|
260
261
|
which is required when a register is configured.
|
|
261
262
|
|
|
263
|
+
`runlist decisions --json` gives each open or held decision its `question`,
|
|
264
|
+
its document's `docTitle`, and `blocks`: what its record says it blocks or
|
|
265
|
+
gates, then every unticked checklist item and frontmatter blocker that names
|
|
266
|
+
its id, alone or inside a written range (`A2 to A12`). In another document the
|
|
267
|
+
id has to sit beside a link to the decision's document.
|
|
268
|
+
|
|
262
269
|
## Flags
|
|
263
270
|
|
|
264
271
|
A flag is something someone found that the person should know about when they
|
|
@@ -282,6 +289,59 @@ merged. The log is append-only, `.runlist/flags.jsonl` by default
|
|
|
282
289
|
beside the flag, never over it. Every session start shows a count and the top
|
|
283
290
|
open flags.
|
|
284
291
|
|
|
292
|
+
## Local model
|
|
293
|
+
|
|
294
|
+
Summaries (`--summarize`, `runlist summary`, `runlist diff --summarize`) and
|
|
295
|
+
lint's status inference use one local model server, Ollama by default or any
|
|
296
|
+
OpenAI-compatible server. runlist never starts it or pulls a model on its own:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
runlist model # server, model, cap, free memory, what is loaded
|
|
300
|
+
runlist model start # ollama serve, one model and one request at a time
|
|
301
|
+
runlist model measure # record each pulled candidate's peak memory and speed here
|
|
302
|
+
runlist model use qwen3.5:9b # name a model; `auto` picks the first measured one under the cap
|
|
303
|
+
runlist model cap 8 # most memory a model may take; `auto` is a quarter of the machine
|
|
304
|
+
runlist model stop # unload it now
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The model unloads after 5 idle minutes. Before it loads, the memory free right
|
|
308
|
+
now must hold it plus 1.5 GB with memory pressure normal, or the command goes
|
|
309
|
+
on without it. On an 8 GB machine the cap is 2 GB and model features stay off;
|
|
310
|
+
point `RUNLIST_MODEL_ENDPOINT` at a server on a bigger machine instead. Settings
|
|
311
|
+
are per machine in `~/.runlist/model.json`.
|
|
312
|
+
|
|
313
|
+
`runlist model status --json` leads with a one-line reading for dashboards,
|
|
314
|
+
`running`, `name` (the model loaded now, else the one runlist would load) and
|
|
315
|
+
`memoryMb` (what it holds, null when nothing is loaded), followed by the full
|
|
316
|
+
detail.
|
|
317
|
+
|
|
318
|
+
## One document's card
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
runlist show docs/plans/shelf.md # title, status, next step, blockers, checklist, related, links
|
|
322
|
+
runlist show shelf bins --json # [{ path, title, status, nextStep, blockers, checklist,
|
|
323
|
+
# related: [{ field, path, title, status }], links: [...] }]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Read-only: nothing is claimed. `related` is what the frontmatter names
|
|
327
|
+
(`related_plans`, `related_docs`, `supports_plans`, `parent_plan`, `runlist` and
|
|
328
|
+
the configured reference fields), each with its title and status; `links` is
|
|
329
|
+
every other document the body links to.
|
|
330
|
+
|
|
331
|
+
## Failed commands
|
|
332
|
+
|
|
333
|
+
Every runlist command that fails, by an error or a non-zero exit, appends one
|
|
334
|
+
line to `~/.claude/logs/runlist-errors.log` (`RUNLIST_ERROR_LOG_DIR` moves it)
|
|
335
|
+
with the time, the command with secrets redacted, and the error's one-line
|
|
336
|
+
message. It rolls over once, to `runlist-errors.log.1`, at 5 MB or a new
|
|
337
|
+
runlist version. Dry runs and the session-start `hud` are never logged.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
runlist errors # the newest 20 failures, newest first
|
|
341
|
+
runlist errors --limit 50 # the newest N
|
|
342
|
+
runlist errors --json # [{ at, command, message, repo, exit }]
|
|
343
|
+
```
|
|
344
|
+
|
|
285
345
|
## Safety Model
|
|
286
346
|
|
|
287
347
|
- Mutation commands support `--dry-run` / `-n`.
|
package/bin/dotmd.mjs
CHANGED
|
@@ -203,6 +203,20 @@ stop the others: every step runs, the failures are listed together, and the
|
|
|
203
203
|
exit code is 1. A plugin whose marketplace registration is gone gets the
|
|
204
204
|
marketplace re-added before the update (see \`runlist install claude\`).`,
|
|
205
205
|
|
|
206
|
+
errors: `runlist errors — the newest failed runlist commands, newest first
|
|
207
|
+
|
|
208
|
+
Every runlist command that fails (an error, or a non-zero exit such as
|
|
209
|
+
\`check\` finding errors) appends one line to ~/.claude/logs/runlist-errors.log
|
|
210
|
+
(RUNLIST_ERROR_LOG_DIR moves it): when, the command with secrets redacted, and
|
|
211
|
+
the error's one-line message. It rolls over to runlist-errors.log.1 at 5 MB or
|
|
212
|
+
a new runlist version; this reads both. Dry runs and the session-start hud are
|
|
213
|
+
never logged.
|
|
214
|
+
|
|
215
|
+
runlist errors last 20
|
|
216
|
+
runlist errors --limit 50 last N (--tail is the same)
|
|
217
|
+
runlist errors --repo <name> only failures in a matching repo
|
|
218
|
+
runlist errors --json [{ at, command, message, repo, exit }]`,
|
|
219
|
+
|
|
206
220
|
misuse: `runlist misuse — read the cross-repo guard log (~/.claude/logs/runlist-misuse.log,
|
|
207
221
|
merged by time with the legacy dotmd-misuse.log that older CLIs wrote)
|
|
208
222
|
|
|
@@ -247,8 +261,10 @@ Analyze:
|
|
|
247
261
|
unblocks <file> [--json] Show what completes when this doc ships
|
|
248
262
|
diff [file] [--summarize] Show changes since last updated date
|
|
249
263
|
summary <file> [--json] AI summary of a document
|
|
264
|
+
model [start|stop|use|cap] The local model: server, which model, memory cap
|
|
250
265
|
glossary <term> [--list] [--json] Look up domain terms + related docs
|
|
251
266
|
decisions [doc|id] [--all|--check] Open and held decisions, read from the corpus
|
|
267
|
+
show <file...> [--json] One document's card: status, next step, related plans and docs, links
|
|
252
268
|
|
|
253
269
|
Validate & Fix:
|
|
254
270
|
doctor [--apply] Auto-fix everything: refs, membership, lint, long fields, dates, index (preview by default)
|
|
@@ -294,6 +310,7 @@ Setup:
|
|
|
294
310
|
completions <shell> Shell completion script (bash, zsh)
|
|
295
311
|
journal [--tail N|--errors|--by-command|--session id|--since iso|--json]
|
|
296
312
|
View opt-in JSONL command journal (enable: RUNLIST_JOURNAL=1 or journal: true)
|
|
313
|
+
errors [--limit N] [--json] The newest failed runlist commands, from the cross-repo error log
|
|
297
314
|
|
|
298
315
|
Global Options:
|
|
299
316
|
--config <path> Explicit config file path
|
|
@@ -659,10 +676,11 @@ Options:
|
|
|
659
676
|
--json Output as JSON ({ owned, prompts, errors, previousSelf,
|
|
660
677
|
fleet, recentRejections, misuseRecap, drift })`,
|
|
661
678
|
|
|
662
|
-
briefing: `runlist briefing —
|
|
679
|
+
briefing: `runlist briefing — comprehensive live-work summary
|
|
663
680
|
|
|
664
|
-
Shows plan
|
|
665
|
-
|
|
681
|
+
Shows every live plan with its next step, plus doc/research counts and health.
|
|
682
|
+
Output grows with the corpus; use \`runlist plans\` for compact orientation or
|
|
683
|
+
\`runlist agent-context\` for structured agent context.
|
|
666
684
|
|
|
667
685
|
Options:
|
|
668
686
|
--json Output as JSON`,
|
|
@@ -1096,7 +1114,7 @@ Options:
|
|
|
1096
1114
|
Generates an AI-powered summary using a local model.
|
|
1097
1115
|
|
|
1098
1116
|
Options:
|
|
1099
|
-
--model <name> Model to use (default:
|
|
1117
|
+
--model <name> Model to use (default: the one \`runlist model\` shows)
|
|
1100
1118
|
--max-tokens <n> Max tokens for generation (default: 200)
|
|
1101
1119
|
--json Output as JSON`,
|
|
1102
1120
|
|
|
@@ -1109,7 +1127,7 @@ Options:
|
|
|
1109
1127
|
--stat Summary only (files changed, insertions/deletions)
|
|
1110
1128
|
--since <date> Override: diff since this date instead of frontmatter
|
|
1111
1129
|
--summarize Generate AI summary using local model
|
|
1112
|
-
--model <name> Model to use (default:
|
|
1130
|
+
--model <name> Model to use (default: the one \`runlist model\` shows)`,
|
|
1113
1131
|
|
|
1114
1132
|
lint: `runlist lint [--fix] — check and auto-fix frontmatter issues
|
|
1115
1133
|
|
|
@@ -1336,7 +1354,8 @@ Options:
|
|
|
1336
1354
|
runlist decisions <doc> one document, by path, filename or slug
|
|
1337
1355
|
runlist decisions <ID> one record, whole, as it is written
|
|
1338
1356
|
runlist decisions --all every disposition, not only the pending ones
|
|
1339
|
-
runlist decisions --json the same rows as data
|
|
1357
|
+
runlist decisions --json the same rows as data, each with its question, its
|
|
1358
|
+
document's title and what it blocks
|
|
1340
1359
|
runlist decisions --check every item it could not read or that is incomplete; exit 1 if any
|
|
1341
1360
|
|
|
1342
1361
|
A decision is an item in a decisions section (a heading naming the section word
|
|
@@ -1345,7 +1364,53 @@ token is an id. A register is a fenced block whose first line carries
|
|
|
1345
1364
|
decisions.register.statusLine; its rows are \`ID text\`. A disposition is read
|
|
1346
1365
|
from a \`Disposition:\` line or a row's opening word, then from prose when
|
|
1347
1366
|
decisions.prose is on, then from the block or bold lead above it. Configure it
|
|
1348
|
-
with \`export const decisions = { ... }\` (see src/decisions.mjs)
|
|
1367
|
+
with \`export const decisions = { ... }\` (see src/decisions.mjs).
|
|
1368
|
+
|
|
1369
|
+
What a decision blocks, in --json as \`blocks: [{ doc, line, kind, section, text }]\`:
|
|
1370
|
+
what its record says it blocks or gates (kind "stated"), and every unticked
|
|
1371
|
+
checklist item (kind "item") or frontmatter blocker (kind "blocker") that
|
|
1372
|
+
names its id, alone or inside a written range such as \`A2 to A12\`. In
|
|
1373
|
+
another document the id has to sit beside a link to the decision's document.`,
|
|
1374
|
+
|
|
1375
|
+
show: `runlist show <file...> — one document's card, read-only
|
|
1376
|
+
|
|
1377
|
+
runlist show <file> title, status, next step, blockers, checklist,
|
|
1378
|
+
related plans and docs, documents linked from the body
|
|
1379
|
+
runlist show <a> <b> --json [{ path, type, title, status, summary, currentState,
|
|
1380
|
+
nextStep, blockers, checklist, updated,
|
|
1381
|
+
related: [{ field, ref, path, exists, title, status, type }],
|
|
1382
|
+
links: [{ path, title, status, type }] }]
|
|
1383
|
+
|
|
1384
|
+
A file is a path, filename or slug, as for \`use\`. Nothing is claimed or
|
|
1385
|
+
written. With --json a file that is not found is { path, error } rather than
|
|
1386
|
+
an exit.`,
|
|
1387
|
+
|
|
1388
|
+
model: `runlist model — the local model behind summaries and lint
|
|
1389
|
+
|
|
1390
|
+
One local server (Ollama by default) holds one model, serves one request at a
|
|
1391
|
+
time and unloads it after it sits idle. runlist never starts the server or
|
|
1392
|
+
pulls a model on its own.
|
|
1393
|
+
|
|
1394
|
+
Subcommands:
|
|
1395
|
+
status [--json] Default. Server, model, cap, what is loaded and its memory
|
|
1396
|
+
start [--force] Start \`ollama serve\` (one model, one request at a time);
|
|
1397
|
+
refused where no model fits unless forced
|
|
1398
|
+
stop [--all] Unload runlist's model now; --all unloads every loaded model
|
|
1399
|
+
measure [name...] Load each pulled candidate (or the named ones) in turn,
|
|
1400
|
+
summarise the corpus's largest documents at lower
|
|
1401
|
+
priority, and record peak memory and speed for this
|
|
1402
|
+
machine; a model is picked only once measured here
|
|
1403
|
+
use <name|auto> Name the model; auto takes the first measured candidate
|
|
1404
|
+
under the cap
|
|
1405
|
+
cap <gb|auto> The most memory a model may take; auto (the default) is a
|
|
1406
|
+
quarter of this machine's memory, at most 12 GB
|
|
1407
|
+
|
|
1408
|
+
Before a model loads here, the memory free now must hold it plus 1.5 GB and
|
|
1409
|
+
memory pressure must be normal; otherwise the command goes on without it.
|
|
1410
|
+
|
|
1411
|
+
Settings live in ~/.runlist/model.json. RUNLIST_MODEL, RUNLIST_MODEL_CAP_GB and
|
|
1412
|
+
RUNLIST_MODEL_ENDPOINT override them; \`runtime: "openai"\` there points at any
|
|
1413
|
+
OpenAI-compatible server (mlx_lm.server, llama-server, LM Studio).`,
|
|
1349
1414
|
|
|
1350
1415
|
glossary: `runlist glossary <term> — look up domain terms and related docs
|
|
1351
1416
|
|
|
@@ -1650,7 +1715,8 @@ async function main() {
|
|
|
1650
1715
|
if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
|
|
1651
1716
|
if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
|
|
1652
1717
|
process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: runlist <cmd> --help\n`);
|
|
1653
|
-
process.
|
|
1718
|
+
process.exitCode = 1;
|
|
1719
|
+
return;
|
|
1654
1720
|
}
|
|
1655
1721
|
process.stdout.write(`${HELP._main}\n`);
|
|
1656
1722
|
return;
|
|
@@ -1916,7 +1982,17 @@ async function main() {
|
|
|
1916
1982
|
if (command === 'flags') { const { runFlags } = await import('../src/flags.mjs'); runFlags(restArgs, config); return; }
|
|
1917
1983
|
if (command === 'flag') { const { runFlag } = await import('../src/flags.mjs'); runFlag(restArgs, config); return; }
|
|
1918
1984
|
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
1919
|
-
if (command === '
|
|
1985
|
+
if (command === 'model') { const { runModel } = await import('../src/model.mjs'); await runModel(restArgs, config); return; }
|
|
1986
|
+
if (command === 'show') { const { runShow } = await import('../src/show.mjs'); runShow(restArgs, config); return; }
|
|
1987
|
+
if (command === 'decisions') {
|
|
1988
|
+
const { runDecisions } = await import('../src/decisions.mjs');
|
|
1989
|
+
const result = runDecisions(restArgs, config);
|
|
1990
|
+
if (result?.defects?.length) {
|
|
1991
|
+
const first = result.defects[0];
|
|
1992
|
+
_exitFailureMessage = `${result.defects.length} decision defect(s); first: ${first.doc}:${first.line} ${first.message}`;
|
|
1993
|
+
}
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1920
1996
|
if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
1921
1997
|
|
|
1922
1998
|
// Lifecycle commands
|
|
@@ -1930,6 +2006,7 @@ async function main() {
|
|
|
1930
2006
|
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config, { dryRun }); return; }
|
|
1931
2007
|
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config, { dryRun }); return; }
|
|
1932
2008
|
if (command === 'install') { const { runInstall } = await import('../src/install.mjs'); runInstall(restArgs, config, { dryRun }); return; }
|
|
2009
|
+
if (command === 'errors') { const { runErrors } = await import('../src/errors-read.mjs'); runErrors(restArgs); return; }
|
|
1933
2010
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1934
2011
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1935
2012
|
if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
|
|
@@ -2107,7 +2184,10 @@ async function main() {
|
|
|
2107
2184
|
writeCheckPreviewNote();
|
|
2108
2185
|
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse, verbose }));
|
|
2109
2186
|
}
|
|
2110
|
-
if (freshIndex.errors.length > 0)
|
|
2187
|
+
if (freshIndex.errors.length > 0) {
|
|
2188
|
+
process.exitCode = 1;
|
|
2189
|
+
_exitFailureMessage = checkFailureSummary(freshIndex.errors);
|
|
2190
|
+
}
|
|
2111
2191
|
return;
|
|
2112
2192
|
}
|
|
2113
2193
|
|
|
@@ -2117,13 +2197,19 @@ async function main() {
|
|
|
2117
2197
|
|
|
2118
2198
|
if (args.includes('--json')) {
|
|
2119
2199
|
process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
|
|
2120
|
-
if (index.errors.length > 0)
|
|
2200
|
+
if (index.errors.length > 0) {
|
|
2201
|
+
process.exitCode = 1;
|
|
2202
|
+
_exitFailureMessage = checkFailureSummary(index.errors);
|
|
2203
|
+
}
|
|
2121
2204
|
return;
|
|
2122
2205
|
}
|
|
2123
2206
|
|
|
2124
2207
|
writeCheckPreviewNote();
|
|
2125
2208
|
process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse, verbose }));
|
|
2126
|
-
if (index.errors.length > 0)
|
|
2209
|
+
if (index.errors.length > 0) {
|
|
2210
|
+
process.exitCode = 1;
|
|
2211
|
+
_exitFailureMessage = checkFailureSummary(index.errors);
|
|
2212
|
+
}
|
|
2127
2213
|
return;
|
|
2128
2214
|
}
|
|
2129
2215
|
|
|
@@ -2348,9 +2434,15 @@ async function main() {
|
|
|
2348
2434
|
let _resolvedConfig = null;
|
|
2349
2435
|
let _resolvedCommand = null;
|
|
2350
2436
|
let _suppressObservability = false;
|
|
2437
|
+
let _exitFailureMessage = null;
|
|
2351
2438
|
const _startMs = Date.now();
|
|
2352
2439
|
const _invocationArgs = process.argv.slice(2);
|
|
2353
2440
|
|
|
2441
|
+
function checkFailureSummary(errors) {
|
|
2442
|
+
const first = errors[0];
|
|
2443
|
+
return `${errors.length} check error(s); first: ${first.path ? `${first.path}: ` : ''}${first.message}`;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2354
2446
|
function _journalExit(err) {
|
|
2355
2447
|
if (_suppressObservability || _resolvedCommand === 'hud' || _invocationArgs.includes('--dry-run') || _invocationArgs.includes('-n')) return;
|
|
2356
2448
|
try {
|
|
@@ -2362,19 +2454,30 @@ function _journalExit(err) {
|
|
|
2362
2454
|
version: pkg.version,
|
|
2363
2455
|
});
|
|
2364
2456
|
} catch { /* never break exit on journal failure */ }
|
|
2365
|
-
|
|
2457
|
+
// A command that reports its own failure through the exit code (check with
|
|
2458
|
+
// errors, a failed update step) is a failure too.
|
|
2459
|
+
const code = Number(process.exitCode ?? 0);
|
|
2460
|
+
const failure = err ?? (code !== 0 ? { name: 'ExitStatus', message: _exitFailureMessage ?? `exited with status ${code}` } : null);
|
|
2461
|
+
if (failure) {
|
|
2366
2462
|
try {
|
|
2367
2463
|
recordGlobalError({
|
|
2368
2464
|
config: _resolvedConfig,
|
|
2369
2465
|
startMs: _startMs,
|
|
2370
2466
|
args: _invocationArgs,
|
|
2371
|
-
err,
|
|
2467
|
+
err: failure,
|
|
2372
2468
|
version: pkg.version,
|
|
2373
2469
|
});
|
|
2374
2470
|
} catch { /* never break exit on error-log failure */ }
|
|
2375
2471
|
}
|
|
2376
2472
|
}
|
|
2377
2473
|
|
|
2474
|
+
// A reader that stops early (`runlist flags | head`) closes the pipe; the rest
|
|
2475
|
+
// of the output has nowhere to go, which is not a failure.
|
|
2476
|
+
process.stdout.on('error', err => {
|
|
2477
|
+
if (err.code === 'EPIPE') process.exit(0);
|
|
2478
|
+
throw err;
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2378
2481
|
main()
|
|
2379
2482
|
.then(() => { _journalExit(null); })
|
|
2380
2483
|
.catch(err => {
|
package/package.json
CHANGED
|
@@ -329,6 +329,6 @@ export const presets = {
|
|
|
329
329
|
// by age, and an expired lease retries only when its owner is demonstrably dead.
|
|
330
330
|
// export function onPickup({ path, oldStatus, newStatus, operationId }) {}
|
|
331
331
|
|
|
332
|
-
// AI hooks — override summarization (replaces local
|
|
332
|
+
// AI hooks — override summarization (replaces the local model server; see `runlist model`).
|
|
333
333
|
// export function summarizeDoc(body, meta) { return 'Custom summary'; }
|
|
334
334
|
// export function summarizeDiff(diffOutput, filePath) { return 'Custom diff summary'; }
|
package/src/ai.mjs
CHANGED
|
@@ -1,56 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { warn } from './util.mjs';
|
|
3
|
-
import { dim } from './color.mjs';
|
|
1
|
+
import { generate } from './model.mjs';
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
// Model-backed text for summaries and lint. The server applies each model's
|
|
4
|
+
// own chat template, so prompts are plain system and user messages.
|
|
5
|
+
// `runlist model` shows which model runs and where.
|
|
6
6
|
|
|
7
|
-
export function
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
if (!uvChecked) warn('uv is not installed. Install it to enable AI summaries: https://docs.astral.sh/uv/');
|
|
12
|
-
return uvChecked;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const DEFAULT_MODEL = 'mlx-community/Llama-3.2-3B-Instruct-4bit';
|
|
16
|
-
|
|
17
|
-
export function runMLX(prompt, opts = {}) {
|
|
18
|
-
const { model = DEFAULT_MODEL, maxTokens = 200, timeout = 120000 } = opts;
|
|
19
|
-
if (!checkUvAvailable()) return null;
|
|
20
|
-
|
|
21
|
-
process.stderr.write(dim(' generating...'));
|
|
22
|
-
const result = spawnSync('uv', [
|
|
23
|
-
'run', '--with', 'mlx-lm',
|
|
24
|
-
'python3', '-m', 'mlx_lm', 'generate',
|
|
25
|
-
'--model', model,
|
|
26
|
-
'--prompt', prompt,
|
|
27
|
-
'--max-tokens', String(maxTokens),
|
|
28
|
-
'--verbose', 'false',
|
|
29
|
-
], { encoding: 'utf8', timeout });
|
|
30
|
-
|
|
31
|
-
process.stderr.write('\r\x1b[K');
|
|
32
|
-
if (result.status !== 0) return null;
|
|
33
|
-
|
|
34
|
-
const output = result.stdout.trim();
|
|
35
|
-
const lines = output.split('\n')
|
|
36
|
-
.filter(l => !l.includes('Fetching') && !l.includes('Warning:') && !l.includes('=========='));
|
|
37
|
-
const text = lines.join(' ').trim();
|
|
7
|
+
export function runModel(prompt, opts = {}) {
|
|
8
|
+
const { system, ...rest } = opts;
|
|
9
|
+
const messages = system ? [{ role: 'system', content: system }, { role: 'user', content: prompt }] : [{ role: 'user', content: prompt }];
|
|
10
|
+
const text = generate(messages, rest);
|
|
38
11
|
return text ? text.replace(/\*\*/g, '').replace(/^#+\s*/gm, '').trim() : null;
|
|
39
12
|
}
|
|
40
13
|
|
|
41
14
|
export function summarizeDocBody(bodyText, meta, opts = {}) {
|
|
42
15
|
if (!bodyText?.trim()) return null;
|
|
43
|
-
const prompt =
|
|
44
|
-
You write brief, direct summaries. No preamble. No filler. Start with the subject immediately.<|eot_id|><|start_header_id|>user<|end_header_id|>
|
|
45
|
-
Write a 2-3 sentence plain text summary of this document. State what it covers, its current state (${meta.status}), and what remains to be done. No markdown formatting. No bold, headers, or bullets. Do not start with "This document" or "Here is".
|
|
16
|
+
const prompt = `Write a 2-3 sentence plain text summary of this document. State what it covers, its current state (${meta.status}), and what remains to be done. No markdown formatting. No bold, headers, or bullets. Do not start with "This document" or "Here is".
|
|
46
17
|
|
|
47
18
|
Title: ${meta.title}
|
|
48
|
-
${bodyText.slice(0, 6000)}
|
|
49
|
-
|
|
50
|
-
|
|
19
|
+
${bodyText.slice(0, 6000)}`;
|
|
20
|
+
return runModel(prompt, {
|
|
21
|
+
system: 'You write brief, direct summaries. No preamble. No filler. Start with the subject immediately.',
|
|
22
|
+
maxTokens: 200,
|
|
23
|
+
...opts,
|
|
24
|
+
});
|
|
51
25
|
}
|
|
52
26
|
|
|
53
27
|
export function summarizeDiffText(diffText, filePath, model) {
|
|
54
28
|
const prompt = `Summarize this git diff in 1-2 sentences. Focus on what changed semantically, not line counts.\n\nFile: ${filePath}\n\n${diffText.slice(0, 4000)}`;
|
|
55
|
-
return
|
|
29
|
+
return runModel(prompt, { model, maxTokens: 150 });
|
|
56
30
|
}
|
package/src/baton.mjs
CHANGED
|
@@ -65,7 +65,7 @@ function pendingHandoffs(promptPath, planPath, config) {
|
|
|
65
65
|
function refusePendingHandoff(pending) {
|
|
66
66
|
const lines = pending.map(p => ` ${p}`).join('\n');
|
|
67
67
|
const slug = path.basename(pending[0], '.md');
|
|
68
|
-
die(`Nothing saved: a handoff for this is already pending:\n${lines}\
|
|
68
|
+
die(`Nothing saved: a handoff for this is already pending:\n${lines}\nInspect it with \`runlist prompts show ${slug}\`. If it is current, keep it and do not hand off again. If it is stale, run \`runlist prompts archive ${pending[0]}\`, then run baton again.`);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Is this positional a filesystem reference (must resolve, typos die) or a
|
package/src/body-link.mjs
CHANGED
|
@@ -9,12 +9,14 @@ function isWithin(root, candidate) {
|
|
|
9
9
|
// Markdown body links are filesystem paths relative to the document that
|
|
10
10
|
// contains them. They deliberately do not inherit frontmatter references'
|
|
11
11
|
// repo-root fallback: that fallback can make a broken Markdown link look valid.
|
|
12
|
-
export function resolveBodyLinkTarget(href, docDir, repoRoot) {
|
|
12
|
+
export function resolveBodyLinkTarget(href, docDir, repoRoot, externalRoots = []) {
|
|
13
13
|
if (!href) return { ok: false, reason: 'missing' };
|
|
14
14
|
|
|
15
15
|
const root = path.resolve(repoRoot);
|
|
16
16
|
const candidate = path.resolve(docDir, href);
|
|
17
|
-
|
|
17
|
+
const allowedRoots = externalRoots.map(entry => path.resolve(root, entry));
|
|
18
|
+
const allowedLexically = isWithin(root, candidate) || allowedRoots.some(entry => isWithin(entry, candidate));
|
|
19
|
+
if (!allowedLexically) return { ok: false, reason: 'outside-repo' };
|
|
18
20
|
if (!existsSync(candidate)) return { ok: false, reason: 'missing' };
|
|
19
21
|
|
|
20
22
|
let canonicalRoot;
|
|
@@ -25,7 +27,13 @@ export function resolveBodyLinkTarget(href, docDir, repoRoot) {
|
|
|
25
27
|
} catch {
|
|
26
28
|
return { ok: false, reason: 'unreadable' };
|
|
27
29
|
}
|
|
28
|
-
|
|
30
|
+
const canonicalAllowedRoots = allowedRoots.flatMap(entry => {
|
|
31
|
+
try { return [realpathSync(entry)]; } catch { return []; }
|
|
32
|
+
});
|
|
33
|
+
if (!isWithin(canonicalRoot, canonicalTarget)
|
|
34
|
+
&& !canonicalAllowedRoots.some(entry => isWithin(entry, canonicalTarget))) {
|
|
35
|
+
return { ok: false, reason: 'outside-repo' };
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
try {
|
|
31
39
|
const stat = statSync(canonicalTarget);
|
package/src/commands.mjs
CHANGED
|
@@ -88,11 +88,22 @@ const definitions = [
|
|
|
88
88
|
form('sync <check-name> [findings]', { subcommands: ['sync'], args: positionals(1, 2), dashPositionalsAfter: 1 }),
|
|
89
89
|
]),
|
|
90
90
|
command('glossary', none, 'read', [form('[term]', { args: positionals(0, 1), options: [flag('--list'), flag('--json')] })]),
|
|
91
|
+
command('model', mutates('~/.runlist/model.json and the local model server'), 'read', [
|
|
92
|
+
form('', { options: [flag('--json')] }),
|
|
93
|
+
form('status', { subcommands: ['status'], options: [flag('--json')] }),
|
|
94
|
+
form('start', { subcommands: ['start'], options: [flag('--force')] }),
|
|
95
|
+
form('stop', { subcommands: ['stop'], options: [flag('--all')] }),
|
|
96
|
+
form('measure [name...]', { subcommands: ['measure'], args: positionals(0, Infinity) }),
|
|
97
|
+
form('use <name>', { subcommands: ['use'], args: positionals(1, 1) }),
|
|
98
|
+
form('cap <gb>', { subcommands: ['cap'], args: positionals(1, 1) }),
|
|
99
|
+
]),
|
|
100
|
+
command('show', none, 'read', [form('<file...>', { args: positionals(1, Infinity), options: [flag('--json')] })]),
|
|
91
101
|
command('decisions', none, 'read', [form('[doc-or-id]', { args: positionals(0, 1), options: [flag('--all'), flag('--json'), flag('--check')] })]),
|
|
92
102
|
command('modules', none, 'read', [form('', { options: [value('--sort'), value('--limit'), flag('--all'), flag('--json')] })]),
|
|
93
103
|
command('module', none, 'read', [form('<name>', { args: positionals(1, 1), options: [value('--sort'), flag('--json')] })]),
|
|
94
104
|
command('surfaces', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
95
105
|
command('journal', none, 'read', [form('', { options: [value('--tail'), flag('--errors'), value('--session'), value('--since'), flag('--by-command'), flag('--json')] })]),
|
|
106
|
+
command('errors', none, 'read', [form('', { options: [flag('--json'), value('--limit'), value('--tail'), value('--repo')] })]),
|
|
96
107
|
command('misuse', none, 'read', [form('', { options: [flag('--json'), value('--tail'), flag('--by-rule'), value('--repo')] })]),
|
|
97
108
|
|
|
98
109
|
command('roadmap', mutates('managed source when `next`; otherwise read-only'), 'workflow', [
|
package/src/config.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const DEFAULTS = {
|
|
|
19
19
|
root: '.',
|
|
20
20
|
archiveDir: 'archived',
|
|
21
21
|
excludeDirs: [],
|
|
22
|
+
externalBodyLinkRoots: [],
|
|
22
23
|
// Floor under the scan surface; null = off. See `applyScanFloor` in validate.mjs.
|
|
23
24
|
minDocs: null,
|
|
24
25
|
|
|
@@ -640,6 +641,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
640
641
|
configFound: Boolean(configPath),
|
|
641
642
|
archiveDir: config.archiveDir,
|
|
642
643
|
excludeDirs: new Set(config.excludeDirs),
|
|
644
|
+
externalBodyLinkRoots: Array.isArray(config.externalBodyLinkRoots) ? config.externalBodyLinkRoots : [],
|
|
643
645
|
docsRootPrefix,
|
|
644
646
|
minDocs,
|
|
645
647
|
|