@sublang/playbook 12.3.0 → 13.0.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/docs/cli.md CHANGED
@@ -235,7 +235,7 @@ module's directory is a git repository, add `node_modules/` to its
235
235
 
236
236
  Interactive and headless commands write the same logical-session records in
237
237
  the configured [`sessions` directory](configuration.md#session-storage), whose
238
- default is `${XDG_STATE_HOME:-$HOME/.local/state}/playbook/sessions/`. A fresh
238
+ default is `${SPEX_HOME:-$HOME/.spex}/sessions/`. A fresh
239
239
  interactive child persists turn zero before printing `playbook: session <id>`
240
240
  and opening Boss input; a fresh headless turn returns the same kind of ID in
241
241
  `--json`. After the current writer exits or explicitly hands off, either
@@ -259,7 +259,7 @@ uncertainty rules below still apply to whichever record is selected
259
259
  ([DR-041](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/041-working-directory-aware-continuation.md)).
260
260
 
261
261
  A missing headless reply is read verbatim from stdin. Reopening restores the
262
- compiled Captain conversation, engagement stack, nested child boundary,
262
+ Captain recovery journal, engagement stack, nested child boundary,
263
263
  stable-player ledger, and absolute working directory without replaying a
264
264
  settled or pending child start. One exclusive writer owns the session, so a
265
265
  detached interactive pane child remains the owner until it shuts down; a
@@ -272,8 +272,9 @@ current `model`, `effort`, and optional `fastMode` settings apply to the next
272
272
  call. Boolean `false` explicitly resets model or effort to the provider
273
273
  default, but `fastMode: false` is a literal disabled request; omission selects
274
274
  the fast-mode provider default or inherits the player value at a role binding.
275
- The retained provider token is never silently replaced by a fresh conversation
276
- if a requested setting is not supported
275
+ Provider conversations are local hints. Missing hints start fresh from stored
276
+ context; a definite pre-execution rejection permits one fresh attempt.
277
+ Unsupported settings and ambiguous failures do not trigger that fallback
277
278
  ([[playbook-cli-22](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-22)],
278
279
  [DR-032](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/032-explicit-roles-session-players.md)).
279
280
 
@@ -287,36 +288,44 @@ one unambiguously. The stream is a mode-`0600` regular non-symlink file inside
287
288
  the private sessions directory. Readers consume only the complete
288
289
  newline-terminated prefix; a final partial line is not presented as a record.
289
290
 
290
- The replay projection recursively removes provider `resumeToken` fields and
291
- string-valued `resume` selections while preserving `resume: false`. It is
292
- therefore useful for following and presentation, but it cannot resume an agent
293
- conversation and does not replace the private canonical manifest. External
294
- hosts can follow this stream without taking the writer lease through the
295
- published [`@sublang/playbook/session-store`
296
- facade](embedding.md#sharing-the-cli-session-store).
297
-
298
- Streams are unbounded in this version: neither front end prunes them, and
299
- retention remains tied to the deferred session-deletion policy. Removing
300
- provider credentials does not make the remaining replay content non-sensitive;
301
- protect the prompts, replies, tool calls, events, and timestamps it contains as
302
- sensitive session data.
303
-
304
- Replay recording is fail-soft after the canonical session lease is valid. If
305
- initialization, sanitization, append, repair, publication synchronization, or a
306
- checkpoint fails, recording stops for that lease without changing the agent
307
- turn, durable session settlement, reply, or exit outcome. The headless command
308
- attempts this warning once on stderr:
309
-
310
- ```text
311
- playbook run: warning: replay history for session "<session-id>" may be incomplete; recording has stopped
291
+ Manifests use schema 7 and contain token-free recovery. The matching replay
292
+ stream carries immutable configuration and graph records, so history does not
293
+ require installed modules. Provider continuation hints live separately in
294
+ `<session-id>.hints.json` and stay local. A hint is consumed before use and
295
+ accepted only for the exact checkpoint that created it.
296
+
297
+ The manifest hashes its exact durable replay prefix. Missing or changed bytes,
298
+ saved incompleteness, or unsupported required context block continuation while
299
+ preserving readable history. A partial final line waits for completion; valid
300
+ unknown record kinds are skipped for presentation. New session creation fails
301
+ if its required context cannot be saved before work.
302
+
303
+ After work begins, replay failure stops recording and reports an incomplete
304
+ history warning without authorizing a repeated action. Recovery and uncertainty
305
+ remain durable; the incomplete marker persists across process restarts.
306
+ Release retains the lease if that marker cannot be saved.
307
+
308
+ Copy the manifest and matching replay together. Do not copy hints or leases.
309
+ Continuation also requires matching working-directory/module paths and
310
+ compatible runtimes; changed paths allow history only. Prompts, replies and
311
+ tool content remain sensitive even after structured continuation fields have
312
+ been removed. See the [shared storage contract](https://github.com/sublang-ai/playbook/blob/main/specs/packages/session-storage.md).
313
+
314
+ ### Migrating older sessions
315
+
316
+ Stop old writers and snapshot Spex home and `${XDG_STATE_HOME:-$HOME/.local/state}/playbook` before upgrading.
317
+ The ordinary default migrates known legacy formats into `~/.spex/sessions`, retaining history even when recovery is unavailable; explicit store overrides remain isolated.
318
+ Migration retains original files under `local/migrations/` beside the destination sessions directory, validates the new bundle, then removes the former replay and manifest.
319
+ Unknown or unsafe inputs stay in place with a diagnostic.
320
+
321
+ For an explicitly configured store, migrate each selected legacy session before continuing it:
322
+
323
+ ```sh
324
+ playbook migrate-session <id> [--with <path>]...
312
325
  ```
313
326
 
314
- The interactive child instead presents one Captain status with the same
315
- `warning: replay history ...` text and writes no raw stderr diagnostic. A later
316
- lease gets its own warning only if its replay writer independently becomes
317
- incomplete. The public facade emits no warning; an embedding host decides how
318
- to present the `incomplete` status it receives
319
- ([[playbook-cli-84](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-84)]).
327
+ Schema 6 may retain execution; older CLI records and desktop sidecars lacking complete recovery remain readable history.
328
+ Discovery skips these histories and unmigrated records with a reason; they do not block other sessions.
320
329
 
321
330
  ### Reconciling possible repository effects
322
331
 
@@ -359,23 +368,18 @@ current config cannot retune that attempt, and retry may duplicate external
359
368
  effects. Discard
360
369
  reads no input and runs no model: it restores the exact prior settled boundary,
361
370
  or deletes a never-settled fresh session, while abandoning the attempted work.
362
- An interrupted interactive turn uses the same uncertain record and is
363
- recovered with these headless commands. Current sessions use Captain-session
364
- record schema 6. Records from the removed direct v6 runner and released record
365
- schema 2 have incompatible player identity; Playbook 9 record schema 3, the
366
- historical schema-4 retention shape, and both pre-release schema-5 shapes
367
- predate the canonical schema-6 record boundary; the earlier schema-5 shape also
368
- lacks `unresolvedEffects` and cannot prove whether governed work may replay.
369
- Explicit selection rejects
370
- all of them before registry construction or governed work with the applicable
371
- cutover explanation. Implicit `--continue` reports and skips each fully
372
- validated nonresumable record with its session id, path, applicable reason, and
373
- an archive-or-remove remedy while leaving the file intact. Fresh-session
374
- discovery likewise leaves and reports nonresumable, malformed, unsafe, or
375
- unknown-schema files. A fully validated nonresumable record participates in
376
- settled same-directory predecessor ordering: it declines adoption only when it
377
- is newest, while an older or different-directory record does not block a newer
378
- resumable predecessor. A record whose directory or order cannot be validated
379
- publishes an empty fresh boundary without falling through. Those invalid
380
- records still fail closed when explicitly selected or encountered by
381
- `--continue`; no path converts or restores them.
371
+ An interrupted interactive turn uses the same uncertain record and these
372
+ headless recovery commands. Discard preserves the attempt's replay history and
373
+ refuses if the effect ledger has advanced beyond the prior checkpoint.
374
+
375
+ Stop old writers before upgrading. When using the ordinary `~/.spex/sessions`
376
+ default, the CLI imports sessions from
377
+ `${XDG_STATE_HOME:-$HOME/.local/state}/playbook/sessions`. Explicit `SPEX_HOME`
378
+ or `sessions` selections bypass this discovery. Other locations can be migrated
379
+ through the [shared API](embedding.md#sharing-the-cli-session-store).
380
+
381
+ Migration preserves the complete replay and retains original bytes before
382
+ removing old active files. Valid schema-6 recovery becomes token-free; schemas
383
+ 2–5 and incomplete desktop sidecars become history only. Unsupported inputs
384
+ stay in place with a diagnostic; active writers and destination conflicts stop
385
+ the cutover. Ordinary continuation never guesses missing identity or effects.
@@ -250,7 +250,7 @@ attempted settings already stored with that turn.
250
250
  ## Session storage
251
251
 
252
252
  Both front ends select canonical session manifests and write replay streams in
253
- one directory, where external hosts may keep their own sidecars too. Set the
253
+ one directory, shared with embedding hosts. Set the
254
254
  optional top-level `sessions` key to move that shared store:
255
255
 
256
256
  ```yaml
@@ -259,7 +259,7 @@ sessions: ./state/playbook-sessions
259
259
 
260
260
  The value must be a nonempty filesystem path. When the key is absent, the
261
261
  directory is
262
- `${XDG_STATE_HOME:-$HOME/.local/state}/playbook/sessions`. An absolute path is
262
+ `${SPEX_HOME:-$HOME/.spex}/sessions`. An absolute path is
263
263
  used as given; `~` and `~/...` expand from the home directory, while `~user`
264
264
  is rejected. Every other value, including a bare relative path such as the one
265
265
  above, resolves against the primary config file's directory rather than the
package/docs/embedding.md CHANGED
@@ -200,19 +200,11 @@ sequential call lane across every frame that names them, while distinct IDs
200
200
  remain isolated. Child return, frame disposal, and a later root engagement do
201
201
  not clear the session ledger.
202
202
 
203
- Runtime and complete shell snapshots are schema 4. Their role-resume
204
- projection remains role-local, while the composing shell persists the stable
205
- player ledger, every frame's exact role bindings, and the host's authoritative
206
- effect-ledger mirror. The bundled CLI wraps that shell state in Captain
207
- session-record schema 6. Do not restore an earlier snapshot or session record
208
- by guessing identity or effect evidence. This includes both pre-release
209
- schema-5 record shapes; the earlier one predates required `unresolvedEffects`,
210
- and neither is a canonical schema-6 boundary. Explicit selection rejects them,
211
- while fresh discovery reports and skips them. A fully validated nonresumable
212
- record participates in settled same-directory ordering and declines adoption
213
- only when it is newest; an older or different-directory record does not block a
214
- newer resumable predecessor. An unvalidatable record leaves ordering unproved,
215
- so discovery publishes an empty target without falling through. On a compatible
203
+ Runtime and complete shell snapshots are schema 4. The shared store projects
204
+ provider continuations out of nested snapshots and writes schema-7 manifests.
205
+ Local hints may rehydrate the current checkpoint; retained generations never
206
+ restore provider tokens. Use the shared migrator for older records instead of
207
+ guessing identity or effect evidence. On a compatible
216
208
  restore, rebuild
217
209
  `promptIdentity` from the current model selection (or adapter for an explicit
218
210
  provider-default selection) and rebuild live host capabilities under the
@@ -304,12 +296,9 @@ lease, session record, resume credential, catalog, or recovery member
304
296
 
305
297
  ## Sharing the CLI session store
306
298
 
307
- An external host that needs the CLI's canonical session validation and
308
- token-free replay format can use the narrow, semver-stable
309
- `@sublang/playbook/session-store` facade. It shares the CLI's private store and
310
- validators by construction but exposes no canonical manifest, snapshot,
311
- provider credential, effect ledger, recovery operation, or turn-lifecycle
312
- operation:
299
+ `@sublang/playbook/session-store` provides the shared lifecycle and management
300
+ API. Its existing `openSessionStore()` facade remains available for narrow
301
+ summary/replay consumers:
313
302
 
314
303
  ```ts
315
304
  import {
@@ -373,15 +362,10 @@ An `append()` suppressed before release by either unavailable initialization or
373
362
  a numeric incomplete latch resolves `undefined` without recording the supplied
374
363
  record, so fulfillment alone does not prove persistence.
375
364
 
376
- Always release a successfully acquired lease. `release()` closes append
377
- admission, drains earlier appends, attempts the final checkpoint, retires the
378
- lease, and returns the final `lastReadableSeq`, `lastDurableSeq`, and
379
- `incomplete` status. A replay initialization failure reports unavailable null
380
- boundaries; a later sanitization, repair, or persistence failure latches
381
- numeric `incomplete` status. Either state suppresses later replay work on that
382
- lease and remains isolated from canonical session lifecycle work. The facade
383
- writes no warning to stdout or stderr; the embedding host owns any presentation
384
- of that status.
365
+ Always release a successfully acquired lease. Release drains admitted work,
366
+ saves newly detected incompleteness and retires ownership. A failed save or
367
+ unproved ownership leaves the lease held. The facade owns no presentation;
368
+ embedding hosts decide how to display recording failures.
385
369
 
386
370
  For control flow, a missing canonical manifest from `read()` uses
387
371
  `Error.code === 'PLAYBOOK_SESSION_NOT_FOUND'`, and a competing live or foreign
@@ -390,10 +374,62 @@ messages or assume those codes for malformed input, unsafe storage, an
390
374
  indeterminate owner probe, or another storage failure
391
375
  ([[playbook-cli-73](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-73)]).
392
376
 
393
- See
394
- [`code.playbook.test.ts`](https://github.com/sublang-ai/playbook/blob/main/reference/sdlc/code.playbook/code.playbook.test.ts)
395
- for the full range of port shapes (classifier, judge, abort, interrupt,
396
- status/telemetry) the runtime is contract-tested against.
377
+ For complete sessions, use `createSessionStore()` and `openSessionHost()`:
378
+
379
+ ```ts
380
+ import { createSessionStore } from '@sublang/playbook/session-store';
381
+ import { openSessionHost } from '@sublang/playbook/session-host';
382
+
383
+ const shared = createSessionStore();
384
+ await shared.prepare();
385
+ const controller = await openSessionHost({
386
+ store: shared,
387
+ sessionId,
388
+ mode: 'continue',
389
+ });
390
+ try {
391
+ await controller.handleBossTurn('Continue the recorded work.');
392
+ } finally {
393
+ await controller.dispose();
394
+ }
395
+ ```
396
+
397
+ A new host supplies a validated `SessionExecutionProjection` as `config`, or a
398
+ plan from `loadLaunchPlan()`, and the working directory. Resolve a configured
399
+ `sessions` path with `resolveLaunchSessionsDir()` and pass that store explicitly.
400
+ The controller owns uncertainty, reconciliation, settlement and lease release;
401
+ observers receive presentation events or exact appended envelopes through
402
+ `onStoredRecord`. Keep the controller open for successive turns.
403
+
404
+ `shared.readLeaseState(sessionId)` reports `active`, `idle` or `unknown` without
405
+ changing files. Use it for presentation; mutations still require a lease.
406
+
407
+ `shared.migrate(id, { sourcePath })` imports a legacy manifest and its adjacent
408
+ replay while holding both stores' leases. `shared.migrateLegacyDefault()` imports
409
+ the former XDG default and reports migrated and preserved unsupported IDs.
410
+ Embedding applications select and migrate stores during startup;
411
+ `openSessionHost()` does not discover old profiles. Stop old writers first.
412
+ Automatic discovery belongs only to the ordinary
413
+ `~/.spex/sessions` profile with no `SPEX_HOME` or `sessions` override; custom
414
+ profiles require an explicit migration request.
415
+
416
+ For an uncertain session, reopen with `mode:'retry'` and call `retry()`. It uses
417
+ the exact recorded input and attempted configuration. Module-free
418
+ `discardSessionUncertain(shared, sessionId)` restores the prior recovery only
419
+ when no effect-ledger advancement prevents discard.
420
+
421
+ `readHistory()` returns readable history and a damaged boundary, including a
422
+ clearly marked synthetic projection when a validated legacy journal has no
423
+ stream. `validate()` separates byte integrity from resumability.
424
+ `readManifest()` may return an older or unknown object; use
425
+ `validateSessionManifest()` before interpreting schema-7 recovery.
426
+ `migrate()` preserves original inputs before conversion. `delete()` removes the
427
+ bundle under the shared lease, with the manifest last. Management needs no
428
+ playbook module. A coordination-only `acquireManagement()` lease reserves even
429
+ an absent ID and never checkpoints or rewrites selected bytes on release.
430
+
431
+ The [storage contract](https://github.com/sublang-ai/playbook/blob/main/specs/packages/session-storage.md)
432
+ defines the files, versions and compatibility rules.
397
433
 
398
434
  ## Reading the published spec contracts
399
435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "12.3.0",
3
+ "version": "13.0.0",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, and DEV workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -113,7 +113,11 @@
113
113
  "reference/sdlc/dev.playbook/dev.registry.ts",
114
114
  "reference/sdlc/dev.playbook/dev.registry.js",
115
115
  "reference/sdlc/dev.playbook/dev.registry.d.ts",
116
- "docs"
116
+ "docs",
117
+ "reference/sdlc/code.playbook/session-host.js",
118
+ "reference/sdlc/code.playbook/session-host.d.ts",
119
+ "reference/sdlc/code.playbook/bin/portable-codec.js",
120
+ "reference/sdlc/code.playbook/bin/session-host.js"
117
121
  ],
118
122
  "bin": {
119
123
  "playbook": "reference/sdlc/code.playbook/bin/playbook.js"
@@ -175,6 +179,10 @@
175
179
  "./dev/registry": {
176
180
  "types": "./reference/sdlc/dev.playbook/dev.registry.d.ts",
177
181
  "default": "./reference/sdlc/dev.playbook/dev.registry.js"
182
+ },
183
+ "./session-host": {
184
+ "types": "./reference/sdlc/code.playbook/session-host.d.ts",
185
+ "default": "./reference/sdlc/code.playbook/session-host.js"
178
186
  }
179
187
  },
180
188
  "scripts": {
@@ -190,7 +198,7 @@
190
198
  "provenance": true
191
199
  },
192
200
  "dependencies": {
193
- "@sublang/cligent": "^0.24.0",
201
+ "@sublang/cligent": "^0.25.0",
194
202
  "@sublang/spex": "^3.0.0",
195
203
  "p-queue": "^9.3.1",
196
204
  "xstate": "^5.19.4",
@@ -378,6 +378,7 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
378
378
  let host;
379
379
  try {
380
380
  lease = await store.acquire(payload.sessionId);
381
+ if (payload.mode === 'selected') await lease.assertContinuable({ cwd: payload.cwd, executionProjection: payload.executionProjection });
381
382
  replayChannel = createManagedReplayChannel({
382
383
  lease,
383
384
  sessionId: payload.sessionId,
@@ -173,6 +173,7 @@ export function resolveLaunchSessionsDir({
173
173
  preparePrimary = false,
174
174
  templatePath = DEFAULT_TEMPLATE_PATH,
175
175
  onNotice = () => {},
176
+ onDefault = () => {},
176
177
  }) {
177
178
  // The private injection used by tests and managed launch plumbing remains
178
179
  // authoritative over configuration, just like an injected store.
@@ -199,6 +200,7 @@ export function resolveLaunchSessionsDir({
199
200
  }
200
201
 
201
202
  if (locator === undefined) {
203
+ onDefault();
202
204
  return defaultCaptainSessionsDir(env, homeDir);
203
205
  }
204
206
  if (typeof locator !== "string" || locator.length === 0) {
@@ -82,6 +82,31 @@ export async function runPlaybookCli(options = {}) {
82
82
  const userConfigPath =
83
83
  options.userConfigPath ?? resolveUserConfigPath(env, home);
84
84
 
85
+ if (argv[0] === "migrate-session") {
86
+ try {
87
+ const { withPaths, rest } = extractWithFlags(argv.slice(1));
88
+ if (rest.length === 1 && ["--help", "-h"].includes(rest[0])) {
89
+ stdout.write("Usage: playbook migrate-session <id> [--with <path>]...\n");
90
+ return { code: 0 };
91
+ }
92
+ if (rest.length !== 1 || !SESSION_ID_PATTERN.test(rest[0])) throw new Error("expected one canonical session UUID: playbook migrate-session <id> [--with <path>]...");
93
+ const sessionsDir = options.sessionStore === undefined ? resolveLaunchSessionsDir({
94
+ userConfigPath, overlayPaths: withPaths, env, homeDir: home,
95
+ ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),
96
+ preparePrimary: false,
97
+ }) : options.sessionStore.sessionsDir;
98
+ const store = options.sessionStore ?? createCaptainSessionStore({ sessionsDir, env, homeDir: home });
99
+ const result = await store.migrate(rest[0]);
100
+ stdout.write(`playbook: ${result.migrated ? 'migrated' : 'already portable'} session ${rest[0]} in ${sessionsDir} (${result.manifest.state})\n`);
101
+ if (result.manifest.state === 'history-only') stderr.write(`playbook: ${result.manifest.reason}\n`);
102
+ for (const reason of result.reasons) stderr.write(`playbook: ${reason}\n`);
103
+ return { code: 0 };
104
+ } catch (error) {
105
+ stderr.write(`playbook migrate-session: ${errorMessage(error)}\n`);
106
+ return { code: COMPOSITION_FAILURE_EXIT_CODE };
107
+ }
108
+ }
109
+
85
110
  // PBCLI-18: `playbook run ...` is the non-interactive presentation of the
86
111
  // same generic-config Captain session. It never resolves or launches the
87
112
  // tmux presenter, but it receives the launch inputs shared with this host.
@@ -229,6 +254,7 @@ export async function runPlaybookCli(options = {}) {
229
254
  interactiveArgs.cwd ?? ".",
230
255
  );
231
256
  let resolvedSessionsDir;
257
+ let migrateDefaultStore = false;
232
258
  if (options.sessionStore === undefined) {
233
259
  try {
234
260
  resolvedSessionsDir = resolveLaunchSessionsDir({
@@ -240,6 +266,7 @@ export async function runPlaybookCli(options = {}) {
240
266
  ? { sessionsDir: options.sessionsDir }
241
267
  : {}),
242
268
  preparePrimary: interactiveArgs.sessionId === undefined,
269
+ onDefault: () => { migrateDefaultStore = !(typeof env.SPEX_HOME === 'string' && env.SPEX_HOME.trim() !== ''); },
243
270
  onNotice: (line) => stderr.write(line),
244
271
  });
245
272
  // PBCLI-78: listing validates the locator but never consumes the store.
@@ -258,7 +285,10 @@ export async function runPlaybookCli(options = {}) {
258
285
  let selectedRecord;
259
286
  if (interactiveArgs.sessionId !== undefined) {
260
287
  try {
261
- store = createInteractiveStore(options, env, home, resolvedSessionsDir);
288
+ store = await createInteractiveStore(options, env, home, resolvedSessionsDir, migrateDefaultStore, stderr);
289
+ await store.prepare();
290
+ const validation = await store.validate(interactiveArgs.sessionId);
291
+ if (!validation.resumable) throw new Error(validation.reasons.join('; '));
262
292
  selectedRecord = validateCaptainSessionRecord(
263
293
  await store.read(interactiveArgs.sessionId),
264
294
  );
@@ -276,6 +306,7 @@ export async function runPlaybookCli(options = {}) {
276
306
  const recoveryLease = await store.acquire(interactiveArgs.sessionId);
277
307
  let recoveryError;
278
308
  try {
309
+ await recoveryLease.assertContinuable();
279
310
  selectedRecord = validateCaptainSessionRecord(
280
311
  await recoveryLease.recoverUnresolvedEffectAbandonment(),
281
312
  );
@@ -405,7 +436,7 @@ export async function runPlaybookCli(options = {}) {
405
436
  }
406
437
 
407
438
  try {
408
- store ??= createInteractiveStore(options, env, home, resolvedSessionsDir);
439
+ store ??= await createInteractiveStore(options, env, home, resolvedSessionsDir, migrateDefaultStore, stderr);
409
440
  } catch (error) {
410
441
  stderr.write(`playbook: ${errorMessage(error)}\n`);
411
442
  return { code: COMPOSITION_FAILURE_EXIT_CODE };
@@ -743,8 +774,8 @@ function optionValue(argv, index, name) {
743
774
  return value;
744
775
  }
745
776
 
746
- function createInteractiveStore(options, env, home, sessionsDir) {
747
- return (
777
+ async function createInteractiveStore(options, env, home, sessionsDir, migrateDefault, stderr) {
778
+ const store = (
748
779
  options.sessionStore ??
749
780
  createCaptainSessionStore({
750
781
  env,
@@ -756,6 +787,12 @@ function createInteractiveStore(options, env, home, sessionsDir) {
756
787
  : {}),
757
788
  })
758
789
  );
790
+ if (migrateDefault && options.sessionStore === undefined) {
791
+ const migrated = await store.migrateLegacyDefault();
792
+ if (migrated.migrated.length > 0) stderr.write(`playbook: migrated ${migrated.migrated.length} sessions from ${migrated.sourceDir}\n`);
793
+ for (const skipped of migrated.skipped) stderr.write(`playbook: preserved legacy session ${skipped.sessionId} in ${migrated.sourceDir}: ${skipped.reason}\n`);
794
+ }
795
+ return store;
759
796
  }
760
797
 
761
798
  async function writeStream(stream, text, signal) {
@@ -887,6 +924,7 @@ function helpText({
887
924
  " playbook run (--continue | --session <id>) [reply]",
888
925
  " playbook run --session <id> --retry-uncertain",
889
926
  " playbook run --session <id> --discard-uncertain",
927
+ " playbook migrate-session <id> [--with <path>]...",
890
928
  " playbook --help",
891
929
  "",
892
930
  `Default config: ${userConfigPath}`,