@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 +56 -52
- package/docs/configuration.md +2 -2
- package/docs/embedding.md +68 -32
- package/package.json +11 -3
- package/reference/sdlc/code.playbook/bin/interactive-session.js +1 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +2 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +42 -4
- package/reference/sdlc/code.playbook/bin/portable-codec.js +190 -0
- package/reference/sdlc/code.playbook/bin/replay-observer.js +18 -2
- package/reference/sdlc/code.playbook/bin/run.js +62 -18
- package/reference/sdlc/code.playbook/bin/session-host.js +104 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +622 -65
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +5 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +53 -14
- package/reference/sdlc/code.playbook/playbook-captain.ts +68 -21
- package/reference/sdlc/code.playbook/session-host.d.ts +75 -0
- package/reference/sdlc/code.playbook/session-host.js +18 -0
- package/reference/sdlc/code.playbook/session-store.d.ts +176 -1
- package/reference/sdlc/code.playbook/session-store.js +22 -6
- package/src/xstate-playbook-runtime.js +12 -13
- package/src/xstate-playbook-runtime.ts +14 -21
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 `${
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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.
|
package/docs/configuration.md
CHANGED
|
@@ -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,
|
|
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
|
-
`${
|
|
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.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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": "
|
|
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.
|
|
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
|
-
|
|
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}`,
|