@sublang/playbook 13.1.0 → 13.3.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 (43) hide show
  1. package/README.md +7 -5
  2. package/docs/cli.md +28 -9
  3. package/docs/configuration.md +27 -14
  4. package/docs/embedding.md +1 -1
  5. package/package.json +40 -2
  6. package/reference/sdlc/branch.md +37 -0
  7. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +169 -0
  8. package/reference/sdlc/branch.playbook/branch.fsm.js +325 -0
  9. package/reference/sdlc/branch.playbook/branch.fsm.ts +458 -0
  10. package/reference/sdlc/branch.playbook/branch.gears.md +44 -0
  11. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +20 -0
  12. package/reference/sdlc/branch.playbook/branch.playbook.js +121 -0
  13. package/reference/sdlc/branch.playbook/branch.playbook.ts +217 -0
  14. package/reference/sdlc/branch.playbook/branch.registry.d.ts +40 -0
  15. package/reference/sdlc/branch.playbook/branch.registry.js +63 -0
  16. package/reference/sdlc/branch.playbook/branch.registry.ts +121 -0
  17. package/reference/sdlc/code.playbook/bin/session-host.js +13 -3
  18. package/reference/sdlc/code.playbook/playbook-captain.d.ts +26 -1
  19. package/reference/sdlc/code.playbook/playbook-captain.js +218 -6
  20. package/reference/sdlc/code.playbook/playbook-captain.ts +281 -6
  21. package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -8
  22. package/reference/sdlc/code.playbook/session-host.d.ts +26 -0
  23. package/reference/sdlc/dev.md +24 -3
  24. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +100 -9
  25. package/reference/sdlc/dev.playbook/dev.fsm.js +235 -18
  26. package/reference/sdlc/dev.playbook/dev.fsm.ts +368 -24
  27. package/reference/sdlc/dev.playbook/dev.gears.md +45 -7
  28. package/reference/sdlc/dev.playbook/dev.playbook.js +13 -1
  29. package/reference/sdlc/dev.playbook/dev.playbook.ts +13 -1
  30. package/reference/sdlc/dev.playbook/dev.registry.d.ts +1 -1
  31. package/reference/sdlc/dev.playbook/dev.registry.js +7 -2
  32. package/reference/sdlc/dev.playbook/dev.registry.ts +7 -2
  33. package/reference/sdlc/pr.md +108 -0
  34. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +352 -0
  35. package/reference/sdlc/pr.playbook/pr.fsm.js +902 -0
  36. package/reference/sdlc/pr.playbook/pr.fsm.ts +1251 -0
  37. package/reference/sdlc/pr.playbook/pr.gears.md +155 -0
  38. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +27 -0
  39. package/reference/sdlc/pr.playbook/pr.playbook.js +133 -0
  40. package/reference/sdlc/pr.playbook/pr.playbook.ts +235 -0
  41. package/reference/sdlc/pr.playbook/pr.registry.d.ts +40 -0
  42. package/reference/sdlc/pr.playbook/pr.registry.js +68 -0
  43. package/reference/sdlc/pr.playbook/pr.registry.ts +124 -0
@@ -0,0 +1,217 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ //
4
+ // Generated by slc/link.md (FSM-to-Runtime linker).
5
+ // Source FSM: ./branch.fsm.ts
6
+ // Role metadata: Coder→coder (canonical local role)
7
+ // Boss event: deterministic START_BRANCH entry; exact Boss text becomes
8
+ // callerInput; pending Coder questions retain BOSS_REPLY
9
+ // Adjudication: LLM judge for the one Coder state; coderOutput is carried
10
+ // verbatim; baseRevision is receipt-owned effect evidence —
11
+ // the unchanged receipt's observed HEAD — never taken from
12
+ // Coder's prose
13
+ // Compat: artifact schema 3 / runtime ABI 1
14
+
15
+ import {
16
+ createXStatePlaybookRuntime,
17
+ composePlayerContinuation,
18
+ snapshotJsonValue,
19
+ type PlaybookPlayerInput,
20
+ type XStatePlaybookRuntimeFactory,
21
+ type XStatePlaybookRuntimeConstruction,
22
+ type XStatePlaybookRuntimeSpecV3,
23
+ } from '@sublang/playbook/xstate-runtime';
24
+ import {
25
+ branchMachine,
26
+ type BranchInput,
27
+ type PlayerInput,
28
+ } from './branch.fsm.js';
29
+ import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
30
+ import type {
31
+ CaptainCallOptions,
32
+ CaptainResult,
33
+ JsonValue,
34
+ NormalizedError,
35
+ PlayerCallOptions,
36
+ PlaybookCallRequest,
37
+ PlaybookCallResult,
38
+ PlaybookCallStart,
39
+ PlaybookControlReceipt,
40
+ PlaybookControlView,
41
+ PlaybookPendingCall,
42
+ PlaybookPorts,
43
+ PlaybookRunResult,
44
+ PlaybookRuntime,
45
+ PlaybookRuntimeFactory,
46
+ PlaybookRuntimeSnapshot,
47
+ PlaybookSession,
48
+ PlaybookState,
49
+ PlaybookStateValue,
50
+ PlaybookTraceEvent,
51
+ PlaybookTraceType,
52
+ PlayerResult,
53
+ PlayerSessionStore,
54
+ } from '@sublang/playbook/runtime';
55
+
56
+ export type {
57
+ CaptainCallOptions,
58
+ CaptainResult,
59
+ JsonValue,
60
+ NormalizedError,
61
+ PlayerCallOptions,
62
+ PlaybookCallRequest,
63
+ PlaybookCallResult,
64
+ PlaybookCallStart,
65
+ PlaybookControlReceipt,
66
+ PlaybookControlView,
67
+ PlaybookPendingCall,
68
+ PlaybookPorts,
69
+ PlaybookRunResult,
70
+ PlaybookRuntime,
71
+ PlaybookRuntimeFactory,
72
+ PlaybookRuntimeSnapshot,
73
+ PlaybookSession,
74
+ PlaybookState,
75
+ PlaybookStateValue,
76
+ PlaybookTraceEvent,
77
+ PlaybookTraceType,
78
+ PlayerResult,
79
+ PlayerSessionStore,
80
+ };
81
+
82
+ export type BranchPlaybookOptions = BranchInput;
83
+ export type BranchPlaybookHostCapabilities =
84
+ PlaybookHostConstructionCapabilities &
85
+ XStatePlaybookRuntimeConstruction<BranchPlaybookOptions, object>['hostCapabilities'];
86
+
87
+ const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
88
+
89
+ const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
90
+ 'coderOutput',
91
+ ]);
92
+ const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set(['refused']);
93
+
94
+ function snapshotBranchOptions(value: unknown): BranchPlaybookOptions {
95
+ const captured = snapshotJsonValue(value, 'BRANCH runtime options');
96
+ if (
97
+ captured === null ||
98
+ typeof captured !== 'object' ||
99
+ Array.isArray(captured)
100
+ ) {
101
+ throw new TypeError('BRANCH runtime options must be an object');
102
+ }
103
+ const record = captured as Readonly<Record<string, JsonValue>>;
104
+ for (const key of Object.keys(record)) {
105
+ throw new TypeError(`BRANCH runtime options.${key} is not declared`);
106
+ }
107
+ return captured as unknown as BranchPlaybookOptions;
108
+ }
109
+
110
+ function placeholderField(token: string): string {
111
+ if (token === '#') return 'irNumber';
112
+ return token.replace(/-([A-Za-z0-9_$])/g, (_match, next: string) =>
113
+ next.toUpperCase(),
114
+ );
115
+ }
116
+
117
+ function quotedContinuation(value: string): string {
118
+ return value.replaceAll('\n', '\n> ');
119
+ }
120
+
121
+ /**
122
+ * Preserve authored Markdown quote markers around every line of relayed
123
+ * runtime text. The generic composer preserves the marker itself; BRANCH's
124
+ * override additionally keeps a multiline caller input inside that quote.
125
+ */
126
+ function composePlayerPrompt(input: PlayerInput, resuming = false): string {
127
+ const fields = input as unknown as Record<string, unknown>;
128
+ const body = input.prompt.replace(
129
+ PLACEHOLDER,
130
+ (match: string, token: string, offset: number, source: string) => {
131
+ const value = fields[placeholderField(token)];
132
+ if (typeof value !== 'string') return match;
133
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
134
+ const literal = source.slice(lineStart, offset);
135
+ return literal.startsWith('> ') ? quotedContinuation(value) : value;
136
+ },
137
+ );
138
+ return composePlayerContinuation(input, body, resuming);
139
+ }
140
+
141
+ export const _internal = {
142
+ composePlayerPrompt,
143
+ VERBATIM_PAYLOAD_FIELDS,
144
+ UNFINISHED_FINAL_STATE_IDS,
145
+ };
146
+
147
+ const runtimeSpec = {
148
+ label: 'BRANCH',
149
+ // DR-022 / slc/link.md: the declaration carries the value current at link
150
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
151
+ // which would follow whatever engine loads the module and make the
152
+ // factory's skew check compare that engine with itself.
153
+ compat: { artifactSchema: 3, runtimeAbi: 1 },
154
+ snapshotOptions: snapshotBranchOptions,
155
+ entryEvent: {
156
+ type: 'START_BRANCH',
157
+ textField: 'callerInput',
158
+ // `startBranch` copies the entry text here, so the failure-state retry
159
+ // reads it back from the persisted machine snapshot and survives a
160
+ // continued session (DR-034).
161
+ contextField: 'callerInput',
162
+ },
163
+ roleStates: {
164
+ createBranch: {
165
+ role: 'coder',
166
+ label:
167
+ 'Coder is reading the named issue and creating and checking out a new branch at the current commit.',
168
+ },
169
+ },
170
+ // DR-050: BRANCH owns no repository commit — every Coder outcome, including
171
+ // the Boss question for an ambiguous request, is governed with the
172
+ // `unchanged` repository disposition, because a new branch at the current
173
+ // commit changes neither HEAD's OID nor the worktree projection.
174
+ outcomeAuthority: {
175
+ governedPlayerStates: {
176
+ createBranch: {
177
+ branched: {
178
+ fields: {
179
+ branch: 'semantic',
180
+ // DR-045: the unchanged receipt observes the commit the branch
181
+ // was created from as HEAD; the reconciler injects it here.
182
+ baseRevision: 'effect',
183
+ issueSummary: 'semantic',
184
+ },
185
+ repositoryDisposition: 'unchanged',
186
+ },
187
+ refused: {
188
+ fields: { coderOutput: 'presentation' },
189
+ repositoryDisposition: 'unchanged',
190
+ },
191
+ needsBossReply: {
192
+ fields: { question: 'presentation' },
193
+ repositoryDisposition: 'unchanged',
194
+ },
195
+ },
196
+ },
197
+ },
198
+ composePlayerPrompt: (input: PlaybookPlayerInput, _identity, resuming) =>
199
+ composePlayerPrompt(input as PlayerInput, resuming),
200
+ verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
201
+ controlContextFields: [],
202
+ unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
203
+ transitionEventFields: ['callerInput', 'answer', 'questionId'],
204
+ } satisfies XStatePlaybookRuntimeSpecV3<BranchPlaybookOptions>;
205
+
206
+ const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
207
+ XStatePlaybookRuntimeConstruction<
208
+ BranchPlaybookOptions,
209
+ BranchPlaybookHostCapabilities
210
+ >,
211
+ 3
212
+ > = createXStatePlaybookRuntime<
213
+ BranchPlaybookOptions,
214
+ BranchPlaybookHostCapabilities
215
+ >(branchMachine, runtimeSpec);
216
+
217
+ export default createPlaybookRuntime;
@@ -0,0 +1,40 @@
1
+ import { type BranchPlaybookHostCapabilities, type PlaybookRuntime } from './branch.playbook.js';
2
+ export interface PlaybookSummaryPolicy {
3
+ stateCountLabels: Readonly<Record<string, string>>;
4
+ copyPasteGuardNames: readonly string[];
5
+ savedCountsLine(counts: {
6
+ interruptions: number;
7
+ copyPastes: number;
8
+ }, rounds: number): string;
9
+ }
10
+ export type BranchOptions = Readonly<Record<string, never>>;
11
+ export interface BranchPlaybookRegistryEntry {
12
+ id: 'branch';
13
+ command: 'branch';
14
+ intent: string;
15
+ artifactSchema: 3;
16
+ runtimeProfile: {
17
+ readonly kind: 'shared-factory';
18
+ readonly compat: {
19
+ readonly artifactSchema: 3;
20
+ readonly runtimeAbi: number;
21
+ };
22
+ };
23
+ requiredRoleIds: readonly ['coder'];
24
+ concurrentRoleSets: readonly [];
25
+ summaryPolicy: PlaybookSummaryPolicy;
26
+ validateOptions(optionSlice: unknown): BranchOptions;
27
+ createRuntime(options: BranchOptions, hostCapabilities: BranchPlaybookHostCapabilities): PlaybookRuntime;
28
+ }
29
+ export declare const branchStateCountLabels: {
30
+ readonly createBranch: "branching round";
31
+ };
32
+ export declare const branchCopyPasteGuardNames: readonly ["branched"];
33
+ export declare function branchSavedCountsLine(counts: {
34
+ interruptions: number;
35
+ copyPastes: number;
36
+ }, rounds: number): string;
37
+ export declare const branchSummaryPolicy: PlaybookSummaryPolicy;
38
+ export declare function validateBranchOptions(optionSlice: unknown): BranchOptions;
39
+ export declare const branchPlaybookRegistryEntry: BranchPlaybookRegistryEntry;
40
+ export default branchPlaybookRegistryEntry;
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ import createPlaybookRuntime from './branch.playbook.js';
4
+ // BRANCH's one Coder call is its only round: the branching call that reads
5
+ // the issue and creates and checks out the branch.
6
+ export const branchStateCountLabels = {
7
+ createBranch: 'branching round',
8
+ };
9
+ export const branchCopyPasteGuardNames = ['branched'];
10
+ function countNoun(count, singular, plural = `${singular}s`) {
11
+ return `${count} ${count === 1 ? singular : plural}`;
12
+ }
13
+ export function branchSavedCountsLine(counts, rounds) {
14
+ return [
15
+ 'Saved you',
16
+ countNoun(counts.interruptions, 'interruption'),
17
+ 'and',
18
+ countNoun(counts.copyPastes, 'copy-paste'),
19
+ 'across',
20
+ countNoun(rounds, 'round'),
21
+ 'of branching.',
22
+ ].join(' ');
23
+ }
24
+ export const branchSummaryPolicy = {
25
+ stateCountLabels: branchStateCountLabels,
26
+ copyPasteGuardNames: branchCopyPasteGuardNames,
27
+ savedCountsLine: branchSavedCountsLine,
28
+ };
29
+ export function validateBranchOptions(optionSlice) {
30
+ if (optionSlice === undefined)
31
+ return Object.freeze({});
32
+ if (optionSlice === null ||
33
+ typeof optionSlice !== 'object' ||
34
+ Array.isArray(optionSlice)) {
35
+ throw new Error('captain.options.playbooks.branch.options must be an object');
36
+ }
37
+ const keys = Object.keys(optionSlice);
38
+ if (keys.length > 0) {
39
+ throw new Error(`Unknown config field captain.options.playbooks.branch.options.${keys[0]}`);
40
+ }
41
+ return Object.freeze({});
42
+ }
43
+ export const branchPlaybookRegistryEntry = {
44
+ id: 'branch',
45
+ command: 'branch',
46
+ intent: 'create and check out a new branch for a GitHub issue or request before pull-request delivery',
47
+ artifactSchema: 3,
48
+ runtimeProfile: Object.freeze({
49
+ kind: 'shared-factory',
50
+ compat: createPlaybookRuntime.compat,
51
+ }),
52
+ requiredRoleIds: ['coder'],
53
+ concurrentRoleSets: [],
54
+ summaryPolicy: branchSummaryPolicy,
55
+ validateOptions: validateBranchOptions,
56
+ createRuntime(options, hostCapabilities) {
57
+ return createPlaybookRuntime({
58
+ configuredOptions: options,
59
+ hostCapabilities,
60
+ });
61
+ },
62
+ };
63
+ export default branchPlaybookRegistryEntry;
@@ -0,0 +1,121 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ import createPlaybookRuntime, {
5
+ type BranchPlaybookHostCapabilities,
6
+ type PlaybookRuntime,
7
+ } from './branch.playbook.js';
8
+
9
+ export interface PlaybookSummaryPolicy {
10
+ stateCountLabels: Readonly<Record<string, string>>;
11
+ copyPasteGuardNames: readonly string[];
12
+ savedCountsLine(
13
+ counts: { interruptions: number; copyPastes: number },
14
+ rounds: number,
15
+ ): string;
16
+ }
17
+
18
+ export type BranchOptions = Readonly<Record<string, never>>;
19
+
20
+ export interface BranchPlaybookRegistryEntry {
21
+ id: 'branch';
22
+ command: 'branch';
23
+ intent: string;
24
+ artifactSchema: 3;
25
+ runtimeProfile: {
26
+ readonly kind: 'shared-factory';
27
+ readonly compat: {
28
+ readonly artifactSchema: 3;
29
+ readonly runtimeAbi: number;
30
+ };
31
+ };
32
+ requiredRoleIds: readonly ['coder'];
33
+ concurrentRoleSets: readonly [];
34
+ summaryPolicy: PlaybookSummaryPolicy;
35
+ validateOptions(optionSlice: unknown): BranchOptions;
36
+ createRuntime(
37
+ options: BranchOptions,
38
+ hostCapabilities: BranchPlaybookHostCapabilities,
39
+ ): PlaybookRuntime;
40
+ }
41
+
42
+ // BRANCH's one Coder call is its only round: the branching call that reads
43
+ // the issue and creates and checks out the branch.
44
+ export const branchStateCountLabels = {
45
+ createBranch: 'branching round',
46
+ } as const;
47
+
48
+ export const branchCopyPasteGuardNames = ['branched'] as const;
49
+
50
+ function countNoun(
51
+ count: number,
52
+ singular: string,
53
+ plural = `${singular}s`,
54
+ ): string {
55
+ return `${count} ${count === 1 ? singular : plural}`;
56
+ }
57
+
58
+ export function branchSavedCountsLine(
59
+ counts: { interruptions: number; copyPastes: number },
60
+ rounds: number,
61
+ ): string {
62
+ return [
63
+ 'Saved you',
64
+ countNoun(counts.interruptions, 'interruption'),
65
+ 'and',
66
+ countNoun(counts.copyPastes, 'copy-paste'),
67
+ 'across',
68
+ countNoun(rounds, 'round'),
69
+ 'of branching.',
70
+ ].join(' ');
71
+ }
72
+
73
+ export const branchSummaryPolicy: PlaybookSummaryPolicy = {
74
+ stateCountLabels: branchStateCountLabels,
75
+ copyPasteGuardNames: branchCopyPasteGuardNames,
76
+ savedCountsLine: branchSavedCountsLine,
77
+ };
78
+
79
+ export function validateBranchOptions(optionSlice: unknown): BranchOptions {
80
+ if (optionSlice === undefined) return Object.freeze({});
81
+ if (
82
+ optionSlice === null ||
83
+ typeof optionSlice !== 'object' ||
84
+ Array.isArray(optionSlice)
85
+ ) {
86
+ throw new Error(
87
+ 'captain.options.playbooks.branch.options must be an object',
88
+ );
89
+ }
90
+ const keys = Object.keys(optionSlice);
91
+ if (keys.length > 0) {
92
+ throw new Error(
93
+ `Unknown config field captain.options.playbooks.branch.options.${keys[0]}`,
94
+ );
95
+ }
96
+ return Object.freeze({});
97
+ }
98
+
99
+ export const branchPlaybookRegistryEntry: BranchPlaybookRegistryEntry = {
100
+ id: 'branch',
101
+ command: 'branch',
102
+ intent:
103
+ 'create and check out a new branch for a GitHub issue or request before pull-request delivery',
104
+ artifactSchema: 3,
105
+ runtimeProfile: Object.freeze({
106
+ kind: 'shared-factory',
107
+ compat: createPlaybookRuntime.compat,
108
+ }),
109
+ requiredRoleIds: ['coder'],
110
+ concurrentRoleSets: [],
111
+ summaryPolicy: branchSummaryPolicy,
112
+ validateOptions: validateBranchOptions,
113
+ createRuntime(options, hostCapabilities) {
114
+ return createPlaybookRuntime({
115
+ configuredOptions: options,
116
+ hostCapabilities,
117
+ });
118
+ },
119
+ };
120
+
121
+ export default branchPlaybookRegistryEntry;
@@ -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;
@@ -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. */