runlist 0.97.0 → 0.98.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 +10 -7
- package/bin/runlist.mjs +10 -0
- package/package.json +1 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/runlist-codex/skills/runlist/SKILL.md +1 -0
- package/src/brief.mjs +38 -9
- package/src/show.mjs +52 -6
- package/src/use.mjs +14 -3
package/README.md
CHANGED
|
@@ -71,8 +71,8 @@ controls are experimental; live retrieval depends on a compatible external
|
|
|
71
71
|
provider. Local chat and push are deferred.
|
|
72
72
|
|
|
73
73
|
<!-- runlist:release:start -->
|
|
74
|
-
CLI and desktop release version: **0.
|
|
75
|
-
[Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.
|
|
74
|
+
CLI and desktop release version: **0.98.1**.
|
|
75
|
+
[Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.98.1).
|
|
76
76
|
Normal product releases publish both components; an installed Mac app is updated with rollback after it has quit.
|
|
77
77
|
<!-- runlist:release:end -->
|
|
78
78
|
|
|
@@ -105,11 +105,14 @@ Host source, cache and registry copies are labelled separately; a cache entry
|
|
|
105
105
|
alone does not prove which copy a running host has loaded. No package manager or
|
|
106
106
|
host command runs to produce the `doctor --project` installation report.
|
|
107
107
|
|
|
108
|
-
**0.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
**0.98.0 is published and installed locally.** `runlist show` briefs a plan
|
|
109
|
+
(outline, open boxes and `--section` reads with line numbers), marks each
|
|
110
|
+
phase's tally and the active phase, and groups open boxes by phase;
|
|
111
|
+
`runlist use <doc>` briefs a doc over 500 lines. The agent workflow says to
|
|
112
|
+
read plans that way. The desktop app's own code is unchanged from 0.96.1; it
|
|
113
|
+
bundles the 0.98.0 engine. The Mac installer is signed, notarized and verified; installation
|
|
114
|
+
retained the previous app for rollback. Open it with `rl desktop`. See the
|
|
115
|
+
[release validation](desktop/VALIDATION.md#current-mac-release--2026-10-22)
|
|
113
116
|
for checks and evidence. Full-app dialog accessibility remains an open finding;
|
|
114
117
|
fresh/minimum-host and controlled whole-app performance qualification remain
|
|
115
118
|
follow-up work.
|
package/bin/runlist.mjs
CHANGED
|
@@ -263,6 +263,7 @@ View & Query:
|
|
|
263
263
|
plans Live plans (excludes archived; --include-archived for all)
|
|
264
264
|
app [document] [--port 5173] Local Markdown document library with reviewed saves and draft recovery
|
|
265
265
|
use [<file-or-slug>] Open a doc by type: prompt → consume, plan → start, doc → read
|
|
266
|
+
(a doc over 500 lines prints its brief; --full prints it whole)
|
|
266
267
|
baton [<plan>|<slug>] @<draft> Save a resume prompt; releases the plan + prints the commit when one is in-session
|
|
267
268
|
prompts [list|show|archive|new] Prompt admin (list / peek / archive / save). Use \`runlist use\` to consume.
|
|
268
269
|
stale Stale docs (preset)
|
|
@@ -1593,6 +1594,15 @@ substring; other matches are named. A section read stops at 500 lines or
|
|
|
1593
1594
|
60 KB and prints the command that continues it. Headings and boxes inside code
|
|
1594
1595
|
fences are ignored. Several files at once get a shorter outline each.
|
|
1595
1596
|
|
|
1597
|
+
A phase heading shows its own boxes as [done/total], → marks the active phase
|
|
1598
|
+
(the one \`use\` would point at), and a marker its boxes contradict is named
|
|
1599
|
+
(marked shipped with boxes open, or todo with every box checked). Open boxes
|
|
1600
|
+
are grouped by phase, and the last line is the --section call for the active
|
|
1601
|
+
phase.
|
|
1602
|
+
|
|
1603
|
+
\`runlist use <doc>\` prints this brief for a doc over 500 lines or 60 KB;
|
|
1604
|
+
\`use <doc> --full\` prints the whole body.
|
|
1605
|
+
|
|
1596
1606
|
A file is a path, filename or slug, as for \`use\`, and any Markdown file in
|
|
1597
1607
|
the repo works. Nothing is claimed or written. With --json a file that is not
|
|
1598
1608
|
found is { path, error } rather than an exit; a heading that matches nothing
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ Use this skill in repositories with `runlist.config.mjs` or `dotmd.config.mjs`.
|
|
|
12
12
|
**Optional filing policy:** with `export const filing = true` (or `{ categoryDepth: 3 }`), `check` warns on missing/multiple/uncategorized homes, while `coverage` lists direct/inherited homes and hub:line evidence. A home is a status-bearing table row whose first cell links the plan; mentions and order lists are references. A child without its own row can inherit a home through explicit `parent_plan:` ancestors. Categories come from headings or the containing hub's filing category. Scoping retains full evidence; incomplete scans suppress absence findings. Neither command assigns homes or repairs rows. Enable the policy or retire an older custom check only with repository authorization. Available in CLI 0.93.0.
|
|
13
13
|
|
|
14
14
|
- Orient with `runlist plans`. Use `runlist agent-context` for structured state; `runlist briefing` is the comprehensive view and can be large.
|
|
15
|
+
- Read a plan or long doc with `runlist show <file>` (card, outline and open boxes with line numbers, the active phase), then `runlist show <file> --section "<heading>"` one section at a time; never read a plan whole.
|
|
15
16
|
- Start plan work with `runlist use <plan-file>`. It claims the plan, marks it `in-session`, and prints the card.
|
|
16
17
|
- Change status with `runlist set <status> <file> [--note "why"]`; this runs validation, lifecycle hooks, ref repairs, and index updates. Never hand-edit a `status:` field.
|
|
17
18
|
- Close a shipped plan with `runlist archive <file>`; use `partial`, `active`, `awaiting`, or `blocked` when those match the actual state. Valid statuses are repository and type specific: `runlist statuses list --type plan`.
|
package/src/brief.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractFrontmatter, normalizeEol } from './frontmatter.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import { detectMarker, findActivePhase, isPhaseHeading, matchSections, phaseMarkerConflict, phaseTally, walkChecklist, walkSections } from './section.mjs';
|
|
3
3
|
|
|
4
4
|
// A document in a few KB, for an agent to read in place of the whole file: a
|
|
5
5
|
// heading outline and the open checklist boxes, both with file line numbers,
|
|
@@ -16,7 +16,7 @@ export const LIMITS = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
// Several files at once is a survey; each one gets less.
|
|
19
|
-
export const SURVEY_LIMITS = { ...LIMITS, outline: 20, boxes: 10 };
|
|
19
|
+
export const SURVEY_LIMITS = { ...LIMITS, outline: 20, boxes: 10, listItems: 4 };
|
|
20
20
|
|
|
21
21
|
export const cut = (text, width = LIMITS.width) =>
|
|
22
22
|
text.length > width ? `${text.slice(0, width - 3)}...` : text;
|
|
@@ -30,13 +30,39 @@ function fileLines(text) {
|
|
|
30
30
|
|
|
31
31
|
// Headings with file coordinates: `line` is the heading, `end` the last line of
|
|
32
32
|
// its section (the line before the next heading at the same or a higher level).
|
|
33
|
+
// A phase heading also carries `phase`: its marker, its own checklist tally,
|
|
34
|
+
// whether it is the active phase (the rule the `use` pickup card applies), and
|
|
35
|
+
// a contradiction between marker and boxes when there is one.
|
|
33
36
|
function headingsOf(body, bodyLineOffset, lastLine) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const sections = walkSections(body);
|
|
38
|
+
const active = findActivePhase(sections);
|
|
39
|
+
return sections.map(s => {
|
|
40
|
+
const h = {
|
|
41
|
+
line: s.lineStart + bodyLineOffset,
|
|
42
|
+
end: Math.min(s.lineEnd + bodyLineOffset, lastLine),
|
|
43
|
+
level: s.level,
|
|
44
|
+
heading: s.heading,
|
|
45
|
+
};
|
|
46
|
+
if (isPhaseHeading(s)) {
|
|
47
|
+
const tally = phaseTally(s);
|
|
48
|
+
const conflict = phaseMarkerConflict(s);
|
|
49
|
+
h.phase = {
|
|
50
|
+
marker: detectMarker(s.heading),
|
|
51
|
+
done: tally.checked,
|
|
52
|
+
total: tally.total,
|
|
53
|
+
active: s === active,
|
|
54
|
+
conflict: conflict ? { declared: conflict.declared, implied: conflict.implied } : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return h;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The phase heading a body line sits under, if any.
|
|
62
|
+
function phaseAt(headings, line) {
|
|
63
|
+
let found = null;
|
|
64
|
+
for (const h of headings) if (h.phase && h.line <= line && line <= h.end) found = h;
|
|
65
|
+
return found;
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
// Drop the deepest heading levels until the outline fits, then cut the tail.
|
|
@@ -91,7 +117,9 @@ export function buildBrief(raw, { all = false, limits = LIMITS } = {}) {
|
|
|
91
117
|
const headings = headingsOf(body, bodyLineOffset, lines.length);
|
|
92
118
|
const { shown, omitted } = boundOutline(headings, limits.outline);
|
|
93
119
|
const boxes = walkChecklist(body).map(b => ({ ...b, line: b.line + bodyLineOffset }));
|
|
94
|
-
const open = boxes.filter(b => b.open)
|
|
120
|
+
const open = boxes.filter(b => b.open)
|
|
121
|
+
.map(({ line, text: t }) => ({ line, text: t, phase: phaseAt(headings, line)?.heading ?? null }));
|
|
122
|
+
const active = headings.find(h => h.phase?.active) ?? null;
|
|
95
123
|
const listed = all ? open : open.slice(0, limits.boxes);
|
|
96
124
|
return {
|
|
97
125
|
lines: lines.length,
|
|
@@ -99,6 +127,7 @@ export function buildBrief(raw, { all = false, limits = LIMITS } = {}) {
|
|
|
99
127
|
frontmatterLines: frontmatter ? bodyLineOffset : 0,
|
|
100
128
|
outline: shown,
|
|
101
129
|
outlineOmitted: omitted,
|
|
130
|
+
activePhase: active ? { line: active.line, heading: active.heading } : null,
|
|
102
131
|
boxes: { open: open.length, done: boxes.length - open.length },
|
|
103
132
|
open: listed,
|
|
104
133
|
openOmitted: open.length - listed.length,
|
package/src/show.mjs
CHANGED
|
@@ -79,6 +79,9 @@ export function showDoc(abs, config) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// A long next step is cut in the brief, as the pickup card cuts it.
|
|
83
|
+
const NEXT_STEP_WIDTH = 300;
|
|
84
|
+
|
|
82
85
|
// Fields the text card prints. Everything else non-empty is named on one line
|
|
83
86
|
// so a reader knows it is there without printing the frontmatter.
|
|
84
87
|
const CARD_FIELDS = new Set(['title', 'status', 'next_step', 'blockers']);
|
|
@@ -150,18 +153,45 @@ function runSection(target, opts, config) {
|
|
|
150
153
|
process.stdout.write(out.join('\n') + '\n');
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
// `[3/7]` for a phase with boxes, and the contradiction between its marker and
|
|
157
|
+
// its boxes when there is one.
|
|
158
|
+
function phaseNote(phase) {
|
|
159
|
+
if (!phase) return '';
|
|
160
|
+
let note = phase.total ? ` [${phase.done}/${phase.total}]` : '';
|
|
161
|
+
if (phase.conflict) {
|
|
162
|
+
note += phase.conflict.declared === 'shipped'
|
|
163
|
+
? ` (marked shipped; ${phase.total - phase.done} open)`
|
|
164
|
+
: ' (marked todo; every box checked)';
|
|
165
|
+
}
|
|
166
|
+
return note;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A heading as a --section query: trailing markers dropped, which the matcher
|
|
170
|
+
// ignores anyway, so the suggested command reads cleanly.
|
|
171
|
+
const sectionQuery = heading => heading.replace(/[^\w\s]+$/u, '').trim() || heading;
|
|
172
|
+
|
|
153
173
|
function renderBrief(card, brief) {
|
|
154
174
|
const out = [];
|
|
155
175
|
const width = String(brief.lines).length;
|
|
156
176
|
const kb = Math.max(1, Math.round(brief.bytes / 1024));
|
|
157
177
|
out.push('', `Outline: ${brief.lines.toLocaleString('en-US')} lines, ${kb} KB (line, heading)`);
|
|
158
178
|
if (!brief.outline.length) out.push(' (no headings)');
|
|
159
|
-
for (const h of brief.outline)
|
|
179
|
+
for (const h of brief.outline) {
|
|
180
|
+
const gutter = h.phase?.active ? '→ ' : ' ';
|
|
181
|
+
out.push(`${gutter}${pad(h.line, width)} ${cut(`${'#'.repeat(h.level)} ${h.heading}`)}${phaseNote(h.phase)}`);
|
|
182
|
+
}
|
|
160
183
|
if (brief.outlineOmitted) out.push(` (+${brief.outlineOmitted} deeper headings not shown; a --section read shows them)`);
|
|
161
184
|
const total = brief.boxes.open + brief.boxes.done;
|
|
162
185
|
if (total) {
|
|
163
186
|
out.push(`Open boxes: ${brief.boxes.open} of ${total} (line, text)`);
|
|
164
|
-
|
|
187
|
+
// Grouped under the phase each box sits in, so the list reads as what is
|
|
188
|
+
// left, phase by phase.
|
|
189
|
+
let group;
|
|
190
|
+
for (const b of brief.open) {
|
|
191
|
+
if (b.phase !== group && (b.phase || group)) out.push(` ${b.phase ? cut(b.phase) : '(not in a phase)'}`);
|
|
192
|
+
group = b.phase;
|
|
193
|
+
out.push(`${group ? ' ' : ' '}${pad(b.line, width)} ${cut(b.text)}`);
|
|
194
|
+
}
|
|
165
195
|
if (brief.openOmitted) out.push(` (+${brief.openOmitted} more: add --all)`);
|
|
166
196
|
}
|
|
167
197
|
if (brief.frontmatter) {
|
|
@@ -201,17 +231,33 @@ export function runShow(args, config) {
|
|
|
201
231
|
cards.forEach((c, n) => {
|
|
202
232
|
if (n) process.stdout.write('\n');
|
|
203
233
|
const out = [`${c.title} (${c.status ?? 'no status'})`, c.path];
|
|
204
|
-
if (c.nextStep) out.push(`Next: ${c.nextStep}`);
|
|
234
|
+
if (c.nextStep) out.push(`Next: ${c.brief ? cut(c.nextStep, NEXT_STEP_WIDTH) : c.nextStep}`);
|
|
205
235
|
for (const b of c.blockers) out.push(`Blocker: ${b}`);
|
|
206
236
|
// The brief's open-box line carries the same count, read outside code fences.
|
|
207
237
|
if (c.checklist?.total && !c.brief) out.push(`Checklist: ${c.checklist.completed} of ${c.checklist.total} done`);
|
|
208
|
-
|
|
209
|
-
|
|
238
|
+
// With the brief, each related field and the body links show their first
|
|
239
|
+
// few entries and a count: a hub's dozens of related plans outweighed the
|
|
240
|
+
// brief itself. --card and --json list every one.
|
|
241
|
+
const cap = c.brief ? limits.listItems : Infinity;
|
|
242
|
+
const perField = new Map();
|
|
243
|
+
for (const r of c.related) {
|
|
244
|
+
const n = (perField.get(r.field) ?? 0) + 1;
|
|
245
|
+
perField.set(r.field, n);
|
|
246
|
+
if (n <= cap) out.push(`${r.field}: ${r.path ?? `${r.ref} (missing)`}${r.status ? ` (${r.status})` : ''}`);
|
|
247
|
+
}
|
|
248
|
+
for (const [field, n] of perField) if (n > cap) out.push(`${field}: +${n - cap} more (--card lists all)`);
|
|
249
|
+
if (c.links.length) {
|
|
250
|
+
const shown = c.links.slice(0, cap).map(l => l.path).join(', ');
|
|
251
|
+
out.push(`Linked from the body: ${shown}${c.links.length > cap ? ` (+${c.links.length - cap} more)` : ''}`);
|
|
252
|
+
}
|
|
210
253
|
if (c.brief) out.push(...renderBrief(c, c.brief));
|
|
211
254
|
process.stdout.write(out.join('\n') + '\n');
|
|
212
255
|
});
|
|
213
256
|
if (!opts.card) {
|
|
257
|
+
const active = cards.length === 1 ? cards[0].brief?.activePhase : null;
|
|
214
258
|
const file = cards.length === 1 ? shellQuote(cards[0].path) : '<file>';
|
|
215
|
-
process.stdout.write(
|
|
259
|
+
process.stdout.write(active
|
|
260
|
+
? `\nNext: runlist show ${file} --section ${shellQuote(sectionQuery(active.heading))}\n`
|
|
261
|
+
: `\nOne section: runlist show ${file} --section "<heading>"\n`);
|
|
216
262
|
}
|
|
217
263
|
}
|
package/src/use.mjs
CHANGED
|
@@ -5,13 +5,15 @@ import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
|
|
|
5
5
|
import { consumePrompt, pendingPromptsOldestFirst, resolvePromptInput } from './prompts.mjs';
|
|
6
6
|
import { startPlan } from './lifecycle.mjs';
|
|
7
7
|
import { resolveDocArg } from './index.mjs';
|
|
8
|
+
import { LIMITS } from './brief.mjs';
|
|
9
|
+
import { runShow } from './show.mjs';
|
|
8
10
|
|
|
9
11
|
// Top-level `runlist use [file]` — the single "start engaging with this doc"
|
|
10
12
|
// verb. Dispatches by the target doc's `type:` so agents don't have to know
|
|
11
13
|
// the verb-per-type rule:
|
|
12
14
|
// - prompt → print body + archive (one-shot consume)
|
|
13
15
|
// - plan → set status in-session + print the plan card
|
|
14
|
-
// - doc → print body (read-only)
|
|
16
|
+
// - doc → print body (read-only); a long doc prints its brief, --full the body
|
|
15
17
|
// With no argument, consumes the oldest pending prompt.
|
|
16
18
|
export async function runUse(argv, config, opts = {}) {
|
|
17
19
|
const positional = argv.find(a => !a.startsWith('-'));
|
|
@@ -51,8 +53,17 @@ export async function runUse(argv, config, opts = {}) {
|
|
|
51
53
|
}
|
|
52
54
|
// Anything else (doc, untyped, custom): print the body. The frontmatter
|
|
53
55
|
// already names everything an agent needs to know about lifecycle for that
|
|
54
|
-
// type, so the verb stays a no-op read for non-actionable types.
|
|
56
|
+
// type, so the verb stays a no-op read for non-actionable types. A doc past
|
|
57
|
+
// the section-read caps prints its brief instead: printing a 2,000-line spec
|
|
58
|
+
// whole is the read `runlist show` exists to replace.
|
|
59
|
+
const rel = toRepoPath(filePath, config.repoRoot);
|
|
60
|
+
const lines = raw.split('\n').length - (raw.endsWith('\n') ? 1 : 0);
|
|
61
|
+
if (!argv.includes('--full') && (lines > LIMITS.sectionLines || Buffer.byteLength(raw) > LIMITS.sectionBytes)) {
|
|
62
|
+
runShow([rel], config);
|
|
63
|
+
process.stderr.write(`${rel} (${type ?? 'untyped'}) is ${lines} lines, so this is its brief. \`runlist use ${rel} --full\` prints it whole.\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
55
66
|
process.stdout.write(raw);
|
|
56
67
|
if (!raw.endsWith('\n')) process.stdout.write('\n');
|
|
57
|
-
process.stderr.write(`${
|
|
68
|
+
process.stderr.write(`${rel} (${type ?? 'untyped'})\n`);
|
|
58
69
|
}
|