runlist 0.97.0 → 0.98.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 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.97.0**.
75
- [Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.97.0).
74
+ CLI and desktop release version: **0.98.0**.
75
+ [Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.98.0).
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,13 @@ 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.96.1 is published and installed locally**, including the desktop usability,
109
- draft recovery, handoff and failed-status-preview confirmation fixes. The Mac
110
- installer is signed, notarized and verified; installation retained the previous
111
- app for rollback. Open it with `rl desktop`. See the
112
- [release validation](desktop/VALIDATION.md#current-mac-release--2026-10-21)
108
+ **0.97.0 is published and installed locally.** It adds the `runlist show`
109
+ brief (outline, open boxes and `--section` reads with line numbers), counts
110
+ checklists outside code fences only, and trims session and subagent start-up
111
+ context. The desktop app's own code is unchanged from 0.96.1; it bundles the
112
+ 0.97.0 engine. The Mac installer is signed, notarized and verified; installation
113
+ retained the previous app for rollback. Open it with `rl desktop`. See the
114
+ [release validation](desktop/VALIDATION.md#current-mac-release--2026-10-22)
113
115
  for checks and evidence. Full-app dialog accessibility remains an open finding;
114
116
  fresh/minimum-host and controlled whole-app performance qualification remain
115
117
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlist",
3
- "version": "0.97.0",
3
+ "version": "0.98.0",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlist-codex",
3
- "version": "0.97.0",
3
+ "version": "0.98.0",
4
4
  "description": "Runlist workflow skill, session orientation, and tool guard for Codex",
5
5
  "author": {
6
6
  "name": "Reo Owens"
@@ -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 { walkSections, matchSections, walkChecklist } from './section.mjs';
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,
@@ -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
- return walkSections(body).map(s => ({
35
- line: s.lineStart + bodyLineOffset,
36
- end: Math.min(s.lineEnd + bodyLineOffset, lastLine),
37
- level: s.level,
38
- heading: s.heading,
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).map(({ line, text: t }) => ({ line, text: t }));
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
@@ -150,18 +150,45 @@ function runSection(target, opts, config) {
150
150
  process.stdout.write(out.join('\n') + '\n');
151
151
  }
152
152
 
153
+ // `[3/7]` for a phase with boxes, and the contradiction between its marker and
154
+ // its boxes when there is one.
155
+ function phaseNote(phase) {
156
+ if (!phase) return '';
157
+ let note = phase.total ? ` [${phase.done}/${phase.total}]` : '';
158
+ if (phase.conflict) {
159
+ note += phase.conflict.declared === 'shipped'
160
+ ? ` (marked shipped; ${phase.total - phase.done} open)`
161
+ : ' (marked todo; every box checked)';
162
+ }
163
+ return note;
164
+ }
165
+
166
+ // A heading as a --section query: trailing markers dropped, which the matcher
167
+ // ignores anyway, so the suggested command reads cleanly.
168
+ const sectionQuery = heading => heading.replace(/[^\w\s]+$/u, '').trim() || heading;
169
+
153
170
  function renderBrief(card, brief) {
154
171
  const out = [];
155
172
  const width = String(brief.lines).length;
156
173
  const kb = Math.max(1, Math.round(brief.bytes / 1024));
157
174
  out.push('', `Outline: ${brief.lines.toLocaleString('en-US')} lines, ${kb} KB (line, heading)`);
158
175
  if (!brief.outline.length) out.push(' (no headings)');
159
- for (const h of brief.outline) out.push(` ${pad(h.line, width)} ${cut(`${'#'.repeat(h.level)} ${h.heading}`)}`);
176
+ for (const h of brief.outline) {
177
+ const gutter = h.phase?.active ? '→ ' : ' ';
178
+ out.push(`${gutter}${pad(h.line, width)} ${cut(`${'#'.repeat(h.level)} ${h.heading}`)}${phaseNote(h.phase)}`);
179
+ }
160
180
  if (brief.outlineOmitted) out.push(` (+${brief.outlineOmitted} deeper headings not shown; a --section read shows them)`);
161
181
  const total = brief.boxes.open + brief.boxes.done;
162
182
  if (total) {
163
183
  out.push(`Open boxes: ${brief.boxes.open} of ${total} (line, text)`);
164
- for (const b of brief.open) out.push(` ${pad(b.line, width)} ${cut(b.text)}`);
184
+ // Grouped under the phase each box sits in, so the list reads as what is
185
+ // left, phase by phase.
186
+ let group;
187
+ for (const b of brief.open) {
188
+ if (b.phase !== group && (b.phase || group)) out.push(` ${b.phase ? cut(b.phase) : '(not in a phase)'}`);
189
+ group = b.phase;
190
+ out.push(`${group ? ' ' : ' '}${pad(b.line, width)} ${cut(b.text)}`);
191
+ }
165
192
  if (brief.openOmitted) out.push(` (+${brief.openOmitted} more: add --all)`);
166
193
  }
167
194
  if (brief.frontmatter) {
@@ -211,7 +238,10 @@ export function runShow(args, config) {
211
238
  process.stdout.write(out.join('\n') + '\n');
212
239
  });
213
240
  if (!opts.card) {
241
+ const active = cards.length === 1 ? cards[0].brief?.activePhase : null;
214
242
  const file = cards.length === 1 ? shellQuote(cards[0].path) : '<file>';
215
- process.stdout.write(`\nOne section: runlist show ${file} --section "<heading>"\n`);
243
+ process.stdout.write(active
244
+ ? `\nNext: runlist show ${file} --section ${shellQuote(sectionQuery(active.heading))}\n`
245
+ : `\nOne section: runlist show ${file} --section "<heading>"\n`);
216
246
  }
217
247
  }
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(`${toRepoPath(filePath, config.repoRoot)} (${type ?? 'untyped'})\n`);
68
+ process.stderr.write(`${rel} (${type ?? 'untyped'})\n`);
58
69
  }