runlist 0.99.2 → 0.100.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.
Files changed (58) hide show
  1. package/GETTING_STARTED.md +249 -0
  2. package/README.md +414 -46
  3. package/bin/runlist.mjs +402 -51
  4. package/package.json +3 -1
  5. package/plugins/runlist-codex/.codex-plugin/plugin.json +1 -1
  6. package/plugins/runlist-codex/bin/consume-prompt.mjs +45 -0
  7. package/plugins/runlist-codex/bin/runlist-hook +29 -11
  8. package/plugins/runlist-codex/bin/session-start.mjs +16 -0
  9. package/plugins/runlist-codex/bin/subagent-start.mjs +16 -0
  10. package/plugins/runlist-codex/hooks/hooks.json +11 -0
  11. package/plugins/runlist-codex/skills/runlist/SKILL.md +27 -11
  12. package/plugins/runlist-codex/skills/runlist-setup/SKILL.md +109 -0
  13. package/runlist.config.example.mjs +6 -0
  14. package/src/agent-context.mjs +8 -2
  15. package/src/agent-hosts.mjs +305 -0
  16. package/src/app-create.mjs +11 -1
  17. package/src/atomic-mutation.mjs +140 -35
  18. package/src/baton.mjs +127 -33
  19. package/src/codex-integration.mjs +57 -7
  20. package/src/commands.mjs +92 -38
  21. package/src/config-command.mjs +115 -0
  22. package/src/config-setting.mjs +163 -0
  23. package/src/config.mjs +57 -12
  24. package/src/doctor-hosts.mjs +192 -0
  25. package/src/doctor.mjs +16 -6
  26. package/src/export.mjs +11 -0
  27. package/src/git.mjs +58 -2
  28. package/src/guard.mjs +41 -13
  29. package/src/handoff-record.mjs +290 -30
  30. package/src/handoffs-help.mjs +143 -0
  31. package/src/handoffs-mode.mjs +486 -0
  32. package/src/handoffs-scope.mjs +98 -0
  33. package/src/help.mjs +72 -0
  34. package/src/host-integration.mjs +7 -1
  35. package/src/hud.mjs +174 -29
  36. package/src/index.mjs +11 -4
  37. package/src/init.mjs +255 -160
  38. package/src/install.mjs +35 -18
  39. package/src/installation.mjs +110 -27
  40. package/src/journal.mjs +2 -2
  41. package/src/lifecycle.mjs +22 -11
  42. package/src/managed-path.mjs +68 -4
  43. package/src/migrate.mjs +7 -0
  44. package/src/naming.mjs +26 -0
  45. package/src/new.mjs +202 -91
  46. package/src/pickup-card.mjs +1 -1
  47. package/src/plan-changes.mjs +49 -0
  48. package/src/prompt-handoff.mjs +16 -0
  49. package/src/prompts.mjs +101 -27
  50. package/src/rename.mjs +13 -0
  51. package/src/setup-carry.mjs +291 -0
  52. package/src/setup-waiting.mjs +20 -0
  53. package/src/setup-worker.mjs +257 -0
  54. package/src/setup.mjs +404 -0
  55. package/src/statuses.mjs +5 -2
  56. package/src/update.mjs +239 -79
  57. package/src/use.mjs +4 -2
  58. package/src/validate.mjs +7 -0
package/bin/runlist.mjs CHANGED
@@ -8,11 +8,15 @@ import { readPlanStage } from '../src/stages.mjs';
8
8
  import { scopeFilingCoverage } from '../src/filing.mjs';
9
9
  import { die, warn, levenshtein, isArchivedPath, toRepoPath } from '../src/util.mjs';
10
10
  import { recordCliInvocation, recordGlobalError, sanitizeTelemetryArgv } from '../src/journal.mjs';
11
+ import { outsideHandoffs, outsideHandoffsLine } from '../src/handoffs-scope.mjs';
11
12
  import { findRepeatFailureHint } from '../src/hints.mjs';
13
+ import { focusedCommandHelp, unsupportedCommandHelp } from '../src/help.mjs';
12
14
  import {
13
15
  KNOWN_COMMANDS,
14
16
  canonicalCommand,
17
+ commandDefinition,
15
18
  commandOwnsOption,
19
+ commandPlanning,
16
20
  commandPolicy,
17
21
  commandUsage,
18
22
  validateCommandArgs,
@@ -30,6 +34,8 @@ const QUERY_VALUE_FLAGS = new Set([
30
34
 
31
35
  function requireCommandPolicy(command, policy) {
32
36
  if (policy) return policy;
37
+ const correction = unsupportedCommandHelp(command);
38
+ if (correction) die(`Unknown command: ${command}\n\n${correction}`);
33
39
  const matches = KNOWN_COMMANDS
34
40
  .map(cmd => ({ cmd, dist: levenshtein(command, cmd) }))
35
41
  .sort((a, b) => a.dist - b.dist);
@@ -61,6 +67,9 @@ function applyPathScopeToIndex(index, config, inputs) {
61
67
 
62
68
  const selected = new Set();
63
69
  for (const input of inputs) {
70
+ // A handoffs-only project checks its prompts and nothing else; any other
71
+ // path hears that planning is off (src/handoffs-scope.mjs).
72
+ if (outsideHandoffs(input, config)) die(outsideHandoffsLine(input));
64
73
  const resolved = resolveExistingPath(input, config);
65
74
  if (!resolved) die(`Could not resolve check path: ${input}`);
66
75
 
@@ -108,11 +117,39 @@ function applyPathScopeToIndex(index, config, inputs) {
108
117
  }
109
118
  }
110
119
 
120
+ // Help where planning is off (a handoffs-only project, src/handoffs-mode.mjs):
121
+ // the handoff verbs and the command that turns planning on, nothing else.
122
+ const HANDOFFS_HELP = `runlist v${pkg.version} — session handoffs (planning is off in this project)
123
+
124
+ Commands:
125
+ baton <slug> @<draft-file> Save a resume prompt for the next session (resume-<slug>)
126
+ prompts List saved prompts
127
+ prompts show <name...> Read saved prompts without consuming them
128
+ use [<name>] Resume a saved prompt (no name: the oldest pending)
129
+ new prompt <name> @<file> Save a prompt under a name of its own
130
+ hud What a new session is told at startup
131
+ setup Turn planning on (plans, docs and an index); prompts stay put
132
+
133
+ More help:
134
+ runlist help statuses Prompt statuses
135
+ runlist <cmd> --help Per-command details
136
+
137
+ Global flags: --config <path> --dry-run/-n --verbose --version`;
138
+
139
+ const HANDOFFS_STATUS_HELP = `runlist help statuses — prompt statuses (planning is off in this project)
140
+
141
+ pending Saved for a later session. \`runlist use <name>\` resumes it.
142
+ archived Consumed or set aside; kept under docs/prompts/archived/.
143
+
144
+ Plan and doc statuses come with planning; \`runlist setup\` turns it on.`;
145
+
111
146
  const HELP = {
112
147
  _main: `runlist v${pkg.version} — frontmatter markdown document manager
113
148
 
114
149
  Common commands:
150
+ setup Guided project setup; preview changes before applying
115
151
  plans Live plans (excludes archived)
152
+ show <file> Plan/doc outline and open boxes; --section reads one section
116
153
  prompts Prompt queue/admin (list, next, archive, new)
117
154
  briefing Full briefing with plan counts + next steps
118
155
  agent-context Compact bounded JSON context for agents
@@ -140,7 +177,8 @@ sessions keep making, and to log every one for audit:
140
177
  {"matcher":"Bash|Read|Edit|Write","hooks":[{"type":"command","command":"runlist guard"}]}
141
178
 
142
179
  Rules:
143
- commit-prompt deny git add/commit of a (often gitignored) saved prompt
180
+ commit-prompt policy git add/commit including a pending prompt:
181
+ untracked warns, tracked allows, local denies
144
182
  cat-prompt warn cat/less/head of a docs/prompts/*.md (use \`runlist use\`)
145
183
  read-prompt warn Read tool on a saved prompt (use \`runlist use\`)
146
184
  edit-status deny CHANGING a \`status:\` line — via Edit/Write or in-place
@@ -202,15 +240,32 @@ host gets that a different way:
202
240
  \`runlist.js\` without that stamp is treated as hand-authored and is
203
241
  never overwritten or removed without --force.
204
242
 
243
+ Before installing, warns if no persistent CLI is on PATH. Temporary npx,
244
+ pnpm dlx, bunx, Yarn wrapper bins and npm-injected project bins do not
245
+ count. Run \`npm install -g runlist\`, then restart/reload the host.
246
+ --json reports persistentCliMissing only when true.
247
+
205
248
  Writing outside the repo is why this is an explicit verb: \`runlist doctor\`
206
249
  reports a missing integration but never installs one.`,
207
250
 
208
- update: `runlist update — update the runlist CLI and the Claude Code plugin together
251
+ update: `runlist update — update the CLI and already installed host integrations
209
252
 
210
253
  runlist update npm i -g runlist + claude plugin update runlist@runlist
211
254
  runlist update --check report CLI vs plugin versions, do nothing (network-free)
255
+ runlist update --check --json offline executable/profile/source report; no host programs run
212
256
  runlist update --cli-only only the npm CLI
213
- runlist update --plugin-only only the plugin
257
+ runlist update --plugin-only only installed host integrations
258
+ runlist update --host codex only this installed host integration (no npm CLI/GUI update)
259
+ runlist update --host claude --claude-config /absolute/profile --claude-bin /absolute/native/claude
260
+
261
+ --host accepts claude, codex or opencode and conflicts with --cli-only.
262
+ Repeat --claude-config to select separate profiles; shared stores update once.
263
+ --claude-bin preserves the chosen executable path. Unknown wrapper/profile
264
+ mapping requires an explicit native binary; no profile is guessed.
265
+ Missing integrations stay missing during update; use install or an explicitly
266
+ selected doctor repair. Unmarked files remain user-owned. Catalog/source parity
267
+ and installed records are checked separately from live activation, which still
268
+ requires reload/trust. Unpublished checkout differences are labelled separately.
214
269
 
215
270
  The plugin and CLI ship in lockstep; a release bumps both. Updating the plugin
216
271
  requires a session restart (or /reload-plugins) to apply. The plugin step needs
@@ -253,7 +308,7 @@ Populated by the \`runlist guard\` PreToolUse hook — see \`runlist help guard\
253
308
  'help:all': `runlist v${pkg.version} — full command list
254
309
 
255
310
  View & Query:
256
- hud [--json] Command primer + pending-prompt triage — silent when clean
311
+ hud [--json] Session-start handoff orientation + pending-prompt triage
257
312
  list [--verbose] [--json] List docs grouped by status (default command)
258
313
  briefing [--json] Full briefing with plan status counts + next steps
259
314
  context [--summarize] [--json] Full briefing (LLM-oriented; use --json --compact for bounded JSON)
@@ -327,7 +382,9 @@ Create & Export:
327
382
  export [--format md|html|json] Export docs as markdown, HTML, or JSON
328
383
 
329
384
  Setup:
385
+ setup [--yes] [--json] Guided setup, meaningful first plan and selected document adoption
330
386
  init Create starter config + docs directory
387
+ config show|get|set Inspect project layout/policy; explicitly edit prompt tracking
331
388
  install [claude|codex|opencode] Install the agent-host integration (no arg = status)
332
389
  update [--check|--cli-only|--plugin-only] Update the CLI + Claude Code plugin (--check reports skew, no network)
333
390
  statuses [list|add|set|remove|migrate] Manage per-project status taxonomy
@@ -713,23 +770,26 @@ Options:
713
770
 
714
771
  hud: `runlist hud — actionable triage for session start
715
772
 
716
- Prints the runlist command primer (the verb cheat-sheet) plus, in --json mode,
717
- pending prompts and the check-error count for programmatic callers.
718
-
719
- Silent when there's nothing actionable — designed for SessionStart hooks where
720
- zero noise is the right default. Distinct from \`runlist briefing\`, which
721
- dumps the full plan-status pipeline and per-plan next_step bodies (kilobytes
773
+ Prints pending prompts, this session's owned plan, and actionable notices.
774
+ Before the first saved handoff, a configured repo with no pending prompts or
775
+ owned plan gets one orientation line. Otherwise silent when nothing needs
776
+ attention. --json returns structured state and the check-error count.
777
+ Distinct from \`runlist briefing\`, which dumps the full plan-status pipeline and per-plan next_step bodies (kilobytes
722
778
  on large repos). Use hud for ergonomic session boot; use briefing for
723
779
  explicit "give me the full picture."
724
780
 
725
- The pending-prompts line tells Claude to consume them via
726
- \`runlist prompts use <file>\` rather than reading/cat'ing — that atomically
727
- prints the body and archives the prompt so it cannot be double-consumed.
781
+ The pending-prompts line says to resume only when asked, via
782
+ \`runlist use <name>\`. --prompt-submit reads a hook JSON payload from stdin:
783
+ save requests get baton instructions; pickup requests get a distinct hint only
784
+ when prompts are pending. Text hooks read prompt frontmatter and ownership
785
+ records without building the index.
728
786
 
729
787
  Recommended SessionStart hook (in ~/.claude/settings.json):
730
788
  "SessionStart": [{ "hooks": [{ "type": "command", "command": "runlist hud", "timeout": 5 }] }]
731
789
 
732
790
  Options:
791
+ --prompt-submit Request-specific save/pickup hint (hook JSON on stdin)
792
+ --subagent Compact workflow primer for child agents
733
793
  --json Output as JSON ({ owned, prompts, errors, previousSelf,
734
794
  fleet, recentRejections, misuseRecap, drift })`,
735
795
 
@@ -859,6 +919,17 @@ sync hub status rows, sync dates from git, regenerate the index, then show
859
919
  remaining issues.
860
920
 
861
921
  Modes:
922
+ --hosts Unified Claude/Codex/OpenCode inventory: executables,
923
+ profiles/shared stores, installed/expected versions,
924
+ source parity and unknown live-session loading.
925
+ Read-only, offline, no project JS import; supports --json.
926
+ --hosts --repair <h> Preview repairs for named hosts only; repeat --repair
927
+ for more hosts. Add --apply to execute those targets.
928
+ --dry-run always wins. --apply alone is refused here.
929
+ Use --claude-config /absolute/profile (repeatable) and
930
+ --claude-bin /absolute/native/claude for explicit targeting.
931
+ Repair may install a selected missing integration;
932
+ foreign/unreadable content and ambiguous wrappers are refused.
862
933
  (default) Auto-fix pass — previews by default since 0.37.0
863
934
  (F4). Use --apply (alias --yes) to actually write;
864
935
  explicit --dry-run still wins over --apply if both
@@ -1058,6 +1129,7 @@ What the draft does:
1058
1129
  - Draft opening with a \`---\` frontmatter block → those keys replace the
1059
1130
  scaffold's (status, surfaces, modules, current_state, next_step, …), so no
1060
1131
  frontmatter edit is needed afterwards. \`type:\` is fixed by <type>.
1132
+ A status invalid for that type is replaced with its normal starting status.
1061
1133
  To change status later, use \`runlist set <status> <file>\`.
1062
1134
 
1063
1135
  The repo's own types, starting statuses and folders are listed at the end of
@@ -1242,13 +1314,83 @@ Examples:
1242
1314
 
1243
1315
  Use --dry-run (-n) to preview changes without writing anything.`,
1244
1316
 
1245
- init: `runlist init — create starter config and docs directory
1317
+ setup: `runlist setup — guided project setup, with a concrete preview
1246
1318
 
1247
- Creates runlist.config.mjs, docs/, and docs/docs.md in the current
1248
- directory. Skips any files that already exist.
1319
+ Run in a terminal for choices and confirmation. Without an interactive terminal,
1320
+ setup previews only; add --yes to apply selected actions. --dry-run always
1321
+ previews without writing. The CLI works without the optional desktop app.
1249
1322
 
1250
- If docs/ already contains .md files, auto-detects statuses, surfaces,
1251
- modules, and reference fields to pre-populate the config.`,
1323
+ --docs-root <path> Fresh project docs folder (default docs); existing roots preserved
1324
+ --prompt-tracking <p> untracked (recommended), tracked, or local; existing choice preserved
1325
+ --gitignore Explicitly add missing ignore rules; default leaves ignore files alone
1326
+ --host <host> none (default), claude, codex, or opencode; explicit host install
1327
+ --first-plan <slug> Create a plan from the following user-supplied state and next action
1328
+ --current-state <text> What is true now (required with --first-plan)
1329
+ --next-step <text> Concrete next action (required with --first-plan)
1330
+ --adopt <path> Preview/adopt selected untagged Markdown; repeat for more paths
1331
+ --no-carry-prompts Leave pre-setup prompts and plans in place (default carries them over)
1332
+ --json Structured preview or completed/outstanding report
1333
+ --yes Apply choices without terminal questions
1334
+
1335
+ Examples:
1336
+ runlist setup
1337
+ runlist setup --docs-root notes --dry-run
1338
+ runlist setup --first-plan example --current-state "Requirements agreed." --next-step "Build a prototype." --dry-run
1339
+ runlist setup --first-plan example --current-state "Requirements agreed." --next-step "Build a prototype." --yes
1340
+ runlist setup --adopt docs/notes.md --dry-run
1341
+
1342
+ Existing JavaScript hooks, configured folders and prompt locations are preserved.
1343
+ In a handoffs-only project (the config the first baton writes), setup turns
1344
+ planning on: it rewrites that config's mode, widens its root from docs/prompts
1345
+ to docs, adds plans and an index, and leaves the prompts where they are. The
1346
+ preview counts the existing Markdown under docs/ that planning starts checking.
1347
+ --docs-root cannot choose another folder there.
1348
+ Fresh setup carries runlist prompts and plans (including archives, and prompts an
1349
+ older runlist left in a root prompts/) into the new folders, fixing references
1350
+ and local handoff/ownership state. Foreign files stay put.
1351
+ Setup must finish transaction recovery before writing the first config; unresolved
1352
+ transactions stay Outstanding. Inspect them with runlist doctor --transactions.
1353
+ Untracked does not mean ignored: git add -A may include prompts and runtime state.
1354
+ Setup lists a missing persistent CLI on PATH under Outstanding.
1355
+ JSON includes checks.host.persistentCliMissing only when true.
1356
+ Run \`npm install -g runlist\` before reloading.
1357
+ Setup reports pre-existing document issues and unfinished host integration;
1358
+ installed integration still needs a new/reloaded host session.`,
1359
+
1360
+ init: `runlist init [--prompt-tracking <untracked|tracked|local>] [--gitignore]
1361
+
1362
+ Creates starter config and missing scaffold files; respects configured roots
1363
+ and index paths. Existing files are preserved. Existing Markdown can be
1364
+ inspected to pre-populate a new config; adoption hints identify untagged docs.
1365
+
1366
+ New projects default to untracked prompts (recommended to reduce commit churn).
1367
+ Untracked does not mean ignored: git add -A can still include them. Existing
1368
+ configs keep their policy; use runlist config set promptTracking <value> to change it.
1369
+ No ignore files are edited by default; runtime state in .runlist/ and legacy
1370
+ .dotmd/ may also be staged by git add -A. --gitignore explicitly appends missing
1371
+ state rules and prompt rules (unless tracked). Preview with --dry-run.
1372
+ Host integration is separate: runlist install <claude|codex|opencode>.`,
1373
+
1374
+ config: `runlist config — inspect or change supported onboarding settings
1375
+
1376
+ runlist config show [--json]
1377
+ runlist config get promptTracking [--json]
1378
+ runlist config get root [--json]
1379
+ runlist config get promptDirectory [--json]
1380
+ runlist config set promptTracking <untracked|tracked|local> [--dry-run] [--json]
1381
+
1382
+ Shows effective values and their sources: promptTracking, mode (plans, or
1383
+ handoffs when planning is off), root, docsRoots, index.path, archiveDir and
1384
+ promptDirectory. Resolved roots, index and prompt
1385
+ directory use absolute paths; root keeps its configured string or array.
1386
+ Only promptTracking is CLI-editable; other settings give manual config guidance.
1387
+ Inspection imports the trusted project's JavaScript config but invokes no hooks.
1388
+ untracked recommends leaving pending prompts out of commits and warns in guards;
1389
+ tracked allows prompt Git operations; local denies them (legacy default for
1390
+ existing configs without an explicit choice). A new project defaults to untracked.
1391
+ Untracked does not mean ignored; git add -A can include pending prompts.
1392
+ Writes preserve hooks and refuse symlinks, computed or ambiguous exports. Run setup first
1393
+ if no config exists. Changing policy never edits ignore files or Git state.`,
1252
1394
 
1253
1395
  plans: `runlist plans — list live plans (excludes archived by default)
1254
1396
 
@@ -1359,10 +1501,23 @@ Usage (write the resume to a file first, then pick the form that matches):
1359
1501
  runlist baton @/tmp/draft.md # a plan is in-session: save resume-<plan-slug>, release the plan
1360
1502
  runlist baton <plan-file> @/tmp/draft.md # hand off a named plan
1361
1503
  runlist baton <slug> @/tmp/draft.md # no plan: save resume-<slug>, change nothing else
1362
- runlist baton --done [<plan-file>] # after a plan handoff: confirm the plan was brought up to date
1504
+ runlist baton --done [<plan-file>] --next-step "..." # after a plan handoff: write next_step, confirm it
1363
1505
  The resume can also come from stdin (\`-\`, or a pipe) or --message "..." for one-liners.
1364
1506
  Baton prints the prompt name it saved, as its last line too, and, in plan mode, the git commit to run.
1365
1507
 
1508
+ No setup needed: in a Git repository with no runlist config, the first saved
1509
+ prompt also creates a handoffs-only runlist.config.mjs at the worktree root,
1510
+ with prompts in docs/prompts/ (planning stays off until \`runlist setup\`).
1511
+ Prompts an older runlist saved in a root prompts/ move to docs/prompts/ first,
1512
+ in one line of output (--dry-run lists the moves); a name collision there
1513
+ refuses the save and writes no config. Other files in prompts/ stay put.
1514
+ Only type: prompt files in docs/prompts/ are runlist's: plan views and reads
1515
+ filtered to another --type say planning is off; plan actions and files outside
1516
+ that folder refuse with the same line. Outside a Git working tree
1517
+ baton refuses. Plans and prompts saved before any config elsewhere in the
1518
+ repository stay where they are; the planning-off line counts the ones
1519
+ \`runlist setup\` brings in.
1520
+
1366
1521
  The resume (10–20 lines): the next concrete decision plus any gotchas, and the
1367
1522
  paths of the plans/docs it concerns — not a recap of the plan body.
1368
1523
 
@@ -1376,6 +1531,8 @@ Options:
1376
1531
  --template Print a resume draft (or --subagent draft), using saved Markdown settings
1377
1532
  --replace Replace exactly one pending handoff; archive its prior text
1378
1533
  --done Report a plan handoff done once next_step changed after the prompt was saved
1534
+ --next-step "..." With --done: write the plan's next_step (one line; must differ from the saved one)
1535
+ --current-state "..." With --done: also write current_state (never counts as done on its own)
1379
1536
  --json Structured repository/session/generated file result
1380
1537
  --dry-run, -n Preview without writing
1381
1538
 
@@ -1395,23 +1552,31 @@ Examples:
1395
1552
  runlist baton docs/plans/auth.md @/tmp/draft.md
1396
1553
  runlist baton docs/plans/auth.md @/tmp/new-draft.md --replace
1397
1554
  runlist baton --status paused --note "blocked on review" @/tmp/d.md
1555
+ runlist baton --done --next-step "wire the retry path" --current-state "parser merged"
1398
1556
  cat /tmp/draft.md | runlist baton
1399
1557
 
1400
1558
  Plan mode (a plan is in-session, or you pass one) publishes in one atomic
1401
1559
  cooperating transaction:
1402
1560
  1. A resume prompt named resume-<plan-slug>, stamped with a plan: link so
1403
- consuming it re-claims the plan (see \`runlist use\`). The prompt is
1404
- session-local — the next session's hud surfaces it; never paste resume text
1561
+ consuming it re-claims the plan (see \`runlist use\`). The next session's
1562
+ hud surfaces it; never paste resume text
1405
1563
  into chat.
1406
1564
  2. Releases the plan: one status flip, in-session → active by default.
1407
1565
  3. Defers the shared generated index and prints exact repository-only commit
1408
- guidance. Prompt and ownership records stay session-local and OUT of the
1409
- pathspec.
1566
+ guidance for the plan. Prompts stay out of this generated pathspec; use
1567
+ \`runlist config get promptTracking\` for the project's prompt Git policy.
1568
+ Ownership records are local runtime state.
1410
1569
  4. When the plan was in-session and has a next_step field, the handoff is not
1411
- done yet: bring the plan up to date (the body, then next_step), then run
1412
- \`runlist baton --done\`, which refuses while next_step still reads as it
1413
- did when the prompt was saved. The prompt is saved first, so a stop between
1414
- the two steps loses nothing, and \`--done <plan-file>\` works from any session.
1570
+ done yet: update the plan body first, then run
1571
+ \`runlist baton --done --next-step "<next move>"\` (plus
1572
+ \`--current-state "..."\` if that changed). It writes the frontmatter,
1573
+ creating a missing key, and refuses a next_step that still reads as it did
1574
+ when the prompt was saved. The prompt is saved first, so a stop between the
1575
+ two steps loses nothing, and \`--done <plan-file>\` works from any session.
1576
+ \`--dry-run\` with --next-step previews the write and removes nothing.
1577
+ The fields go only to the plan at the path the handoff was saved for. If
1578
+ it moved, update it where it is now and run \`--done <new-path>\`, or clear
1579
+ the handoff with \`--done <old-path>\`.
1415
1580
  Which plan? Pass it explicitly, or baton resolves exactly one plan owned by
1416
1581
  this authoritative session. Journal entries and global in-session counts never
1417
1582
  grant ownership. A live pickup-hook delivery lease blocks release and force
@@ -1602,6 +1767,7 @@ heading matches case-insensitively: exact first, then as a prefix, then as a
1602
1767
  substring; other matches are named. A section read stops at 500 lines or
1603
1768
  60 KB and prints the command that continues it. Headings and boxes inside code
1604
1769
  fences are ignored. Several files at once get a shorter outline each.
1770
+ There is no --lines option; continue with --section "<heading>" --from <line>.
1605
1771
 
1606
1772
  A phase heading shows its own boxes as [done/total], → marks the active phase
1607
1773
  (the one \`use\` would point at), and a marker its boxes contradict is named
@@ -1849,6 +2015,7 @@ function splitGlobalArgs(args) {
1849
2015
  let explicitConfig = null;
1850
2016
  let rootArg = null;
1851
2017
  let typeArg = null;
2018
+ const typeArgs = [];
1852
2019
  let dryRun = false;
1853
2020
  let verbose = false;
1854
2021
 
@@ -1863,7 +2030,7 @@ function splitGlobalArgs(args) {
1863
2030
  if (local) rest.push(arg, next);
1864
2031
  else if (arg === '--config') explicitConfig = next;
1865
2032
  else if (arg === '--root') rootArg = next;
1866
- else typeArg = next;
2033
+ else { typeArg = next; typeArgs.push(next); }
1867
2034
  i += 1;
1868
2035
  continue;
1869
2036
  }
@@ -1876,7 +2043,49 @@ function splitGlobalArgs(args) {
1876
2043
  rest.push(arg);
1877
2044
  }
1878
2045
 
1879
- return { command, rest, explicitConfig, rootArg, typeArg, dryRun, verbose };
2046
+ return { command, rest, explicitConfig, rootArg, typeArg, typeArgs, dryRun, verbose };
2047
+ }
2048
+
2049
+ // Every value given for `option`, as `--type x` or `--type=x`.
2050
+ function optionValues(args, option) {
2051
+ const values = [];
2052
+ for (let i = 0; i < args.length; i += 1) {
2053
+ if (args[i] === option && args[i + 1] !== undefined) values.push(args[++i]);
2054
+ else if (args[i].startsWith(`${option}=`)) values.push(args[i].slice(option.length + 1));
2055
+ }
2056
+ return values;
2057
+ }
2058
+
2059
+ // Where planning is off, the briefing views end with the planning-off line.
2060
+ async function writePlanningOffLine(config) {
2061
+ const { planningOffLine } = await import('../src/handoffs-scope.mjs');
2062
+ const { documentsWaitingForSetup } = await import('../src/setup-waiting.mjs');
2063
+ process.stdout.write(`${planningOffLine(await documentsWaitingForSetup(config))}\n`);
2064
+ }
2065
+
2066
+ // A type a command takes as a positional argument rather than --type, which
2067
+ // the planning gate reads like one: `statuses migrate <type>` and the new
2068
+ // value of `migrate type <old> <new>`.
2069
+ function positionalTypeSelector(command, args) {
2070
+ const positional = args.filter(arg => !arg.startsWith('-'));
2071
+ if (command === 'statuses' && positional[0] === 'migrate' && positional[1]) return { type: positional[1], spelling: `statuses migrate ${positional[1]}` };
2072
+ if (command === 'migrate' && positional[0] === 'type' && positional[2]) return { type: positional[2], spelling: `migrate type ${positional[1]} ${positional[2]}` };
2073
+ return null;
2074
+ }
2075
+
2076
+ // The verbs that can change a document, for the carry that runs before them
2077
+ // in a repository with no config (src/handoffs-mode.mjs carryBeforeChange).
2078
+ const DOCUMENT_CHANGES = new Set(['use', 'next', 'archive', 'set', 'status', 'bulk', 'bulk-tag', 'touch', 'rename', 'migrate', 'fix-refs', 'sync-status', 'fix-membership', 'record', 'decision']);
2079
+ function changesDocuments(command, args) {
2080
+ if (DOCUMENT_CHANGES.has(command)) return true;
2081
+ if (command === 'prompts') return ['use', 'resume', 'next', 'archive', 'link', 'hold', 'shelve', 'unhold', 'unshelve'].includes(args[0]);
2082
+ if (command === 'runlist' || command === 'roadmap') return args.some(arg => ['next', 'add', 'remove', 'reorder'].includes(arg));
2083
+ if (command === 'yardstick') return ['mark', 'close', 'fold', 'clear'].includes(args[0]);
2084
+ if (command === 'lint' || command === 'check') return args.includes('--fix');
2085
+ // Not the transaction, claim and session tools: they are how a stuck move or
2086
+ // claim is resolved, so nothing may run ahead of them.
2087
+ if (command === 'doctor') return (args.includes('--apply') || args.includes('--yes')) && !args.some(arg => ['--transactions', '--claims', '--session'].includes(arg));
2088
+ return false;
1880
2089
  }
1881
2090
 
1882
2091
  async function main() {
@@ -1891,7 +2100,7 @@ async function main() {
1891
2100
  // Before-command options are global. After the command, a schema-declared
1892
2101
  // local option wins; otherwise the historical anywhere-global form remains.
1893
2102
  const parsed = splitGlobalArgs(args);
1894
- let { command, explicitConfig, rootArg, typeArg, dryRun, verbose } = parsed;
2103
+ let { command, explicitConfig, rootArg, typeArg, typeArgs, dryRun, verbose } = parsed;
1895
2104
  let restArgs = parsed.rest;
1896
2105
  const captureHelpConfig = async topic => {
1897
2106
  _resolvedCommand = 'help';
@@ -1947,12 +2156,48 @@ async function main() {
1947
2156
  }
1948
2157
  }
1949
2158
 
2159
+ // One help renderer for `runlist help <cmd>` and `runlist <cmd> --help`, so
2160
+ // both spellings say the same thing, including where planning is off.
2161
+ const renderCommandHelp = async (name, helpArgs) => {
2162
+ if (_resolvedConfig?.mode === 'handoffs') {
2163
+ if (commandPlanning(name, helpArgs)) {
2164
+ const { planningOffLine } = await import('../src/handoffs-scope.mjs');
2165
+ const { documentsWaitingForSetup } = await import('../src/setup-waiting.mjs');
2166
+ return `${planningOffLine(await documentsWaitingForSetup(_resolvedConfig))}\n`;
2167
+ }
2168
+ const { handoffsCommandHelp } = await import('../src/handoffs-help.mjs');
2169
+ const text = handoffsCommandHelp(name);
2170
+ if (text) return `${text}\n`;
2171
+ }
2172
+ const focused = focusedCommandHelp(name, helpArgs);
2173
+ let text = focused?.fallback ? `No focused help for this request; showing complete \`runlist ${name} --help\`.\n\n` : '';
2174
+ text += `${focused?.text ?? HELP[name] ?? commandUsage(name)}\n`;
2175
+ if (name === 'new' && !focused?.text) {
2176
+ // Best effort: outside a runlist repo, or with a broken config, the
2177
+ // static help above is the whole answer.
2178
+ try {
2179
+ const repoConfig = _resolvedConfig;
2180
+ if (repoConfig?.configFound !== false) {
2181
+ const { newHelpForRepo } = await import('../src/new.mjs');
2182
+ text += `\n${newHelpForRepo(repoConfig)}\n`;
2183
+ }
2184
+ } catch { /* static help already printed */ }
2185
+ }
2186
+ return text;
2187
+ };
2188
+
1950
2189
  if (command === 'help' || command === '--help' || command === '-h') {
1951
2190
  const topic = restArgs[0];
1952
- await captureHelpConfig(topic ? (HELP[topic] || HELP[`help:${topic}`] ? topic : '(unknown)') : '_main');
2191
+ const topicCommand = topic && commandDefinition(topic) && !HELP[`help:${topic}`] ? commandDefinition(topic).name : null;
2192
+ await captureHelpConfig(topic ? (HELP[topic] || HELP[`help:${topic}`] || topicCommand ? topic : '(unknown)') : '_main');
2193
+ if (_resolvedConfig?.mode === 'handoffs' && (!topic || topic === 'all' || topic === 'statuses')) {
2194
+ process.stdout.write(`${topic === 'statuses' ? HANDOFFS_STATUS_HELP : HANDOFFS_HELP}\n`);
2195
+ return;
2196
+ }
1953
2197
  if (topic) {
1954
2198
  const key = `help:${topic}`;
1955
2199
  if (HELP[key]) { process.stdout.write(`${HELP[key]}\n`); return; }
2200
+ if (topicCommand) { process.stdout.write(await renderCommandHelp(topicCommand, [])); return; }
1956
2201
  if (HELP[topic]) { process.stdout.write(`${HELP[topic]}\n`); return; }
1957
2202
  process.stderr.write(`Unknown help topic: ${topic}\n\nAvailable topics: all, statuses\nPer-command help: runlist <cmd> --help\n`);
1958
2203
  process.exitCode = 1;
@@ -1972,30 +2217,23 @@ async function main() {
1972
2217
  );
1973
2218
  const doctorExplicitApply = args.includes('--apply') || args.includes('--yes');
1974
2219
  const effectiveDryRun = dryRun || (command === 'doctor' && !doctorSubMode && !doctorExplicitApply);
1975
- const passiveMachineContext = command === 'agent-context'
2220
+ const passiveMachineContext = command === 'agent-context' || command === 'config' || command === 'setup'
1976
2221
  || (command === 'context' && args.includes('--json') && args.includes('--compact'));
1977
2222
  // Git/frontmatter drift is validation work, not index construction. Keep the
1978
2223
  // bounded history scan on commands that report or repair that drift; ordinary
1979
2224
  // reads still run schema/reference validation without walking 10k commits.
1980
2225
  const gitStaleness = command === 'check' || command === 'doctor';
1981
2226
  _suppressObservability = effectiveDryRun || command === 'hud' || passiveMachineContext;
2227
+ const hostDiagnostics = command === 'doctor' && restArgs.includes('--hosts');
2228
+ const updateInspection = command === 'update' && restArgs.includes('--check');
2229
+ if (hostDiagnostics || updateInspection) _suppressObservability = true;
1982
2230
 
1983
2231
  // Per-command help
1984
2232
  if (args.includes('--help') || args.includes('-h')) {
1985
2233
  requireCommandPolicy(command, dispatchPolicy);
1986
- await captureHelpConfig(command);
1987
- process.stdout.write(`${HELP[command] ?? commandUsage(command)}\n`);
1988
- if (command === 'new') {
1989
- // Best effort: outside a runlist repo, or with a broken config, the
1990
- // static help above is the whole answer.
1991
- try {
1992
- const repoConfig = _resolvedConfig;
1993
- if (repoConfig?.configFound !== false) {
1994
- const { newHelpForRepo } = await import('../src/new.mjs');
1995
- process.stdout.write(`\n${newHelpForRepo(repoConfig)}\n`);
1996
- }
1997
- } catch { /* static help already printed */ }
1998
- }
2234
+ if (!hostDiagnostics && !updateInspection) await captureHelpConfig(command);
2235
+ const helpArgs = restArgs.filter(arg => arg !== '--help' && arg !== '-h');
2236
+ process.stdout.write(await renderCommandHelp(command, _resolvedConfig?.mode === 'handoffs' ? helpArgs : restArgs));
1999
2237
  return;
2000
2238
  }
2001
2239
 
@@ -2007,6 +2245,37 @@ async function main() {
2007
2245
  return;
2008
2246
  }
2009
2247
 
2248
+ // Host diagnostics must work before project setup and with a broken project
2249
+ // config. This branch deliberately imports no project JavaScript and runs no
2250
+ // state migration or document sweep. Only selected repairs can write.
2251
+ if (command === 'doctor' && restArgs.includes('--hosts')) {
2252
+ requireCommandPolicy(command, dispatchPolicy);
2253
+ try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
2254
+ const { runDoctorHosts } = await import('../src/doctor-hosts.mjs');
2255
+ await runDoctorHosts(restArgs, { repoRoot: process.cwd() }, { dryRun });
2256
+ return;
2257
+ }
2258
+
2259
+ if (command === 'update') {
2260
+ requireCommandPolicy(command, dispatchPolicy);
2261
+ try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
2262
+ const { runUpdate } = await import('../src/update.mjs');
2263
+ await runUpdate(restArgs, { repoRoot: process.cwd() }, { dryRun });
2264
+ return;
2265
+ }
2266
+
2267
+ if (command === 'install') {
2268
+ requireCommandPolicy(command, dispatchPolicy);
2269
+ try { restArgs = validateCommandArgs(command, restArgs); } catch (err) { die(err.message); }
2270
+ const { runInstall } = await import('../src/install.mjs');
2271
+ await runInstall(restArgs, { repoRoot: process.cwd() }, { dryRun });
2272
+ return;
2273
+ }
2274
+
2275
+ if (command === 'doctor' && restArgs.some(arg => ['--repair', '--claude-config', '--claude-bin'].includes(arg))) {
2276
+ die('Host repair and Claude selectors require runlist doctor --hosts.');
2277
+ }
2278
+
2010
2279
  if (command === 'baton' && restArgs.includes('--template')) {
2011
2280
  _suppressObservability = true;
2012
2281
  requireCommandPolicy(command, dispatchPolicy);
@@ -2049,7 +2318,7 @@ async function main() {
2049
2318
  // nags. `install`/`doctor`/`update` are skipped: they report this themselves,
2050
2319
  // and telling `runlist install opencode` to run `runlist install opencode` is
2051
2320
  // noise.
2052
- if (!['install', 'doctor', 'update', 'hud', 'guard'].includes(command)) {
2321
+ if (!['install', 'doctor', 'update', 'hud', 'guard', 'setup', 'config'].includes(command)) {
2053
2322
  const { degradedIdentityNotice } = await import('../src/host-integration.mjs');
2054
2323
  const notice = degradedIdentityNotice(config?.repoRoot, { version: pkg.version });
2055
2324
  if (notice) process.stderr.write(`${notice}\n`);
@@ -2060,7 +2329,9 @@ async function main() {
2060
2329
  value: { dryRun, passive: command === 'hud' || passiveMachineContext, suppressSideEffects, gitStaleness },
2061
2330
  enumerable: false,
2062
2331
  });
2063
- if (!suppressSideEffects) {
2332
+ // Init owns scaffolding and validates policy before writing. Do not migrate
2333
+ // legacy state ahead of that validation (including a refused init request).
2334
+ if (!suppressSideEffects && command !== 'init') {
2064
2335
  const { migrateStateDirectory } = await import('../src/state-migration.mjs');
2065
2336
  const { LEGACY_STATE_DIR, STATE_DIR } = await import('../src/naming.mjs');
2066
2337
  const migration = migrateStateDirectory(config.repoRoot);
@@ -2075,17 +2346,89 @@ async function main() {
2075
2346
  // present in the centralized command policy registry.
2076
2347
  if (!config.presets[command]) requireCommandPolicy(command, dispatchPolicy);
2077
2348
 
2349
+ // Handoffs-only projects (src/handoffs-mode.mjs) have no plans or docs. A
2350
+ // command the registry marks as needing planning, or any command whose
2351
+ // arguments ask for a type other than prompts, says planning is off and
2352
+ // names the command that turns it on. The arguments are read as the command
2353
+ // will run them: a preset's own arguments included, every --type counted
2354
+ // (repeated, comma-separated, or --type=x), and the positional type a
2355
+ // command takes (`statuses migrate <type>`, `migrate type <old> <new>`).
2356
+ // This runs before argument validation so `runlist runlist` with no hub
2357
+ // gets the same answer.
2358
+ if (config.mode === 'handoffs') {
2359
+ const { requirePlanning } = await import('../src/handoffs-scope.mjs');
2360
+ const preset = config.presets[command];
2361
+ const effective = [...(preset ?? []), ...restArgs];
2362
+ const json = effective.includes('--json');
2363
+ const types = [...typeArgs, ...optionValues(effective, '--type')].flatMap(value => value.split(',')).map(type => type.trim()).filter(Boolean);
2364
+ const others = types.filter(type => type !== 'prompt');
2365
+ const positionalType = preset ? null : positionalTypeSelector(command, restArgs);
2366
+ const planning = commandPlanning(command, restArgs);
2367
+ const typed = !planning && others.length > 0;
2368
+ const selected = !planning && !typed && positionalType && positionalType.type !== 'prompt';
2369
+ const requirement = planning?.requirement ?? (typed ? (preset || commandPolicy(command)?.mutation === 'none' ? 'view' : 'action') : selected ? 'action' : null);
2370
+ if (requirement) {
2371
+ for (const w of config.configWarnings ?? []) warn(w);
2372
+ const what = selected ? `\`runlist ${positionalType.spelling}\``
2373
+ : `\`runlist ${[command, planning?.subcommand].filter(Boolean).join(' ')}${typed ? ` --type ${others.join(',')}` : ''}\``;
2374
+ const { documentsWaitingForSetup } = await import('../src/setup-waiting.mjs');
2375
+ if (requirePlanning(config, what, requirement, { json, waiting: await documentsWaitingForSetup(config) })) return;
2376
+ }
2377
+ }
2378
+
2078
2379
  try {
2079
2380
  restArgs = validateCommandArgs(command, restArgs, { preset: Boolean(config.presets[command]) });
2080
2381
  } catch (err) {
2081
2382
  die(err.message);
2082
2383
  }
2083
2384
 
2385
+ // Before any config, prompts an older runlist saved in the root prompts/
2386
+ // move to docs/prompts/ ahead of a command that changes documents
2387
+ // (src/handoffs-mode.mjs), so none is consumed, archived or rewritten where
2388
+ // runlist no longer writes. A file argument naming a moved prompt follows it.
2389
+ if (!config.configFound && changesDocuments(command, restArgs)) {
2390
+ const { carryBeforeChange } = await import('../src/handoffs-mode.mjs');
2391
+ const carried = await carryBeforeChange(config, { dryRun: effectiveDryRun });
2392
+ if (carried?.preview) {
2393
+ process.stderr.write(`[dry-run] \`runlist ${command}\` then runs on the moved prompts; nothing else is previewed until they have moved.\n`);
2394
+ return;
2395
+ }
2396
+ if (carried) {
2397
+ restArgs = restArgs.map(arg => {
2398
+ const move = carried.moves.find(item => [process.cwd(), config.repoRoot].some(base => path.resolve(base, arg) === item.source));
2399
+ return move ? move.target : arg;
2400
+ });
2401
+ // Run from a subfolder, the docs root is that subfolder, and the moved
2402
+ // prompts are in docs/prompts/ at the project's top: the command reads
2403
+ // them there.
2404
+ if (path.resolve(config.docsRoot) !== path.resolve(config.repoRoot)) {
2405
+ const execution = config._execution;
2406
+ config = await resolveConfig(config.repoRoot);
2407
+ Object.defineProperty(config, '_execution', { value: execution, enumerable: false });
2408
+ _resolvedConfig = config;
2409
+ }
2410
+ }
2411
+ }
2412
+
2084
2413
  // Init — runInit re-resolves the config from disk internally (after any
2085
2414
  // starter-config write), so we don't need to pre-pass it.
2086
2415
  if (command === 'init') {
2087
2416
  const { runInit } = await import('../src/init.mjs');
2088
- await runInit(process.cwd(), config, { dryRun });
2417
+ await runInit(process.cwd(), config, { dryRun, argv: restArgs });
2418
+ return;
2419
+ }
2420
+
2421
+ if (command === 'config') {
2422
+ if (rootArg || typeArg) die('Config inspection does not use --root/--type query filters. Inspect root with runlist config get root; edit project layout manually.');
2423
+ const { runConfig } = await import('../src/config-command.mjs');
2424
+ await runConfig(restArgs, config, { dryRun });
2425
+ return;
2426
+ }
2427
+
2428
+ if (command === 'setup') {
2429
+ if (rootArg || typeArg) die('Setup does not use --root/--type query filters. Choose a fresh docs folder with --docs-root; existing configured roots are preserved.');
2430
+ const { runSetup } = await import('../src/setup.mjs');
2431
+ await runSetup(restArgs, config, { dryRun });
2089
2432
  return;
2090
2433
  }
2091
2434
 
@@ -2098,8 +2441,11 @@ async function main() {
2098
2441
  // runlist isn't used, so don't nag them about a missing config (they no-op
2099
2442
  // cleanly on their own). The warning is still useful for interactive commands.
2100
2443
  const HOOK_COMMANDS = new Set(['hud', 'guard']);
2101
- if (!config.configFound && command !== 'init' && !HOOK_COMMANDS.has(command)) {
2102
- warn('No runlist config found — using defaults. Run `runlist init` to create one.');
2444
+ // A prompt saved here creates a handoffs-only config itself (src/handoffs-mode.mjs).
2445
+ const savesPrompt = command === 'baton' || (command === 'new' && restArgs.find(a => !a.startsWith('-')) === 'prompt')
2446
+ || (command === 'prompts' && restArgs[0] === 'new');
2447
+ if (!config.configFound && command !== 'init' && !HOOK_COMMANDS.has(command) && !savesPrompt) {
2448
+ warn('No runlist config found — using defaults. Run `runlist setup` to create one and carry over saved prompts and plans.');
2103
2449
  }
2104
2450
 
2105
2451
  if (config.configWarnings && config.configWarnings.length > 0) {
@@ -2226,7 +2572,7 @@ async function main() {
2226
2572
  // src/baton.mjs for why this is a single verb and not a skill choreography.
2227
2573
  if (command === 'baton') {
2228
2574
  const { runBaton } = await import('../src/baton.mjs');
2229
- await runBaton(restArgs, config, { dryRun });
2575
+ await runBaton(restArgs, config, { dryRun, verbose });
2230
2576
  return;
2231
2577
  }
2232
2578
  // `runlist next` is a top-level alias for `runlist use` with no arg — consume
@@ -2289,9 +2635,11 @@ async function main() {
2289
2635
  if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
2290
2636
  if (command === 'journal') { const { runJournal } = await import('../src/journal-read.mjs'); runJournal(restArgs, config); return; }
2291
2637
  if (command === 'pickup' || command === 'unpickup' || command === 'release' || command === 'finish') {
2638
+ if (config.mode === 'handoffs') die(`\`runlist ${command}\` was removed. Handoffs: \`runlist baton <slug> @<file>\` saves one, \`runlist use <name>\` resumes one, \`runlist prompts\` lists them.`);
2292
2639
  die(`\`runlist ${command}\` was removed — use the ownership-aware lifecycle verbs:\n runlist use <file> # atomically claim + mark in-session + print the plan\n runlist set <status> <file> # transition and release ownership when leaving in-session\n runlist archive <file> # close out atomically`);
2293
2640
  }
2294
2641
  if (command === 'runlist') { const { runRunlist } = await import('../src/runlist.mjs'); await runRunlist(restArgs, config, { dryRun }); return; }
2642
+ if (command === 'handoff' && config.mode === 'handoffs') die('`runlist handoff` was removed. Handoffs: `runlist baton <slug> @<file>` saves one, `runlist use <name>` resumes one, `runlist prompts` lists them.');
2295
2643
  if (command === 'handoff') { die('`runlist handoff` was removed in 0.31.0. Use `runlist prompts new <name>` to create a saved prompt instead. The .dotmd/handoffs/ sidecar mechanism no longer exists; see CHANGELOG.'); }
2296
2644
  if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
2297
2645
  if (command === 'set') { const { runSet } = await import('../src/lifecycle.mjs'); await runSet(restArgs, config, { dryRun }); return; }
@@ -2529,6 +2877,7 @@ async function main() {
2529
2877
 
2530
2878
  if (command === 'index') {
2531
2879
  if (!config.indexPath) {
2880
+ if (config.mode === 'handoffs') die('There is no generated index: planning is off in this project. `runlist setup` turns planning on and adds one (your prompts stay where they are).');
2532
2881
  die('Index generation is not configured. Add an `index` section to your runlist.config.mjs.');
2533
2882
  }
2534
2883
  const print = args.includes('--print');
@@ -2625,6 +2974,7 @@ async function main() {
2625
2974
  }, null, 2) + '\n');
2626
2975
  } else {
2627
2976
  process.stdout.write(renderBriefing(index, config));
2977
+ if (config.mode === 'handoffs') await writePlanningOffLine(config);
2628
2978
  }
2629
2979
  return;
2630
2980
  }
@@ -2696,6 +3046,7 @@ async function main() {
2696
3046
  return;
2697
3047
  }
2698
3048
  process.stdout.write(renderContext(index, config, { summarize, model }));
3049
+ if (config.mode === 'handoffs') await writePlanningOffLine(config);
2699
3050
  return;
2700
3051
  }
2701
3052