runlist 0.95.6 → 0.96.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
@@ -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.6**.
51
- [Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.95.6).
50
+ CLI and desktop release version: **0.96.0**.
51
+ [Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.96.0).
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
 
@@ -652,6 +652,13 @@ operation:
652
652
  runlist baton @/tmp/resume.md
653
653
  ```
654
654
 
655
+ Parent handoffs use `resume-<name>`. Delegated agents explicitly use
656
+ `runlist baton --subagent <task-slug> @/tmp/resume.md` for `subagent-<task-slug>`.
657
+ Under a parent-owned plan, use a distinct task slug that does not resolve to a
658
+ plan. A child owning a dedicated plan can pass that plan with `--subagent`.
659
+ Existing prompts keep their names. See `runlist baton --help` for prefix and
660
+ replacement checks.
661
+
655
662
  Baton refuses when a handoff for the same work is already pending, so one piece
656
663
  of work never has two resume prompts. Inspect the pending handoff with
657
664
  `runlist prompts show <slug>`. Keep it if current; if stale, pass `--replace`
package/bin/runlist.mjs CHANGED
@@ -1349,6 +1349,7 @@ Options:
1349
1349
  --note "why" Append the reason to ## Version History (plan mode only)
1350
1350
  --message / --body Inline body (one-liners; prefer @path or stdin)
1351
1351
  --force Recover another session's plan (explicit path required)
1352
+ --subagent Save subagent-<name> instead of the parent default resume-<name>
1352
1353
  --replace Replace exactly one pending handoff; archive its prior text
1353
1354
  --done Report a plan handoff done once next_step changed after the prompt was saved
1354
1355
  --json Structured repository/session/generated file result
@@ -1363,6 +1364,7 @@ The function returns argv; baton shell-quotes each argument before printing.
1363
1364
  Examples:
1364
1365
  runlist baton @/tmp/draft.md
1365
1366
  runlist baton checkout-fixes @/tmp/draft.md
1367
+ runlist baton --subagent totals-review @/tmp/draft.md
1366
1368
  runlist baton docs/plans/auth.md @/tmp/draft.md
1367
1369
  runlist baton docs/plans/auth.md @/tmp/new-draft.md --replace
1368
1370
  runlist baton --status paused --note "blocked on review" @/tmp/d.md
@@ -1388,7 +1390,14 @@ cooperating transaction:
1388
1390
  grant ownership. A live pickup-hook delivery lease blocks release and force
1389
1391
  takeover; hooks are at-least-once and deduplicate the stable operationId.
1390
1392
 
1391
- Slug mode (no plan involved) saves resume-<slug> without a status change or
1393
+ Use --subagent for a delegated agent handoff, named subagent-<plan-slug> or
1394
+ subagent-<slug>. Under a parent-owned plan, pass a task-specific slug rather
1395
+ than releasing the parent plan. A child owning its own plan can hand it off.
1396
+ Matching prefixes are kept once; conflicting or nested prefixes are refused.
1397
+ Existing prompts keep their names. --subagent --replace refuses a parent-named
1398
+ handoff rather than reclassifying it.
1399
+
1400
+ Slug mode (no plan involved) saves resume-<slug> by default without a status change or
1392
1401
  commit. A bare word that names a plan is treated as that plan.
1393
1402
 
1394
1403
  Baton saves nothing while a handoff for the same work is pending (resume-<name>,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlist",
3
- "version": "0.95.6",
3
+ "version": "0.96.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.95.6",
3
+ "version": "0.96.0",
4
4
  "description": "Runlist workflow skill, session orientation, and tool guard for Codex",
5
5
  "author": {
6
6
  "name": "Reo Owens"
@@ -16,6 +16,7 @@ Use this skill in repositories with `runlist.config.mjs` or `dotmd.config.mjs`.
16
16
  - 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
17
  - 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`.
18
18
  - For a handoff, write a concrete resume draft, then run `runlist baton [<plan-or-slug>] @<draft-file>`. An owned plan is released in the same operation. If a handoff for that work is pending, inspect it with `runlist prompts show <file>`; keep it if current or pass `--replace` with the new draft. Replacement archives the old text and refreshes exactly one pending prompt.
19
+ - **Subagent handoffs:** use `runlist baton --subagent <task-slug> @/tmp/draft.md` to save `subagent-<task-slug>`. Under a parent-owned plan, choose a distinct task slug that does not resolve to a plan; do not release the parent's claim. A child owning a dedicated plan can use `runlist baton --subagent <plan-file> @/tmp/draft.md`. Parent-session batons keep the default `resume-*` naming. Matching prefixes are kept once; conflicting prefixes are refused. Existing prompts keep their names, and `--subagent --replace` refuses a parent-prefixed prompt.
19
20
  - Consume a saved prompt with `runlist use <prompt-file>` (or bare `runlist use` for the oldest). This archives and claims before printing the body. For read-only triage, use `runlist prompts show <file>` or `runlist prompts show --all`; never open pending prompts with a file reader or `cat`.
20
21
  - Pending prompts are session-local. Commit the tracked plan or source files named by baton, not `docs/prompts/*.md`.
21
22
  - Create a document with `runlist new <type> <name> @<draft-file>`. A draft starting with frontmatter can supply scaffold fields; `type` is fixed by the command.
package/src/baton.mjs CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
4
4
  import { asString, toRepoPath, die, warn, isArchivedPath, resolveRefPath, nowIso } from './util.mjs';
5
5
  import { buildIndex, resolveDocArg } from './index.mjs';
6
- import { preparePromptDocument, runNew, readBodyInput, readPipedBodyInput } from './new.mjs';
6
+ import { preparePromptDocument, runNew, readBodyInput, readPipedBodyInput, slugify } from './new.mjs';
7
7
  import { ensurePlanCompletionBeforeRelease, planHasPendingCompletion, renderLifecycleMutation, runSet } from './lifecycle.mjs';
8
8
  import { green, dim, yellow } from './color.mjs';
9
9
  import { handoffNotDoneLine, recordPendingHandoff, runBatonDone } from './handoff-record.mjs';
@@ -123,6 +123,7 @@ export async function runBaton(argv, config, opts = {}) {
123
123
  let force = false;
124
124
  let replace = false;
125
125
  let done = false;
126
+ let subagent = false;
126
127
  const positionals = [];
127
128
  for (let i = 0; i < argv.length; i++) {
128
129
  const a = argv[i];
@@ -132,6 +133,7 @@ export async function runBaton(argv, config, opts = {}) {
132
133
  if (a === '--force') { force = true; continue; }
133
134
  if (a === '--replace') { replace = true; continue; }
134
135
  if (a === '--done') { done = true; continue; }
136
+ if (a === '--subagent') { subagent = true; continue; }
135
137
  if (a === '--json') continue;
136
138
  if (!a.startsWith('-') || a === '-' || a.startsWith('@')) { positionals.push(a); continue; }
137
139
  die(`Unknown flag for \`runlist baton\`: ${a}`);
@@ -199,12 +201,30 @@ export async function runBaton(argv, config, opts = {}) {
199
201
  }
200
202
  }
201
203
 
202
- const nameBase = planPath ? path.basename(planPath, '.md') : promptSlug;
203
- const slugBase = nameBase.startsWith('resume-') ? nameBase : `resume-${nameBase}`;
204
+ const nameBase = slugify(planPath ? path.basename(planPath, '.md') : promptSlug);
205
+ const handoffPrefix = subagent ? 'subagent-' : 'resume-';
206
+ const recognizedPrefix = /^(resume-|subagent-)/.exec(nameBase)?.[0];
207
+ if (recognizedPrefix && recognizedPrefix !== handoffPrefix) {
208
+ die(`Nothing saved: ${nameBase} conflicts with ${subagent ? '--subagent' : 'the parent handoff default'}. Use an unprefixed name or ${handoffPrefix}<name>${subagent ? ', or omit --subagent for a parent handoff' : ', or pass --subagent for a child handoff'}.`);
209
+ }
210
+ const unprefixed = recognizedPrefix ? nameBase.slice(handoffPrefix.length) : nameBase;
211
+ if (!unprefixed || /^(resume-|subagent-)/.test(unprefixed)) {
212
+ die('Nothing saved: use a nonempty handoff name with at most one resume- or subagent- prefix.');
213
+ }
214
+ const slugBase = `${handoffPrefix}${unprefixed}`;
204
215
  let replacement = null;
216
+ let alternatePromptPath = null;
205
217
  const resolvePending = () => {
206
218
  const target = preparePromptDocument(slugBase, body, config, { dryRun: true });
207
219
  const pending = pendingHandoffs(target.filePath, planPath, config);
220
+ if (planPath) {
221
+ const alternateSlug = `${subagent ? 'resume-' : 'subagent-'}${unprefixed}`;
222
+ alternatePromptPath = preparePromptDocument(alternateSlug, body, config, { dryRun: true }).filePath;
223
+ if (existsSync(alternatePromptPath)) {
224
+ const alternate = toRepoPath(alternatePromptPath, config.repoRoot);
225
+ if (!pending.includes(alternate)) pending.push(alternate);
226
+ }
227
+ }
208
228
  if (!replace) {
209
229
  if (pending.length) refusePendingHandoff(pending);
210
230
  return;
@@ -214,6 +234,9 @@ export async function runBaton(argv, config, opts = {}) {
214
234
  die(`Nothing replaced: --replace requires exactly one pending handoff. ${detail}`);
215
235
  }
216
236
  const oldRepoPath = pending[0];
237
+ if (subagent && /^resume-/i.test(path.basename(oldRepoPath))) {
238
+ die(`Nothing replaced: ${oldRepoPath} is a parent handoff. Inspect it with \`runlist prompts show ${path.basename(oldRepoPath, '.md')}\`; use a separate task slug for the child handoff, or omit --subagent to refresh the parent handoff.`);
239
+ }
217
240
  const oldPath = authorizeManagedSource(path.resolve(config.repoRoot, oldRepoPath), config, { kind: 'Baton replacement source' }).path;
218
241
  const oldRaw = readFileSync(oldPath, 'utf8');
219
242
  const { frontmatter, body: oldBody } = extractFrontmatter(oldRaw);
@@ -344,8 +367,8 @@ export async function runBaton(argv, config, opts = {}) {
344
367
  throw err;
345
368
  }
346
369
  }
347
- createdSlug = promptName;
348
370
  promptRepoPath = replacement?.oldRepoPath ?? prepared.repoPath;
371
+ createdSlug = path.basename(promptRepoPath, '.md');
349
372
  } else {
350
373
  const promptName = replacement ? path.basename(replacement.oldPath, '.md') : slugBase;
351
374
  const prepared = preparePromptDocument(promptName, body, config, { plan: repoPath, dryRun });
@@ -361,15 +384,18 @@ export async function runBaton(argv, config, opts = {}) {
361
384
  testHooks: opts.testHooks,
362
385
  additionalUpdates: dryRun ? [] : mutation.updates,
363
386
  creations: dryRun ? [] : replacement ? mutation.creations : [{ path: prepared.filePath, content: prepared.content }],
364
- guards: dryRun ? [] : [...mutation.guards, ...(planGuard ? [planGuard] : [])],
387
+ guards: dryRun ? [] : [
388
+ ...mutation.guards, ...(planGuard ? [planGuard] : []),
389
+ ...(!replacement && alternatePromptPath ? [{ path: alternatePromptPath, absent: true }] : []),
390
+ ],
365
391
  deferIndex: true,
366
392
  });
367
393
  } catch (err) {
368
394
  if (/Destination already exists|File already exists/.test(String(err?.message))) refusePendingHandoff([prepared.repoPath]);
369
395
  throw err;
370
396
  }
371
- createdSlug = prepared.slug;
372
397
  promptRepoPath = replacement?.oldRepoPath ?? prepared.repoPath;
398
+ createdSlug = path.basename(promptRepoPath, '.md');
373
399
  statusChanged = oldStatus !== status;
374
400
  if (!dryRun && !replacement) {
375
401
  try { config.hooks.onNew?.({ path: prepared.repoPath, status: 'pending', title: prepared.slug, type: 'prompt' }); }
package/src/commands.mjs CHANGED
@@ -140,7 +140,7 @@ const definitions = [
140
140
  ], { aliases: ['prompt'] }),
141
141
  command('use', mutates('managed source when starting/consuming; docs remain read-only'), 'workflow', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
142
142
  command('next', mutates('managed prompt source and same-root archive destination'), 'workflow', [form('', { options: [flag('--json'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
143
- command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), flag('--force'), flag('--replace'), flag('--done'), flag('--json')] })]),
143
+ command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), flag('--force'), flag('--replace'), flag('--done'), flag('--subagent'), flag('--json')] })]),
144
144
  command('runlist', mutates('managed hubs/children and managed scaffold destinations'), 'workflow', [
145
145
  form('<hub>', { args: positionals(1, 1), options: [flag('--json')] }),
146
146
  form('next <hub>', { subcommands: ['next'], args: positionals(1, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files')] }),
package/src/hud.mjs CHANGED
@@ -294,6 +294,7 @@ export function buildHud(config) {
294
294
  const SUBAGENT_PRIMER = [
295
295
  'runlist manages this repo\'s plans/docs/prompts (markdown + YAML frontmatter).',
296
296
  'Verbs: plans|briefing | query <filters> | use [<file>] | set <status> <file> | new <type> <slug> | archive <file>.',
297
+ 'Child handoff: `runlist baton --subagent <task-slug> @<draft-file>` saves subagent-<task-slug>. Use a task slug under a parent-owned plan; release only your own plan. Parent batons default to resume-<name>.',
297
298
  'Claims: `runlist doctor --claims` lists which session holds each plan; `--apply` releases the ones whose session is gone.',
298
299
  'Do NOT: cat/read a docs/prompts/*.md (use `runlist use <file>` — archive/claim commits before at-most-once output);',
299
300
  'git add/commit a prompt (they are session-local, often gitignored); hand-edit a `status:` field (use `runlist set`).',