@sublang/playbook 13.2.0 → 14.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.
Files changed (36) hide show
  1. package/README.md +5 -5
  2. package/docs/configuration.md +14 -9
  3. package/docs/embedding.md +20 -1
  4. package/package.json +6 -1
  5. package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
  6. package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
  7. package/reference/sdlc/code.playbook/bin/repository-effects.js +11 -3
  8. package/reference/sdlc/code.playbook/bin/run.js +4 -0
  9. package/reference/sdlc/code.playbook/bin/session-host.js +13 -3
  10. package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
  11. package/reference/sdlc/code.playbook/playbook-captain.d.ts +26 -1
  12. package/reference/sdlc/code.playbook/playbook-captain.js +233 -8
  13. package/reference/sdlc/code.playbook/playbook-captain.ts +298 -8
  14. package/reference/sdlc/code.playbook/playbook.config.template.yaml +5 -6
  15. package/reference/sdlc/code.playbook/session-host.d.ts +26 -0
  16. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +3 -10
  17. package/reference/sdlc/dev.playbook/dev.playbook.js +5 -5
  18. package/reference/sdlc/dev.playbook/dev.playbook.ts +7 -7
  19. package/reference/sdlc/pr.md +17 -4
  20. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +17 -3
  21. package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
  22. package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
  23. package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
  24. package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
  25. package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
  26. package/slc/experiments/fsm-scaffold-guidance.md +31 -0
  27. package/slc/gears2fsm.md +145 -58
  28. package/slc/link.md +158 -7
  29. package/slc/materialize-link.mjs +460 -0
  30. package/slc/optimize.md +46 -6
  31. package/slc/scaffold-fsm.mjs +225 -0
  32. package/slc/slc.pin-inputs.json +13 -0
  33. package/slc/text2gears.md +38 -3
  34. package/slc/workflow-contracts.json +371 -0
  35. package/src/xstate-playbook-runtime.js +35 -1
  36. package/src/xstate-playbook-runtime.ts +54 -1
package/README.md CHANGED
@@ -32,9 +32,10 @@ Run `playbook` for an interactive tmux UI powered by [cligent](https://github.co
32
32
  Out of the box, Playbook includes **CODE** for implementation, **REVIEW** for commit-based review and fixes, **DECIDE** for independently proposed and reviewed specification decisions, **DEV** for repository-aware planning of a development request, **BRANCH** for checking out a new branch for a GitHub issue, and **PR** for publishing, checking, and merging that branch through a pull request.
33
33
  CODE and DECIDE call REVIEW as a nested playbook; DEV calls CODE, or DECIDE and then CODE, and wraps them in BRANCH and PR when the request names a GitHub issue or asks for a pull request.
34
34
 
35
- The shared starter config uses Claude as both Captain and the `dev.coder`
36
- player, and Codex as `dev.reviewer`. CODE, REVIEW, DECIDE, BRANCH, and PR bind
37
- their local roles explicitly to those two stable players, so nested and later
35
+ The shared starter config selects one adapter for the Captain and all players
36
+ from locally visible credentials: Claude first, then Codex. If neither is
37
+ configured, it seeds Claude and prints a notice. CODE, REVIEW, DECIDE, BRANCH, and PR bind
38
+ their local roles explicitly to `dev.coder` and `dev.reviewer`, so nested and later
38
39
  engagements share a conversation only where their bindings name the same
39
40
  player ID; DEV's Analyst binds a distinct `dev.analyst` player.
40
41
 
@@ -49,8 +50,7 @@ If an SDK is missing or older than cligent supports, Playbook prints the pinned
49
50
  Prerequisites:
50
51
 
51
52
  - Node.js >= 20.6.0
52
- - Authenticated [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) or `ANTHROPIC_API_KEY`
53
- - Authenticated [Codex CLI](https://github.com/openai/codex) or `OPENAI_API_KEY`
53
+ - Credentials for the configured adapter: authenticated [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) or `ANTHROPIC_API_KEY` for Claude; authenticated [Codex CLI](https://github.com/openai/codex) or `OPENAI_API_KEY` for Codex
54
54
  - [GitHub CLI](https://cli.github.com/) (`gh`) authenticated for the repository's GitHub remote, for `/branch`, `/pr`, and `/dev` requests that name an issue or ask for a pull request
55
55
 
56
56
  Interactive `playbook` additionally needs tmux 3.3 or newer and [`glow`](https://github.com/charmbracelet/glow#installation) on `PATH`; headless `playbook run` does not.
@@ -55,8 +55,16 @@ key is that playbook's option slice. Every manifest role must be present
55
55
  exactly once. The launcher injects the rest — you do not write host wiring by
56
56
  hand.
57
57
 
58
- The seeded config runs the stable Coder player on GPT-5.6 Sol with fast mode
59
- enabled and the stable Reviewer player on Claude Opus 5:
58
+ The launcher seeds one adapter for the Captain and all stable players from
59
+ locally visible credentials: Claude first, then Codex. It uses Claude Opus 5
60
+ for Claude or GPT-5.6 Sol for Codex, with Captain and Coder at `high` effort
61
+ and Reviewer and Analyst at `xhigh`. If neither adapter is configured, it
62
+ seeds Claude and prints a notice. Credentials do not prove SDK availability
63
+ or remaining quota; launch still checks the configured adapter. Existing
64
+ configs are left unchanged.
65
+
66
+ The Claude default is shown below. A Codex seed also adds
67
+ `permissions.writablePaths: ['.git']` to each agent:
60
68
 
61
69
  ```yaml
62
70
  captain:
@@ -68,14 +76,11 @@ captain:
68
76
 
69
77
  players:
70
78
  dev.coder:
71
- adapter: codex
72
- model: gpt-5.6-sol
73
- effort: ultra
74
- fastMode: true
79
+ adapter: claude
80
+ model: claude-opus-5
81
+ effort: high
75
82
  permissions:
76
- mode: auto
77
- writablePaths:
78
- - .git # allow git metadata writes under Codex auto mode
83
+ mode: auto # protected auto mode for the Claude Coder
79
84
 
80
85
  dev.reviewer:
81
86
  adapter: claude
package/docs/embedding.md CHANGED
@@ -433,7 +433,7 @@ defines the files, versions and compatibility rules.
433
433
 
434
434
  ## Reading the published spec contracts
435
435
 
436
- The authored compiler-phase specs ship in the package and are exposed
436
+ The compiler definitions and supporting assets ship in the package and are exposed
437
437
  as a public, semver-stable surface under `@sublang/playbook/slc/*`.
438
438
  Resolve and read one with `import.meta.resolve` plus `fs`:
439
439
 
@@ -450,3 +450,22 @@ The four specs are [`slc/text2gears.md`](../slc/text2gears.md),
450
450
  [`slc/link.md`](../slc/link.md) — the FSM-to-runtime contract that
451
451
  `@sublang/playbook/runtime` projects into TypeScript — and
452
452
  [`slc/optimize.md`](../slc/optimize.md).
453
+
454
+ Five supporting assets use the same resolution mechanism:
455
+
456
+ | Asset | Purpose |
457
+ | --- | --- |
458
+ | [`slc/workflow-contracts.json`](../slc/workflow-contracts.json) | Public output schemas and default literal target bindings for the builtin REVIEW, DECIDE, CODE, BRANCH, and PR workflows; compilers and embedders can read them without importing workflow implementations. |
459
+ | [`slc/materialize-link.mjs`](../slc/materialize-link.mjs) | Optional CLI that emits a thin linked module from a supported FSM and JSON descriptor; see the profiles and invocation in `slc/link.md`. |
460
+ | [`slc/scaffold-fsm.mjs`](../slc/scaffold-fsm.mjs) | Optional CLI that initializes an incomplete typed FSM from supported GEARS source. |
461
+ | [`slc/experiments/fsm-scaffold-guidance.md`](../slc/experiments/fsm-scaffold-guidance.md) | Opt-in compiler guidance for the initializer, outside ordinary phase discovery. |
462
+ | [`slc/slc.pin-inputs.json`](../slc/slc.pin-inputs.json) | SLC semantic-input closures for `gears2fsm` and `link`, including the workflow catalog and, for `link`, the materializer. Other phases use SLC's inline-input discovery. |
463
+
464
+ For example, resolve `@sublang/playbook/slc/workflow-contracts.json`, read it
465
+ with `readFile`, and parse the result as JSON to inspect the published workflow
466
+ interfaces. Execute the `.mjs` helpers with Node and their documented CLI
467
+ arguments: their supported public interfaces are their resolvable paths and CLI
468
+ contracts. Their JavaScript module exports are internal implementation details
469
+ and carry no SemVer compatibility guarantee.
470
+ The initializer requires further semantic compilation and verification
471
+ before its output is runnable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "13.2.0",
3
+ "version": "14.0.0",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -38,6 +38,11 @@
38
38
  "src/xstate-playbook-runtime.js",
39
39
  "src/xstate-playbook-runtime.d.ts",
40
40
  "slc/link.md",
41
+ "slc/materialize-link.mjs",
42
+ "slc/scaffold-fsm.mjs",
43
+ "slc/experiments/fsm-scaffold-guidance.md",
44
+ "slc/slc.pin-inputs.json",
45
+ "slc/workflow-contracts.json",
41
46
  "slc/gears2fsm.md",
42
47
  "slc/text2gears.md",
43
48
  "slc/optimize.md",
@@ -180,7 +180,7 @@ export function resolveLaunchSessionsDir({
180
180
  if (sessionsDir !== undefined) return sessionsDir;
181
181
 
182
182
  if (preparePrimary) {
183
- seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
183
+ seedUserConfigIfMissing(userConfigPath, templatePath, onNotice, env, homeDir);
184
184
  migrateUserConfigIfRetired(userConfigPath, onNotice);
185
185
  }
186
186
 
@@ -248,8 +248,10 @@ export async function loadLaunchPlan({
248
248
  templatePath = DEFAULT_TEMPLATE_PATH,
249
249
  onNotice = () => {},
250
250
  selectedMembers,
251
+ env = process.env,
252
+ homeDir = env.HOME ?? homedir(),
251
253
  }) {
252
- seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
254
+ seedUserConfigIfMissing(userConfigPath, templatePath, onNotice, env, homeDir);
253
255
  // PBCLI-22/46: a reopen must not rewrite, validate, or otherwise inspect
254
256
  // config members outside the stored projection.
255
257
  if (selectedMembers === undefined) {
@@ -288,10 +290,12 @@ export async function loadSelectedLaunchPlanDataOnly({
288
290
  structuralProjection,
289
291
  templatePath = DEFAULT_TEMPLATE_PATH,
290
292
  onNotice = () => {},
293
+ env = process.env,
294
+ homeDir = env.HOME ?? homedir(),
291
295
  }) {
292
296
  const stored = validateStoredStructuralProjection(structuralProjection);
293
297
  const selectedMembers = selectedMembersFromStoredStructure(stored);
294
- seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
298
+ seedUserConfigIfMissing(userConfigPath, templatePath, onNotice, env, homeDir);
295
299
 
296
300
  let top = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
297
301
  if (overlayPaths.length > 0 && !isObject(top)) {
@@ -1335,11 +1339,90 @@ function assertLegacyRelocationLocatorsSafe(
1335
1339
  );
1336
1340
  }
1337
1341
 
1338
- function seedUserConfigIfMissing(userConfigPath, templatePath, onNotice) {
1342
+ // PBCLI-11 / DR-053: the adapters a seeded lineup may take, highest
1343
+ // precedence first. A fixed order — not whichever the probe answers
1344
+ // first — is what makes one machine seed one file twice.
1345
+ const SEED_LINEUP = Object.freeze([
1346
+ Object.freeze({ adapter: "claude", model: "claude-opus-5" }),
1347
+ Object.freeze({ adapter: "codex", model: "gpt-5.6-sol" }),
1348
+ ]);
1349
+
1350
+ /**
1351
+ * PBCLI-11: the lineup a seed would write here, and whether anything
1352
+ * actually probed ready.
1353
+ *
1354
+ * Only the credential check participates. The SDK probe constructs an
1355
+ * adapter and awaits `isAvailable()`, which reads whatever happens to
1356
+ * be installed and spawns a subprocess for CLI-backed adapters: a seed
1357
+ * consulting it would differ between a developer's machine and a clean
1358
+ * one, and would make creating a file asynchronous.
1359
+ */
1360
+ export function selectSeedLineup(env = process.env, home = homedir()) {
1361
+ const ready = SEED_LINEUP.filter(
1362
+ (entry) =>
1363
+ checkReadiness([entry.adapter], env, home).failingAdapters.length === 0,
1364
+ );
1365
+ return { lineup: ready[0] ?? SEED_LINEUP[0], anyReady: ready.length > 0 };
1366
+ }
1367
+
1368
+ /**
1369
+ * The template is the documented default and the only source of the
1370
+ * starter's structure and comments, so the default choice is still the
1371
+ * byte copy it always was. A different adapter rewrites the agent
1372
+ * scalars in place, which the Document API carries comments through.
1373
+ */
1374
+ function seedTextForLineup(templatePath, lineup) {
1375
+ const template = readFileSync(templatePath, "utf8");
1376
+ if (lineup.adapter === SEED_LINEUP[0].adapter) return template;
1377
+ const doc = parseYamlDocument(template);
1378
+ const players = doc.getIn(["players"]);
1379
+ const agentPaths = [
1380
+ ["captain"],
1381
+ ...[...(players?.items ?? [])].map((pair) => [
1382
+ "players",
1383
+ String(pair.key),
1384
+ ]),
1385
+ ];
1386
+ for (const path of agentPaths) {
1387
+ doc.setIn([...path, "adapter"], lineup.adapter);
1388
+ doc.setIn([...path, "model"], lineup.model);
1389
+ // A codex agent writes git metadata under its sandbox; claude's
1390
+ // auto mode needs no grant, so the key exists for one adapter only.
1391
+ if (lineup.adapter === "codex") {
1392
+ doc.setIn([...path, "permissions", "writablePaths"], [".git"]);
1393
+ } else {
1394
+ doc.deleteIn([...path, "permissions", "writablePaths"]);
1395
+ }
1396
+ }
1397
+ return String(doc);
1398
+ }
1399
+
1400
+ function seedUserConfigIfMissing(
1401
+ userConfigPath,
1402
+ templatePath,
1403
+ onNotice,
1404
+ env = process.env,
1405
+ homeDir = env.HOME ?? homedir(),
1406
+ ) {
1339
1407
  if (existsSync(userConfigPath)) return;
1340
1408
  mkdirSync(dirname(userConfigPath), { recursive: true });
1341
- copyFileSync(templatePath, userConfigPath, constants.COPYFILE_EXCL);
1409
+ const { lineup, anyReady } = selectSeedLineup(env, homeDir);
1410
+ if (lineup.adapter === SEED_LINEUP[0].adapter && anyReady) {
1411
+ // Unchanged default path: one exclusive create of the template bytes.
1412
+ copyFileSync(templatePath, userConfigPath, constants.COPYFILE_EXCL);
1413
+ } else {
1414
+ // Still one exclusive create, so two seeding calls cannot disagree.
1415
+ writeFileSync(userConfigPath, seedTextForLineup(templatePath, lineup), {
1416
+ encoding: "utf8",
1417
+ flag: "wx",
1418
+ });
1419
+ }
1342
1420
  onNotice(`playbook: created config at ${userConfigPath}\n`);
1421
+ if (!anyReady) {
1422
+ onNotice(
1423
+ `playbook: no adapter probed ready; seeded ${lineup.adapter}\n`,
1424
+ );
1425
+ }
1343
1426
  }
1344
1427
 
1345
1428
  // DR-021 §3: migrate once, keeping the original before any rewrite.
@@ -346,10 +346,14 @@ export async function runPlaybookCli(options = {}) {
346
346
  overlayPaths: withPaths,
347
347
  structuralProjection: selectedRecord.structuralProjection,
348
348
  onNotice: (line) => stderr.write(line),
349
+ env,
350
+ homeDir: home,
349
351
  })
350
352
  : await loadLaunchPlan({
351
353
  userConfigPath,
352
354
  overlayPaths: withPaths,
355
+ env,
356
+ homeDir: home,
353
357
  loadModule,
354
358
  prepareRegistryModule:
355
359
  options.prepareRegistryModule ??
@@ -659,10 +659,18 @@ export async function resolveCanonicalGitWorktree(cwd) {
659
659
  }
660
660
 
661
661
  async function sampleWorktree(worktree, env) {
662
- const head = await resolveHead(worktree, env);
663
- const indexVisibility = await rawIndexVisibility(worktree, env);
662
+ const [headResult, visibilityResult, statusResult] = await Promise.allSettled([
663
+ resolveHead(worktree, env),
664
+ rawIndexVisibility(worktree, env),
665
+ rawRepositoryStatus(worktree, env),
666
+ ]);
667
+ if (headResult.status === 'rejected') throw headResult.reason;
668
+ if (visibilityResult.status === 'rejected') throw visibilityResult.reason;
669
+ const head = headResult.value;
670
+ const indexVisibility = visibilityResult.value;
664
671
  assertIndexVisibility(indexVisibility);
665
- const status = await rawRepositoryStatus(worktree, env);
672
+ if (statusResult.status === 'rejected') throw statusResult.reason;
673
+ const status = statusResult.value;
666
674
  const projection = await projectionFromStatus(worktree, status);
667
675
  return { head, indexVisibility, status, projection };
668
676
  }
@@ -359,10 +359,14 @@ export async function runPlaybookRun(options = {}) {
359
359
  overlayPaths: args.withPaths,
360
360
  structuralProjection: priorRecord.structuralProjection,
361
361
  onNotice: (line) => configNotices.push(line),
362
+ env,
363
+ homeDir: home,
362
364
  })
363
365
  : await loadLaunchPlan({
364
366
  userConfigPath,
365
367
  overlayPaths: args.withPaths,
368
+ env,
369
+ homeDir: home,
366
370
  loadModule,
367
371
  prepareRegistryModule,
368
372
  onNotice: (line) => configNotices.push(line),
@@ -43,7 +43,7 @@ export async function openSessionHost(options) {
43
43
  await installRetainedGenerationsForLaunch({ lease, shell: created.shell, ...(record === undefined ? { freshBoundary: { cwd, structuralProjection: structure, executionProjection: config, snapshot: created.snapshot } } : {}), retainedGenerations: record?.retainedGenerations ?? {}, reconcileRepositoryEffects: created.reconcileRepositoryEffects });
44
44
  record = await lease.read();
45
45
  await replay.flushStoredRecords();
46
- const execute = async (input, retry) => {
46
+ const execute = async (input, retry, actionId, shellActionId) => {
47
47
  if (closed || closing) throw new Error('session host is closing');
48
48
  if (active) throw new Error('session turn is already active');
49
49
  const operation = (async () => {
@@ -53,8 +53,18 @@ export async function openSessionHost(options) {
53
53
  input = prior.uncertain.input;
54
54
  if (!retryPending) throw new Error('retry requires reopening the uncertain checkpoint');
55
55
  } else if (prior?.state !== 'settled') throw new Error('session has an uncertain turn; select Retry or Discard');
56
- if (typeof input !== 'string' || input.trim().length === 0) throw new Error('session input must be nonempty');
56
+ if (actionId === undefined && shellActionId === undefined && (typeof input !== 'string' || input.trim().length === 0)) throw new Error('session input must be nonempty');
57
57
  await created.reconcileRepositoryEffects();
58
+ // The selection is made after reconciliation, so the advertised
59
+ // reading it is validated against is the one this turn settles
60
+ // against, and its Boss text is this turn's durable input.
61
+ if (actionId !== undefined) {
62
+ if (typeof created.shell.submitRuntimeAction !== 'function') throw new Error('this Captain shell advertises no runtime actions');
63
+ input = created.shell.submitRuntimeAction(actionId);
64
+ } else if (shellActionId !== undefined) {
65
+ if (typeof created.shell.submitShellAction !== 'function') throw new Error('this Captain shell advertises no controls of its own');
66
+ input = created.shell.submitShellAction(shellActionId);
67
+ }
58
68
  terminal = undefined; replies = [];
59
69
  const attemptId = options.createAttemptId?.() ?? randomUUID();
60
70
  const marked = retry ? await lease.beginRetry({ expectedAttemptId: prior.uncertain.attemptId, nextAttemptId: attemptId }) : await lease.beginTurn({ input, attemptId, attemptedExecutionProjection: config });
@@ -84,7 +94,7 @@ export async function openSessionHost(options) {
84
94
  })();
85
95
  return closing;
86
96
  };
87
- return Object.freeze({ sessionId, host: created.host, shell: created.shell, lease, read: () => lease.read(), handleBossTurn: (input) => execute(input, false), retry: () => execute(undefined, true), dispose });
97
+ return Object.freeze({ sessionId, host: created.host, shell: created.shell, lease, read: () => lease.read(), handleBossTurn: (input) => execute(input, false), listRuntimeActions: () => created.shell.describeRuntimeActions?.() ?? Object.freeze([]), submitRuntimeAction: (actionId) => execute(undefined, false, actionId), listShellActions: () => created.shell.describeShellActions?.() ?? Object.freeze([]), submitShellAction: (actionId) => execute(undefined, false, undefined, actionId), retry: () => execute(undefined, true), dispose });
88
98
  } catch (cause) {
89
99
  const failures = [cause];
90
100
  let disposed = true;
@@ -3163,7 +3163,7 @@ async function createLease({
3163
3163
  }
3164
3164
  if (
3165
3165
  snapshot.mode !== 'chat' ||
3166
- snapshot.lastAction !== 'runtime' ||
3166
+ (snapshot.lastAction !== 'runtime' && snapshot.lastAction !== 'dismiss') ||
3167
3167
  snapshot.lastSettlementStatus !== 'ok'
3168
3168
  ) {
3169
3169
  throw new Error(
@@ -4221,7 +4221,7 @@ function validateCanonicalCaptainSessionRecord(
4221
4221
  }
4222
4222
  if (
4223
4223
  snapshot.mode !== 'chat' ||
4224
- snapshot.lastAction !== 'runtime' ||
4224
+ (snapshot.lastAction !== 'runtime' && snapshot.lastAction !== 'dismiss') ||
4225
4225
  snapshot.lastSettlementStatus !== 'ok' ||
4226
4226
  Object.hasOwn(record.retainedGenerations ?? {}, rootPlaybookId)
4227
4227
  ) {
@@ -1,6 +1,6 @@
1
1
  import { type Captain, type CaptainSession, type TuningSelection } from '@sublang/cligent/tmux-play';
2
2
  import type { Effort, PermissionPolicy } from '@sublang/cligent';
3
- import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
3
+ import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
4
4
  import { type CaptainControllerPort } from '../captain.playbook/captain.playbook.js';
5
5
  import type { PlaybookSummaryPolicy } from './code.registry.js';
6
6
  interface SessionAgent {
@@ -215,6 +215,31 @@ export interface PlaybookCaptainShell extends Captain {
215
215
  exportSnapshot(): PlaybookCaptainShellSnapshot | undefined;
216
216
  exportSettlement(): PlaybookCaptainSettlement | undefined;
217
217
  restore(session: CaptainSession, snapshot: PlaybookCaptainShellSnapshot): Promise<void>;
218
+ /**
219
+ * The active leaf's currently advertised runtime actions, as the host's own
220
+ * reading of the control view the ControlView digest reads (CAPTAIN-60).
221
+ * Declared optional because capability absence is member absence: a shell
222
+ * that publishes no reader advertises nothing.
223
+ */
224
+ describeRuntimeActions?(): readonly PlaybookControlAction[];
225
+ /**
226
+ * Select one currently advertised runtime action as the next Boss turn's
227
+ * decision and return that turn's Boss text, which the host submits through
228
+ * `handleBossTurn` (CAPTAIN-7).
229
+ */
230
+ submitRuntimeAction?(actionId: string): string;
231
+ /**
232
+ * The controls the shell effects on its own behalf, disjoint from the leaf's
233
+ * advertised runtime actions (CAPTAIN-62). Declared optional for the same
234
+ * reason: a shell that publishes no reader advertises nothing.
235
+ */
236
+ describeShellActions?(): readonly PlaybookControlAction[];
237
+ /**
238
+ * Select one currently advertised shell control as the next Boss turn's
239
+ * decision and return that turn's Boss text, which the host submits through
240
+ * `handleBossTurn` (CAPTAIN-7).
241
+ */
242
+ submitShellAction?(actionId: string): string;
218
243
  }
219
244
  export declare function assertPlaybookCaptainUnresolvedEffects(value: unknown): readonly PlaybookCaptainUnresolvedEffect[];
220
245
  /** Validate, detach, and freeze one untrusted shell snapshot. */