dotmd-cli 0.86.0 → 0.87.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 +59 -0
- package/bin/dotmd.mjs +103 -5
- package/package.json +1 -1
- package/runlist.config.example.mjs +1 -1
- package/src/ai.mjs +16 -42
- package/src/body-link.mjs +11 -3
- package/src/commands.mjs +12 -0
- package/src/config.mjs +2 -0
- package/src/decisions.mjs +752 -0
- package/src/diff.mjs +2 -2
- package/src/errors-read.mjs +30 -0
- package/src/fix-refs.mjs +12 -14
- 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/render.mjs +1 -9
- package/src/show.mjs +101 -0
- package/src/summary.mjs +1 -1
- package/src/validate.mjs +32 -39
package/README.md
CHANGED
|
@@ -259,6 +259,12 @@ The register block is the fenced block whose first line carries `statusLine`.
|
|
|
259
259
|
The row is the question plus `--answers` (what each answer leaves in place),
|
|
260
260
|
which is required when a register is configured.
|
|
261
261
|
|
|
262
|
+
`runlist decisions --json` gives each open or held decision its `question`,
|
|
263
|
+
its document's `docTitle`, and `blocks`: what its record says it blocks or
|
|
264
|
+
gates, then every unticked checklist item and frontmatter blocker that names
|
|
265
|
+
its id, alone or inside a written range (`A2 to A12`). In another document the
|
|
266
|
+
id has to sit beside a link to the decision's document.
|
|
267
|
+
|
|
262
268
|
## Flags
|
|
263
269
|
|
|
264
270
|
A flag is something someone found that the person should know about when they
|
|
@@ -282,6 +288,59 @@ merged. The log is append-only, `.runlist/flags.jsonl` by default
|
|
|
282
288
|
beside the flag, never over it. Every session start shows a count and the top
|
|
283
289
|
open flags.
|
|
284
290
|
|
|
291
|
+
## Local model
|
|
292
|
+
|
|
293
|
+
Summaries (`--summarize`, `runlist summary`, `runlist diff --summarize`) and
|
|
294
|
+
lint's status inference use one local model server, Ollama by default or any
|
|
295
|
+
OpenAI-compatible server. runlist never starts it or pulls a model on its own:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
runlist model # server, model, cap, free memory, what is loaded
|
|
299
|
+
runlist model start # ollama serve, one model and one request at a time
|
|
300
|
+
runlist model measure # record each pulled candidate's peak memory and speed here
|
|
301
|
+
runlist model use qwen3.5:9b # name a model; `auto` picks the first measured one under the cap
|
|
302
|
+
runlist model cap 8 # most memory a model may take; `auto` is a quarter of the machine
|
|
303
|
+
runlist model stop # unload it now
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The model unloads after 5 idle minutes. Before it loads, the memory free right
|
|
307
|
+
now must hold it plus 1.5 GB with memory pressure normal, or the command goes
|
|
308
|
+
on without it. On an 8 GB machine the cap is 2 GB and model features stay off;
|
|
309
|
+
point `RUNLIST_MODEL_ENDPOINT` at a server on a bigger machine instead. Settings
|
|
310
|
+
are per machine in `~/.runlist/model.json`.
|
|
311
|
+
|
|
312
|
+
`runlist model status --json` leads with a one-line reading for dashboards,
|
|
313
|
+
`running`, `name` (the model loaded now, else the one runlist would load) and
|
|
314
|
+
`memoryMb` (what it holds, null when nothing is loaded), followed by the full
|
|
315
|
+
detail.
|
|
316
|
+
|
|
317
|
+
## One document's card
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
runlist show docs/plans/shelf.md # title, status, next step, blockers, checklist, related, links
|
|
321
|
+
runlist show shelf bins --json # [{ path, title, status, nextStep, blockers, checklist,
|
|
322
|
+
# related: [{ field, path, title, status }], links: [...] }]
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Read-only: nothing is claimed. `related` is what the frontmatter names
|
|
326
|
+
(`related_plans`, `related_docs`, `supports_plans`, `parent_plan`, `runlist` and
|
|
327
|
+
the configured reference fields), each with its title and status; `links` is
|
|
328
|
+
every other document the body links to.
|
|
329
|
+
|
|
330
|
+
## Failed commands
|
|
331
|
+
|
|
332
|
+
Every runlist command that fails, by an error or a non-zero exit, appends one
|
|
333
|
+
line to `~/.claude/logs/runlist-errors.log` (`RUNLIST_ERROR_LOG_DIR` moves it)
|
|
334
|
+
with the time, the command with secrets redacted, and the error's one-line
|
|
335
|
+
message. It rolls over once, to `runlist-errors.log.1`, at 5 MB or a new
|
|
336
|
+
runlist version. Dry runs and the session-start `hud` are never logged.
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
runlist errors # the newest 20 failures, newest first
|
|
340
|
+
runlist errors --limit 50 # the newest N
|
|
341
|
+
runlist errors --json # [{ at, command, message, repo, exit }]
|
|
342
|
+
```
|
|
343
|
+
|
|
285
344
|
## Safety Model
|
|
286
345
|
|
|
287
346
|
- 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,7 +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
|
|
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
|
|
251
268
|
|
|
252
269
|
Validate & Fix:
|
|
253
270
|
doctor [--apply] Auto-fix everything: refs, membership, lint, long fields, dates, index (preview by default)
|
|
@@ -293,6 +310,7 @@ Setup:
|
|
|
293
310
|
completions <shell> Shell completion script (bash, zsh)
|
|
294
311
|
journal [--tail N|--errors|--by-command|--session id|--since iso|--json]
|
|
295
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
|
|
296
314
|
|
|
297
315
|
Global Options:
|
|
298
316
|
--config <path> Explicit config file path
|
|
@@ -1095,7 +1113,7 @@ Options:
|
|
|
1095
1113
|
Generates an AI-powered summary using a local model.
|
|
1096
1114
|
|
|
1097
1115
|
Options:
|
|
1098
|
-
--model <name> Model to use (default:
|
|
1116
|
+
--model <name> Model to use (default: the one \`runlist model\` shows)
|
|
1099
1117
|
--max-tokens <n> Max tokens for generation (default: 200)
|
|
1100
1118
|
--json Output as JSON`,
|
|
1101
1119
|
|
|
@@ -1108,7 +1126,7 @@ Options:
|
|
|
1108
1126
|
--stat Summary only (files changed, insertions/deletions)
|
|
1109
1127
|
--since <date> Override: diff since this date instead of frontmatter
|
|
1110
1128
|
--summarize Generate AI summary using local model
|
|
1111
|
-
--model <name> Model to use (default:
|
|
1129
|
+
--model <name> Model to use (default: the one \`runlist model\` shows)`,
|
|
1112
1130
|
|
|
1113
1131
|
lint: `runlist lint [--fix] — check and auto-fix frontmatter issues
|
|
1114
1132
|
|
|
@@ -1329,6 +1347,70 @@ plans, and checklist progress. Plans-only view.
|
|
|
1329
1347
|
Options:
|
|
1330
1348
|
--json Output as JSON`,
|
|
1331
1349
|
|
|
1350
|
+
decisions: `runlist decisions — the open and held decisions, read from the corpus
|
|
1351
|
+
|
|
1352
|
+
runlist decisions every open and held decision, one line each
|
|
1353
|
+
runlist decisions <doc> one document, by path, filename or slug
|
|
1354
|
+
runlist decisions <ID> one record, whole, as it is written
|
|
1355
|
+
runlist decisions --all every disposition, not only the pending ones
|
|
1356
|
+
runlist decisions --json the same rows as data, each with its question, its
|
|
1357
|
+
document's title and what it blocks
|
|
1358
|
+
runlist decisions --check every item it could not read or that is incomplete; exit 1 if any
|
|
1359
|
+
|
|
1360
|
+
A decision is an item in a decisions section (a heading naming the section word
|
|
1361
|
+
in its first 4 words): a heading, bold lead, bullet or table row whose first
|
|
1362
|
+
token is an id. A register is a fenced block whose first line carries
|
|
1363
|
+
decisions.register.statusLine; its rows are \`ID text\`. A disposition is read
|
|
1364
|
+
from a \`Disposition:\` line or a row's opening word, then from prose when
|
|
1365
|
+
decisions.prose is on, then from the block or bold lead above it. Configure it
|
|
1366
|
+
with \`export const decisions = { ... }\` (see src/decisions.mjs).
|
|
1367
|
+
|
|
1368
|
+
What a decision blocks, in --json as \`blocks: [{ doc, line, kind, section, text }]\`:
|
|
1369
|
+
what its record says it blocks or gates (kind "stated"), and every unticked
|
|
1370
|
+
checklist item (kind "item") or frontmatter blocker (kind "blocker") that
|
|
1371
|
+
names its id, alone or inside a written range such as \`A2 to A12\`. In
|
|
1372
|
+
another document the id has to sit beside a link to the decision's document.`,
|
|
1373
|
+
|
|
1374
|
+
show: `runlist show <file...> — one document's card, read-only
|
|
1375
|
+
|
|
1376
|
+
runlist show <file> title, status, next step, blockers, checklist,
|
|
1377
|
+
related plans and docs, documents linked from the body
|
|
1378
|
+
runlist show <a> <b> --json [{ path, type, title, status, summary, currentState,
|
|
1379
|
+
nextStep, blockers, checklist, updated,
|
|
1380
|
+
related: [{ field, ref, path, exists, title, status, type }],
|
|
1381
|
+
links: [{ path, title, status, type }] }]
|
|
1382
|
+
|
|
1383
|
+
A file is a path, filename or slug, as for \`use\`. Nothing is claimed or
|
|
1384
|
+
written. With --json a file that is not found is { path, error } rather than
|
|
1385
|
+
an exit.`,
|
|
1386
|
+
|
|
1387
|
+
model: `runlist model — the local model behind summaries and lint
|
|
1388
|
+
|
|
1389
|
+
One local server (Ollama by default) holds one model, serves one request at a
|
|
1390
|
+
time and unloads it after it sits idle. runlist never starts the server or
|
|
1391
|
+
pulls a model on its own.
|
|
1392
|
+
|
|
1393
|
+
Subcommands:
|
|
1394
|
+
status [--json] Default. Server, model, cap, what is loaded and its memory
|
|
1395
|
+
start [--force] Start \`ollama serve\` (one model, one request at a time);
|
|
1396
|
+
refused where no model fits unless forced
|
|
1397
|
+
stop [--all] Unload runlist's model now; --all unloads every loaded model
|
|
1398
|
+
measure [name...] Load each pulled candidate (or the named ones) in turn,
|
|
1399
|
+
summarise the corpus's largest documents at lower
|
|
1400
|
+
priority, and record peak memory and speed for this
|
|
1401
|
+
machine; a model is picked only once measured here
|
|
1402
|
+
use <name|auto> Name the model; auto takes the first measured candidate
|
|
1403
|
+
under the cap
|
|
1404
|
+
cap <gb|auto> The most memory a model may take; auto (the default) is a
|
|
1405
|
+
quarter of this machine's memory, at most 12 GB
|
|
1406
|
+
|
|
1407
|
+
Before a model loads here, the memory free now must hold it plus 1.5 GB and
|
|
1408
|
+
memory pressure must be normal; otherwise the command goes on without it.
|
|
1409
|
+
|
|
1410
|
+
Settings live in ~/.runlist/model.json. RUNLIST_MODEL, RUNLIST_MODEL_CAP_GB and
|
|
1411
|
+
RUNLIST_MODEL_ENDPOINT override them; \`runtime: "openai"\` there points at any
|
|
1412
|
+
OpenAI-compatible server (mlx_lm.server, llama-server, LM Studio).`,
|
|
1413
|
+
|
|
1332
1414
|
glossary: `runlist glossary <term> — look up domain terms and related docs
|
|
1333
1415
|
|
|
1334
1416
|
Searches the glossary table in your docs for matching terms.
|
|
@@ -1632,7 +1714,8 @@ async function main() {
|
|
|
1632
1714
|
if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
|
|
1633
1715
|
if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
|
|
1634
1716
|
process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: runlist <cmd> --help\n`);
|
|
1635
|
-
process.
|
|
1717
|
+
process.exitCode = 1;
|
|
1718
|
+
return;
|
|
1636
1719
|
}
|
|
1637
1720
|
process.stdout.write(`${HELP._main}\n`);
|
|
1638
1721
|
return;
|
|
@@ -1898,6 +1981,9 @@ async function main() {
|
|
|
1898
1981
|
if (command === 'flags') { const { runFlags } = await import('../src/flags.mjs'); runFlags(restArgs, config); return; }
|
|
1899
1982
|
if (command === 'flag') { const { runFlag } = await import('../src/flags.mjs'); runFlag(restArgs, config); return; }
|
|
1900
1983
|
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
1984
|
+
if (command === 'model') { const { runModel } = await import('../src/model.mjs'); await runModel(restArgs, config); return; }
|
|
1985
|
+
if (command === 'show') { const { runShow } = await import('../src/show.mjs'); runShow(restArgs, config); return; }
|
|
1986
|
+
if (command === 'decisions') { const { runDecisions } = await import('../src/decisions.mjs'); runDecisions(restArgs, config); return; }
|
|
1901
1987
|
if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
1902
1988
|
|
|
1903
1989
|
// Lifecycle commands
|
|
@@ -1911,6 +1997,7 @@ async function main() {
|
|
|
1911
1997
|
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config, { dryRun }); return; }
|
|
1912
1998
|
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config, { dryRun }); return; }
|
|
1913
1999
|
if (command === 'install') { const { runInstall } = await import('../src/install.mjs'); runInstall(restArgs, config, { dryRun }); return; }
|
|
2000
|
+
if (command === 'errors') { const { runErrors } = await import('../src/errors-read.mjs'); runErrors(restArgs); return; }
|
|
1914
2001
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
|
1915
2002
|
if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
|
|
1916
2003
|
if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
|
|
@@ -2343,19 +2430,30 @@ function _journalExit(err) {
|
|
|
2343
2430
|
version: pkg.version,
|
|
2344
2431
|
});
|
|
2345
2432
|
} catch { /* never break exit on journal failure */ }
|
|
2346
|
-
|
|
2433
|
+
// A command that reports its own failure through the exit code (check with
|
|
2434
|
+
// errors, a failed update step) is a failure too.
|
|
2435
|
+
const code = Number(process.exitCode ?? 0);
|
|
2436
|
+
const failure = err ?? (code !== 0 ? { name: 'ExitStatus', message: `exited with status ${code}` } : null);
|
|
2437
|
+
if (failure) {
|
|
2347
2438
|
try {
|
|
2348
2439
|
recordGlobalError({
|
|
2349
2440
|
config: _resolvedConfig,
|
|
2350
2441
|
startMs: _startMs,
|
|
2351
2442
|
args: _invocationArgs,
|
|
2352
|
-
err,
|
|
2443
|
+
err: failure,
|
|
2353
2444
|
version: pkg.version,
|
|
2354
2445
|
});
|
|
2355
2446
|
} catch { /* never break exit on error-log failure */ }
|
|
2356
2447
|
}
|
|
2357
2448
|
}
|
|
2358
2449
|
|
|
2450
|
+
// A reader that stops early (`runlist flags | head`) closes the pipe; the rest
|
|
2451
|
+
// of the output has nowhere to go, which is not a failure.
|
|
2452
|
+
process.stdout.on('error', err => {
|
|
2453
|
+
if (err.code === 'EPIPE') process.exit(0);
|
|
2454
|
+
throw err;
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2359
2457
|
main()
|
|
2360
2458
|
.then(() => { _journalExit(null); })
|
|
2361
2459
|
.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/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,10 +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')] })]),
|
|
101
|
+
command('decisions', none, 'read', [form('[doc-or-id]', { args: positionals(0, 1), options: [flag('--all'), flag('--json'), flag('--check')] })]),
|
|
91
102
|
command('modules', none, 'read', [form('', { options: [value('--sort'), value('--limit'), flag('--all'), flag('--json')] })]),
|
|
92
103
|
command('module', none, 'read', [form('<name>', { args: positionals(1, 1), options: [value('--sort'), flag('--json')] })]),
|
|
93
104
|
command('surfaces', none, 'read', [form('', { options: [flag('--json')] })]),
|
|
94
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')] })]),
|
|
95
107
|
command('misuse', none, 'read', [form('', { options: [flag('--json'), value('--tail'), flag('--by-rule'), value('--repo')] })]),
|
|
96
108
|
|
|
97
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
|
|