runlist 0.95.5 → 0.95.6

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
@@ -47,8 +47,8 @@ controls are experimental; live retrieval depends on a compatible external
47
47
  provider. Local chat and push are deferred.
48
48
 
49
49
  <!-- runlist:release:start -->
50
- CLI and desktop release version: **0.95.5**.
51
- [Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.95.5).
50
+ CLI and desktop release version: **0.95.6**.
51
+ [Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.95.6).
52
52
  Normal product releases publish both components; an installed Mac app is updated with rollback after it has quit.
53
53
  <!-- runlist:release:end -->
54
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlist",
3
- "version": "0.95.5",
3
+ "version": "0.95.6",
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.95.5",
3
+ "version": "0.95.6",
4
4
  "description": "Runlist workflow skill, session orientation, and tool guard for Codex",
5
5
  "author": {
6
6
  "name": "Reo Owens"
@@ -81,7 +81,7 @@ export function extractBodyLinks(body) {
81
81
  }
82
82
 
83
83
  export function extractChecklistCounts(body) {
84
- const matches = [...body.matchAll(/^\s*[-*]\s+\[([ xX])\]\s+/gm)];
84
+ const matches = [...body.matchAll(/^\s*[-*]\s+\[([ xX~])\]\s+/gm)];
85
85
  let completed = 0;
86
86
  let open = 0;
87
87
 
package/src/index.mjs CHANGED
@@ -335,12 +335,14 @@ export function parseDocFile(filePath, config, opts = {}) {
335
335
  if (text && text.shipsMetadataVersion !== 1) text = null;
336
336
  // Validation reads the body itself, so only a fast build can skip the read.
337
337
  let body = null;
338
+ let bodyLineOffset = 0;
338
339
  let rawSource = null;
339
340
  if (!text || !fast) {
340
341
  const raw = opts.source ?? readFileSync(filePath, 'utf8');
341
342
  rawSource = raw;
342
343
  const extracted = extractFrontmatter(raw);
343
344
  body = extracted.body;
345
+ bodyLineOffset = extracted.bodyLineOffset;
344
346
  // A file rewritten between the stat and the read no longer matches its stamp.
345
347
  if (text && Buffer.byteLength(raw) !== stampSize(stamp)) text = null;
346
348
  if (!text) {
@@ -471,7 +473,7 @@ export function parseDocFile(filePath, config, opts = {}) {
471
473
  return doc;
472
474
  }
473
475
  validateDoc(doc, parsedFrontmatter, headingTitle, config);
474
- validatePlanShape(doc, body, parsedFrontmatter, config);
476
+ validatePlanShape(doc, body, parsedFrontmatter, config, bodyLineOffset);
475
477
  validateDocShape(doc, body, parsedFrontmatter, config);
476
478
  }
477
479
  return doc;
@@ -17,7 +17,7 @@ import { readEnv, stateDir } from './naming.mjs';
17
17
  // a missing, unreadable or foreign-version file is treated as empty, a failed
18
18
  // write is dropped, and `RUNLIST_NO_PARSE_CACHE=1` turns it off.
19
19
 
20
- const SCHEMA = 1;
20
+ const SCHEMA = 2;
21
21
  const FILE_NAME = 'parse-cache';
22
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const VERSION = (() => {
package/src/section.mjs CHANGED
@@ -73,7 +73,7 @@ export function findSection(sections, name) {
73
73
  const GLYPH_PATTERNS = [
74
74
  { kind: 'shipped', re: /(✅|☑|✔|✓)/ },
75
75
  { kind: 'skipped', re: /(⏭)/ },
76
- { kind: 'in-progress', re: /(🟡|🔄)/ },
76
+ { kind: 'in-progress', re: /(🟡|🟨|🔄)/ },
77
77
  { kind: 'blocked', re: /(🚧|🔴)/ },
78
78
  { kind: 'todo', re: /(⬜|⬛|◻|☐)/ },
79
79
  ];
@@ -115,7 +115,7 @@ export function detectMarker(heading) {
115
115
  // and the space, is not \s, and ends the run — so a heading led by the
116
116
  // emoji-presentation form was not a phase heading at all, while the bare
117
117
  // codepoint was. Reported by the owner, 2026-08-16.
118
- const PHASE_DECORATION = String.raw`[\s>*_~\`#-]*(?:[✅🚧⬜🟡⏭☑✔✓◻☐⬛🔴🔄][︎️]?\s*)*`;
118
+ const PHASE_DECORATION = String.raw`[\s>*_~\`#-]*(?:[✅🚧⬜🟡🟨⏭☑✔✓◻☐⬛🔴🔄][︎️]?\s*)*`;
119
119
  const PHASE_LEAD = new RegExp(`^${PHASE_DECORATION}phase\\b`, 'i');
120
120
  const FILES_MANIFEST_ANCESTOR = new RegExp(`^${PHASE_DECORATION}files\\b`, 'i');
121
121
 
@@ -157,18 +157,25 @@ const PHASE_COMPOUND_COMMENTARY = new RegExp(
157
157
  // Dated execution receipts describe an attempt or acceptance record, not a work phase.
158
158
  const PHASE_DATED_RECEIPT = new RegExp(
159
159
  `^${PHASE_DECORATION}phase\\s+\\S+\\s+` +
160
- `(?:execution|completed[- ]work\\s+record|personal\\s+billing\\s+acceptance|portal\\s+attempt)` +
160
+ `(?:execution|completed[- ]work\\s+record|personal\\s+billing\\s+acceptance|portal\\s+attempt|` +
161
+ `receipts?|state|verification|build|shaping\\s+walk)` +
161
162
  `\\s*,\\s*(?:closed\\s+)?(?:[A-Z][a-z]+\\s+\\d{1,2},\\s+\\d{4}|\\d{4}-\\d{2}-\\d{2})\\b`,
162
163
  'i',
163
164
  );
164
165
 
166
+ const PHASE_RECORDED_MECHANICS = new RegExp(
167
+ `^${PHASE_DECORATION}phase\\s+\\S+\\s+mechanics\\s*,\\s*recorded\\b`,
168
+ 'i',
169
+ );
170
+
165
171
  export function isPhaseHeading(section) {
166
172
  if (section.level !== 3) return false;
167
173
  if (FILES_MANIFEST_ANCESTOR.test(section.h2Ancestor ?? '')) return false;
168
174
  return PHASE_LEAD.test(section.heading)
169
175
  && !PHASE_COMMENTARY.test(section.heading)
170
176
  && !PHASE_COMPOUND_COMMENTARY.test(section.heading)
171
- && !PHASE_DATED_RECEIPT.test(section.heading);
177
+ && !PHASE_DATED_RECEIPT.test(section.heading)
178
+ && !PHASE_RECORDED_MECHANICS.test(section.heading);
172
179
  }
173
180
 
174
181
  // A phase's OWN checklist — the boxes directly under its heading, stopping at
@@ -179,7 +186,7 @@ export function phaseTally(section) {
179
186
  for (const line of String(section?.body ?? '').split('\n')) {
180
187
  if (/^#{1,6}\s/.test(line)) break;
181
188
  if (/^\s*[-*]\s*\[x\]/i.test(line)) checked++;
182
- else if (/^\s*[-*]\s*\[ \]/.test(line)) unchecked++;
189
+ else if (/^\s*[-*]\s*\[[ ~]\]/.test(line)) unchecked++;
183
190
  }
184
191
  return { checked, unchecked, total: checked + unchecked };
185
192
  }
package/src/validate.mjs CHANGED
@@ -626,7 +626,7 @@ export function checkGitStaleness(docs, config, options = {}) {
626
626
 
627
627
  // Plan-shape lint: soft warnings on convention drift. Plan-only.
628
628
  // Body is the unparsed plan body (everything after the closing `---`).
629
- export function validatePlanShape(doc, body, frontmatter, config) {
629
+ export function validatePlanShape(doc, body, frontmatter, config, bodyLineOffset = 0) {
630
630
  if (doc.type !== 'plan') return;
631
631
  // Skip plans in terminal/archive statuses (closed work shouldn't generate noise)
632
632
  if (config.lifecycle.terminalStatuses.has(doc.status) || config.lifecycle.archiveStatuses.has(doc.status)) return;
@@ -707,7 +707,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
707
707
  doc.warnings.push({
708
708
  path: doc.path,
709
709
  level: 'warning',
710
- message: `Phase marker contradicts its own checklist (line ${phase.lineStart}): \`${phase.heading.trim()}\` is marked ${conflict.declared}, but ${conflict.checked}/${conflict.total} boxes are checked — that reads as ${conflict.implied}. Update whichever half is stale.`,
710
+ message: `Phase marker contradicts its own checklist (line ${bodyLineOffset + phase.lineStart}): \`${phase.heading.trim()}\` is marked ${conflict.declared}, but ${conflict.checked}/${conflict.total} boxes are checked — that reads as ${conflict.implied}. Update whichever half is stale.`,
711
711
  });
712
712
  }
713
713
  }