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.
|
|
51
|
-
[Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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 ? [] : [
|
|
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`).',
|