dotmd-cli 0.65.0 → 0.65.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/bin/dotmd.mjs CHANGED
@@ -1219,12 +1219,19 @@ frontmatter — there is no separate doc type. The hub plan can have any status;
1219
1219
  the order of the children comes from the array.
1220
1220
 
1221
1221
  Usage:
1222
- dotmd runlist <hub> Show children + their statuses, in order.
1223
- The first non-archived child is marked \`→\`.
1224
- dotmd runlist next <hub> Open the first non-archived child (marks it
1225
- in-session + prints it). Stops if it's not in a
1226
- workable status (active / planned / in-session)
1227
- so you resolve the blocker first.
1222
+ dotmd runlist <hub> Show children + their statuses, in order. The
1223
+ first pickup-able child (active / planned /
1224
+ in-session) is marked \`→\`. Archived (done) and
1225
+ parked children (blocked / partial / paused /
1226
+ awaiting / queued-after) are skipped — \`→\`
1227
+ advances to the first child you can actually
1228
+ start. Parked ≠ done: they don't count toward
1229
+ done/total.
1230
+ dotmd runlist next <hub> Open the first pickup-able child (marks it
1231
+ in-session + prints it), advancing past archived
1232
+ and parked children. If every remaining child is
1233
+ parked, stops and lists them + the unstick verbs
1234
+ so you resolve a blocker first.
1228
1235
  dotmd runlist add <hub> <child...>
1229
1236
  Append children to the hub's \`runlist:\` array
1230
1237
  (no more hand-editing the YAML). Each child can be:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.65.0",
3
+ "version": "0.65.1",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/query.mjs CHANGED
@@ -768,9 +768,13 @@ function renderHubBlock(hub, info, children, maxWidth, topMaxSlug) {
768
768
  const hubSlug = toSlug(hub);
769
769
  const nextDoc = info.nextChildPath ? info.children.find(c => c.path === info.nextChildPath)?.doc : null;
770
770
  const nextLabel = nextDoc ? stripHubPrefix(toSlug(nextDoc), hubSlug) : null;
771
+ // No pickup-able child: "N parked" when a live-but-unstartable child remains
772
+ // (the runlist is stuck, not finished), else "all archived" (truly done).
771
773
  const descr = nextLabel
772
774
  ? `runlist · ${info.doneCount}/${info.total} · next → ${nextLabel}`
773
- : `runlist · ${info.doneCount}/${info.total} · all archived`;
775
+ : info.parkedCount > 0
776
+ ? `runlist · ${info.doneCount}/${info.total} · ${info.parkedCount} parked`
777
+ : `runlist · ${info.doneCount}/${info.total} · all archived`;
774
778
 
775
779
  // Header row: hub slug + descriptor, with `[RUNLIST]` right-aligned like a tag.
776
780
  const slugCell = hubSlug.padEnd(topMaxSlug);
package/src/runlist.mjs CHANGED
@@ -20,6 +20,20 @@ import { bold, cyan, dim, green, red, yellow } from './color.mjs';
20
20
 
21
21
  const PICKUPABLE_STATUSES = new Set(['active', 'planned', 'in-session']);
22
22
 
23
+ // A child is the runlist's NEXT PICKUP only when a session could start it right
24
+ // now — i.e. its status is one `dotmd use` accepts. The "parked" statuses
25
+ // (blocked/partial/paused/awaiting/queued-after) are deliberately NOT
26
+ // pickup-able: each needs its own unstuck action (monitor / spawn successor /
27
+ // re-evaluate / ask / check predecessor) before work resumes. So next-pickup
28
+ // resolution skips them and advances to the first child that's actually
29
+ // startable. Skipping ≠ done, though: a parked child is not archived, so it
30
+ // never counts toward `done/total` — that tally tracks closed (archived) only.
31
+ // This keeps the `→` marker in agreement with `runlist next`, which already
32
+ // gates on PICKUPABLE_STATUSES.
33
+ function isPickupable(status) {
34
+ return PICKUPABLE_STATUSES.has(status);
35
+ }
36
+
23
37
  // Build a hub/child map straight from the in-memory index — no disk IO. A doc
24
38
  // is a runlist *hub* when its `runlist:` frontmatter (`refFields.runlist`) is
25
39
  // non-empty. Each ref resolves to a doc in the index by path, falling back to
@@ -61,11 +75,17 @@ export function buildRunlistIndex(index, config) {
61
75
  }
62
76
  }
63
77
 
64
- const next = children.find(c => !c.missing && !c.archived) ?? null;
78
+ // Next pickup = first child a session can actually start. Skip archived
79
+ // (done) AND parked children alike; advance to the first pickup-able one.
80
+ const next = children.find(c => !c.missing && !c.archived && isPickupable(c.status)) ?? null;
65
81
  hubs.set(hub.path, {
66
82
  hub,
67
83
  total: children.length,
68
84
  doneCount: children.filter(c => c.archived).length,
85
+ // Live-but-not-startable children (parked: blocked/partial/paused/…). Lets
86
+ // the `dotmd plans` fold say "N parked" instead of mislabelling a hub with
87
+ // a parked-but-unfinished child as "all archived".
88
+ parkedCount: children.filter(c => !c.missing && !c.archived && !isPickupable(c.status)).length,
69
89
  children,
70
90
  nextChildPath: next?.path ?? null,
71
91
  });
@@ -271,6 +291,11 @@ function resolveHubNextPickup(hubDoc, hubDir, resolveRef, archiveStatuses, confi
271
291
  if (child.type && child.type !== 'plan') continue;
272
292
  const archived = archiveStatuses.has(child.status) || isArchivedPath(child.path, config);
273
293
  if (archived) continue;
294
+ // Skip parked ranked children too (blocked/partial/paused/awaiting/
295
+ // queued-after) — the hub's next-pickup is the first startable plan, the
296
+ // same gate sprint runlists use, so a hub never points `→` at a child a
297
+ // session can't actually pick up.
298
+ if (!isPickupable(child.status)) continue;
274
299
  return { path: child.path, status: child.status ?? null, label: coordinationChildLabel(child, hubDoc) };
275
300
  }
276
301
  return null;
@@ -330,7 +355,9 @@ function renderRunlist(hubRepoPath, children, opts = {}) {
330
355
  lines.push(` ${idx}. ${red('missing')} ${c.ref}`);
331
356
  continue;
332
357
  }
333
- const isNext = !nextPicked && !archiveStatuses.has(c.status);
358
+ // → marks the first child a session can actually start: skip archived and
359
+ // parked (blocked/partial/paused/awaiting/queued-after) alike.
360
+ const isNext = !nextPicked && isPickupable(c.status);
334
361
  if (isNext) nextPicked = true;
335
362
  const marker = isNext ? green('→') : ' ';
336
363
  const statusTag = `[${colorStatus(c.status)}]`;
@@ -338,7 +365,12 @@ function renderRunlist(hubRepoPath, children, opts = {}) {
338
365
  }
339
366
  if (!nextPicked) {
340
367
  lines.push('');
341
- lines.push(dim(' All children archived. Hub is ready for archive.'));
368
+ // No pickup-able child. Distinguish "done — ready to archive" from "stuck —
369
+ // a parked child needs unsticking" so the agent knows which it is.
370
+ const parked = children.filter(c => !c.missing && !archiveStatuses.has(c.status));
371
+ lines.push(dim(parked.length === 0
372
+ ? ' All children archived. Hub is ready for archive.'
373
+ : ` No pickup-able child — ${parked.length} parked. Unstick one (e.g. \`dotmd set active <child>\`) to continue.`));
342
374
  }
343
375
  return lines.join('\n') + '\n';
344
376
  }
@@ -817,11 +849,29 @@ export async function runRunlist(argv, config, opts = {}) {
817
849
  return;
818
850
  }
819
851
 
820
- // sub === 'next' — find first non-archived non-missing child and pick it up.
821
- const target = children.find(c => !c.missing && !archiveStatuses.has(c.status));
852
+ // sub === 'next' — pick up the first child a session can actually start.
853
+ // Skip both archived (done) and parked (blocked/partial/paused/awaiting/
854
+ // queued-after) children: the runlist advances to the first pickup-able one,
855
+ // so the picked target is guaranteed in a `dotmd use`-able status.
856
+ const target = children.find(c => !c.missing && !archiveStatuses.has(c.status) && isPickupable(c.status));
822
857
  if (!target) {
823
858
  if (children.length === 0) die(`Hub ${hubRepoPath} has empty \`runlist:\` — nothing to pick up.`);
824
- const allArchived = children.every(c => !c.missing && archiveStatuses.has(c.status));
859
+ // Live (non-archived, non-missing) children that exist but aren't startable.
860
+ const parked = children.filter(c => !c.missing && !archiveStatuses.has(c.status));
861
+ if (parked.length > 0) {
862
+ // Every remaining child is parked — surface them with statuses + the
863
+ // unstick verbs so the agent can resume one instead of being told a
864
+ // generic "no pickup" with no path forward.
865
+ const listed = parked.map(c => ` ${c.path} (status: ${c.status})`).join('\n');
866
+ die(
867
+ `No pickup-able child in runlist ${hubRepoPath} — every remaining child is parked:\n` +
868
+ `${listed}\n` +
869
+ `Unstick one before continuing:\n` +
870
+ ` dotmd set active <child> # if ready to resume\n` +
871
+ ` dotmd use <child> # to inspect`,
872
+ );
873
+ }
874
+ const allArchived = children.some(c => !c.missing) && children.every(c => c.missing || archiveStatuses.has(c.status));
825
875
  if (allArchived) {
826
876
  die(`All children in runlist ${hubRepoPath} are archived. Hub is ready for \`dotmd archive ${hubRepoPath}\`.`);
827
877
  }
@@ -829,18 +879,6 @@ export async function runRunlist(argv, config, opts = {}) {
829
879
  die(`No pickup-able child in runlist ${hubRepoPath}. Unresolved refs: ${missing.join(', ')}`);
830
880
  }
831
881
 
832
- // Pre-check status: pickup will die on non-pickup-able statuses, but with
833
- // a generic message. Surface the runlist context first so the agent knows
834
- // which list is blocked and on which item.
835
- if (!PICKUPABLE_STATUSES.has(target.status)) {
836
- die(
837
- `Next child in runlist ${hubRepoPath} is ${target.path} (status: ${target.status}).\n` +
838
- `Resolve the blocker before continuing the runlist.\n` +
839
- ` dotmd set active ${target.path} # if ready to resume\n` +
840
- ` dotmd use ${target.path} # to inspect`,
841
- );
842
- }
843
-
844
882
  // Open the next child: set it in-session (frontmatter) and render its card.
845
883
  // Dynamic import to avoid circular module-load cost when the runlist command
846
884
  // isn't used.